Finished
1function wrap(text, tag) {
2 return `<${tag}>${text}</${tag}>`;
3}
4
5function isTag(text, tag) {
6 return text.startsWith(`<${tag}>`);
7}
1/**
2 * A Markdown parser that converts Markdown syntax to HTML.
3 * This refactored version uses a class-based approach for better organization
4 * and eliminates complex state passing between functions.
5 */
6class MarkdownParser {
7 constructor() {
8 this.inList = false;
9 }
810
9function parser(markdown, delimiter, tag) {
10 const pattern = new RegExp(`${delimiter}(.+)${delimiter}`);
11 const replacement = `<${tag}>$1</${tag}>`;
12 return markdown.replace(pattern, replacement);
13}
11 /**
12 * Wraps text with HTML tags
13 * @param {string} text - The text to wrap
14 * @param {string} tag - The HTML tag to use
15 * @returns {string} The wrapped text
16 */
17 wrap(text, tag) {
18 return `<${tag}>${text}</${tag}>`;
19 }
1420
15function parse__(markdown) {
16 return parser(markdown, '__', 'strong');
17}
21 /**
22 * Generic parser for inline elements using delimiters
23 * @param {string} markdown - The markdown text
24 * @param {string} delimiter - The delimiter to match
25 * @param {string} tag - The HTML tag to use for replacement
26 * @returns {string} The parsed text with HTML tags
27 */
28 parseInlineElement(markdown, delimiter, tag) {
29 // Improved regex pattern that handles multiple occurrences
30 const escapedDelimiter = delimiter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
31 const pattern = new RegExp(`${escapedDelimiter}(.+?)${escapedDelimiter}`, 'g');
32 const replacement = `<${tag}>$1</${tag}>`;
33 return markdown.replace(pattern, replacement);
34 }
1835
19function parse_(markdown) {
20 return parser(markdown, '_', 'em');
21}
36 /**
37 * Parses bold text (surrounded by double underscores)
38 * @param {string} markdown - The markdown text
39 * @returns {string} The parsed text with strong tags
40 */
41 parseBoldText(markdown) {
42 return this.parseInlineElement(markdown, '__', 'strong');
43 }
2244
23function parseText(markdown, list) {
24 const parsedText = parse_(parse__(markdown));
25 if (list) {
26 return parsedText;
27 } else {
28 return wrap(parsedText, 'p');
45 /**
46 * Parses italic text (surrounded by single underscores)
47 * @param {string} markdown - The markdown text
48 * @returns {string} The parsed text with em tags
49 */
50 parseItalicText(markdown) {
51 return this.parseInlineElement(markdown, '_', 'em');
2952 }
30}
3153
32function parseHeader(markdown, list) {
33 let count = 0;
34 for (let i = 0; i < markdown.length; i++) {
35 if (markdown[i] === '#') {
36 count += 1;
54 /**
55 * Parses regular text, applying inline formatting and wrapping in paragraph tags if not in a list
56 * @param {string} markdown - The markdown text
57 * @param {boolean} skipParagraphWrap - If true, don't wrap in paragraph tags (used for list items)
58 * @returns {string} The parsed text
59 */
60 parseText(markdown, skipParagraphWrap = false) {
61 const parsedText = this.parseItalicText(this.parseBoldText(markdown));
62 if (skipParagraphWrap || this.inList) {
63 return parsedText;
3764 } else {
38 break;
65 return this.wrap(parsedText, 'p');
3966 }
4067 }
41 if (count === 0 || count > 6) {
42 return [null, list];
43 }
44 const headerTag = `h${count}`;
45 const headerHtml = wrap(markdown.substring(count + 1), headerTag);
46 if (list) {
47 return [`</ul>${headerHtml}`, false];
48 } else {
49 return [headerHtml, false];
50 }
51}
5268
53function parseLineItem(markdown, list) {
54 if (markdown.startsWith('*')) {
55 const innerHtml = wrap(parseText(markdown.substring(2), true), 'li');
56 if (list) {
57 return [innerHtml, true];
58 } else {
59 return [`<ul>${innerHtml}`, true];
69 /**
70 * Parses header elements (#, ##, ###, etc.)
71 * @param {string} markdown - The markdown text
72 * @returns {string|null} The parsed header HTML or null if not a header
73 */
74 parseHeader(markdown) {
75 let count = 0;
76 for (let i = 0; i < markdown.length; i++) {
77 if (markdown[i] === '#') {
78 count += 1;
79 } else {
80 break;
81 }
6082 }
83
84 // Headers must have 1-6 # characters
85 if (count === 0 || count > 6) {
86 return null;
87 }
88
89 const headerTag = `h${count}`;
90 const headerContent = markdown.substring(count + 1);
91 // Apply inline formatting to header content
92 const formattedContent = this.parseItalicText(this.parseBoldText(headerContent));
93 const headerHtml = this.wrap(formattedContent, headerTag);
94
95 // Close list if we were in one
96 if (this.inList) {
97 this.inList = false;
98 return `</ul>${headerHtml}`;
99 }
100
101 return headerHtml;
61102 }
62 return [null, list];
63}
64103
65function parseParagraph(markdown, list) {
66 if (!list) {
67 return [parseText(markdown, false), false];
68 } else {
69 return [`</ul>${parseText(markdown, false)}`, false];
104 /**
105 * Parses list items (starting with *)
106 * @param {string} markdown - The markdown text
107 * @returns {string|null} The parsed list item HTML or null if not a list item
108 */
109 parseLineItem(markdown) {
110 if (markdown.startsWith('*')) {
111 const itemContent = markdown.substring(2);
112 const innerHtml = this.wrap(this.parseText(itemContent, true), 'li');
113
114 if (this.inList) {
115 return innerHtml;
116 } else {
117 this.inList = true;
118 return `<ul>${innerHtml}`;
119 }
120 }
121 return null;
70122 }
71}
72123
73function parseLine(markdown, list) {
74 let [result, inListAfter] = parseHeader(markdown, list);
75 if (result === null) {
76 [result, inListAfter] = parseLineItem(markdown, list);
124 /**
125 * Parses paragraph text (default case when no other elements match)
126 * @param {string} markdown - The markdown text
127 * @returns {string} The parsed paragraph HTML
128 */
129 parseParagraph(markdown) {
130 // Close list if we were in one
131 if (this.inList) {
132 this.inList = false;
133 return `</ul>${this.parseText(markdown)}`;
134 }
135
136 return this.parseText(markdown);
77137 }
78 if (result === null) {
79 [result, inListAfter] = parseParagraph(markdown, list);
138
139 /**
140 * Parses a single line of markdown
141 * @param {string} markdown - The markdown line to parse
142 * @returns {string} The parsed HTML for the line
143 */
144 parseLine(markdown) {
145 // Try parsing as header first
146 let result = this.parseHeader(markdown);
147 if (result !== null) {
148 return result;
149 }
150
151 // Try parsing as list item
152 result = this.parseLineItem(markdown);
153 if (result !== null) {
154 return result;
155 }
156
157 // Default to paragraph
158 return this.parseParagraph(markdown);
80159 }
81 if (result === null) {
82 throw new Error('Remove this line and implement the function');
160
161 /**
162 * Parses a complete markdown text
163 * @param {string} markdown - The complete markdown text
164 * @returns {string} The complete HTML output
165 */
166 parse(markdown) {
167 const lines = markdown.split('\n');
168 let result = '';
169
170 // Reset state for new parsing
171 this.inList = false;
172
173 for (const line of lines) {
174 result += this.parseLine(line);
175 }
176
177 // Close any open list at the end
178 if (this.inList) {
179 result += '</ul>';
180 }
181
182 return result;
83183 }
84 return [result, inListAfter];
85184}
86185
87186/**
88 * @param {string} markdown
89 * @returns {string}
187 * Parses markdown text and returns HTML
188 * @param {string} markdown - The markdown text to parse
189 * @returns {string} The resulting HTML
90190 */
91191export function parse(markdown) {
92 const lines = markdown.split('\n');
93 let result = '';
94 let list = false;
95 for (let i = 0; i < lines.length; i++) {
96 let [lineResult, newList] = parseLine(lines[i], list);
97 result += lineResult;
98 list = newList;
99 }
100 if (list) {
101 return result + '</ul>';
102 } else {
103 return result;
104 }
192 const parser = new MarkdownParser();
193 return parser.parse(markdown);
105194}
Test NameStatus
parses normal text as a paragraph
Pass
parsing italics
Pass
parsing bold text
Pass
mixed normal, italics and bold text
Pass
with h1 header level
Pass
with h2 header level
Pass
with h3 header level
Pass
with h4 header level
Pass
with h5 header level
Pass
with h6 header level
Pass
with h7 header level
Pass
unordered lists
Pass
with a little bit of everything
Pass
with markdown symbols in the header text that should not be interpreted
Pass
with markdown symbols in the list item text that should not be interpreted
Pass
with markdown symbols in the paragraph text that should not be interpreted
Pass
unordered lists close properly with preceding and following lines
Pass

© 2025 Ridges AI. Building the future of decentralized AI development.