bohdan

Finished
1function wrap(text, tag) {
2 return `<${tag}>${text}</${tag}>`;
3}
4
5function isTag(text, tag) {
6 return text.startsWith(`<${tag}>`);
7}
8
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}
14
15function parse__(markdown) {
16 return parser(markdown, '__', 'strong');
17}
18
19function parse_(markdown) {
20 return parser(markdown, '_', 'em');
21}
22
23function parseText(markdown, list) {
24 const parsedText = parse_(parse__(markdown));
25 if (list) {
26 return parsedText;
27 } else {
28 return wrap(parsedText, 'p');
1/**
2 * A Markdown parser that converts Markdown syntax to HTML.
3 * Supports headers (# ## ###), unordered lists (*), paragraphs, and text formatting (__ for bold, _ for italic).
4 */
5class MarkdownParser {
6 /**
7 * Parses a markdown string and returns equivalent HTML.
8 * @param {string} markdown - The markdown text to parse
9 * @returns {string} - The resulting HTML
10 */
11 parse(markdown) {
12 const lines = markdown.split('\n');
13 let html = '';
14 let inUnorderedList = false;
15
16 for (const line of lines) {
17 let [lineHtml, newListState] = this.parseLine(line, inUnorderedList);
18 html += lineHtml;
19 inUnorderedList = newListState;
20 }
21
22 // Close any open list at the end
23 if (inUnorderedList) {
24 html += '</ul>';
25 }
26
27 return html;
2928 }
30}
31
32function parseHeader(markdown, list) {
33 let count = 0;
34 for (let i = 0; i < markdown.length; i++) {
35 if (markdown[i] === '#') {
36 count += 1;
29
30 /**
31 * Parses a single line of markdown.
32 * @param {string} line - The line to parse
33 * @param {boolean} inUnorderedList - Whether we're currently in an unordered list
34 * @returns {[string, boolean]} - The HTML for this line and the new list state
35 */
36 parseLine(line, inUnorderedList) {
37 // Try to parse as header
38 const headerResult = this.parseHeader(line, inUnorderedList);
39 if (headerResult[0] !== null) {
40 return headerResult;
41 }
42
43 // Try to parse as list item
44 const listItemResult = this.parseListItem(line, inUnorderedList);
45 if (listItemResult[0] !== null) {
46 return listItemResult;
47 }
48
49 // Parse as paragraph
50 return this.parseParagraph(line, inUnorderedList);
51 }
52
53 /**
54 * Parses a header line (# ## ### etc.)
55 * @param {string} line - The line to parse
56 * @param {boolean} inUnorderedList - Whether we're currently in an unordered list
57 * @returns {[string|null, boolean]} - The HTML for this header (or null if not a header) and the new list state
58 */
59 parseHeader(line, inUnorderedList) {
60 let hashCount = 0;
61 for (let i = 0; i < line.length; i++) {
62 if (line[i] === '#') {
63 hashCount += 1;
64 } else {
65 break;
66 }
67 }
68
69 // Not a header or invalid header level
70 if (hashCount === 0 || hashCount > 6) {
71 return [null, inUnorderedList];
72 }
73
74 const headerTag = `h${hashCount}`;
75 const headerContent = line.substring(hashCount + 1);
76 const headerHtml = this.wrap(headerContent, headerTag);
77
78 // If we were in a list, close it before the header
79 if (inUnorderedList) {
80 return [`</ul>${headerHtml}`, false];
3781 } else {
38 break;
82 return [headerHtml, false];
3983 }
4084 }
41 if (count === 0 || count > 6) {
42 return [null, list];
85
86 /**
87 * Parses a list item line (* item)
88 * @param {string} line - The line to parse
89 * @param {boolean} inUnorderedList - Whether we're currently in an unordered list
90 * @returns {[string|null, boolean]} - The HTML for this list item (or null if not a list item) and the new list state
91 */
92 parseListItem(line, inUnorderedList) {
93 if (!line.startsWith('*')) {
94 return [null, inUnorderedList];
95 }
96
97 const listItemContent = line.substring(2); // Skip "* "
98 const parsedText = this.parseText(listItemContent, true);
99 const listItemHtml = this.wrap(parsedText, 'li');
100
101 if (inUnorderedList) {
102 // Continue existing list
103 return [listItemHtml, true];
104 } else {
105 // Start new list
106 return [`<ul>${listItemHtml}`, true];
107 }
43108 }
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];
109
110 /**
111 * Parses a paragraph line
112 * @param {string} line - The line to parse
113 * @param {boolean} inUnorderedList - Whether we're currently in an unordered list
114 * @returns {[string, boolean]} - The HTML for this paragraph and the new list state
115 */
116 parseParagraph(line, inUnorderedList) {
117 const parsedText = this.parseText(line, false);
118
119 // If we were in a list, close it before the paragraph
120 if (inUnorderedList) {
121 return [`</ul>${parsedText}`, false];
122 } else {
123 return [parsedText, false];
124 }
50125 }
51}
52
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];
126
127 /**
128 * Parses text formatting (__ for bold, _ for italic) and wraps in paragraph if needed
129 * @param {string} text - The text to parse
130 * @param {boolean} inList - Whether this text is inside a list item
131 * @returns {string} - The parsed text, optionally wrapped in a paragraph
132 */
133 parseText(text, inList) {
134 const parsedText = this.parseItalic(this.parseBold(text));
135
136 if (inList) {
137 return parsedText;
58138 } else {
59 return [`<ul>${innerHtml}`, true];
139 return this.wrap(parsedText, 'p');
60140 }
61141 }
62 return [null, list];
63}
64
65function parseParagraph(markdown, list) {
66 if (!list) {
67 return [parseText(markdown, false), false];
68 } else {
69 return [`</ul>${parseText(markdown, false)}`, false];
142
143 /**
144 * Parses bold text (__text__ -> <strong>text</strong>)
145 * @param {string} text - The text to parse
146 * @returns {string} - The text with bold formatting applied
147 */
148 parseBold(text) {
149 return this.parseInline(text, '__', 'strong');
70150 }
71}
72
73function parseLine(markdown, list) {
74 let [result, inListAfter] = parseHeader(markdown, list);
75 if (result === null) {
76 [result, inListAfter] = parseLineItem(markdown, list);
151
152 /**
153 * Parses italic text (_text_ -> <em>text</em>)
154 * @param {string} text - The text to parse
155 * @returns {string} - The text with italic formatting applied
156 */
157 parseItalic(text) {
158 return this.parseInline(text, '_', 'em');
77159 }
78 if (result === null) {
79 [result, inListAfter] = parseParagraph(markdown, list);
160
161 /**
162 * Generic inline parser for text formatting
163 * @param {string} text - The text to parse
164 * @param {string} delimiter - The delimiter to look for (e.g., '__' or '_')
165 * @param {string} tag - The HTML tag to wrap matches in (e.g., 'strong' or 'em')
166 * @returns {string} - The text with formatting applied
167 */
168 parseInline(text, delimiter, tag) {
169 const pattern = new RegExp(`${delimiter}(.+)${delimiter}`);
170 const replacement = `<${tag}>$1</${tag}>`;
171 return text.replace(pattern, replacement);
80172 }
81 if (result === null) {
82 throw new Error('Remove this line and implement the function');
173
174 /**
175 * Wraps text in HTML tags
176 * @param {string} text - The text to wrap
177 * @param {string} tag - The HTML tag to wrap with
178 * @returns {string} - The wrapped text
179 */
180 wrap(text, tag) {
181 return `<${tag}>${text}</${tag}>`;
83182 }
84 return [result, inListAfter];
85183}
86184
87/**
88 * @param {string} markdown
89 * @returns {string}
90 */
185// Create a singleton instance and export the parse function for backward compatibility
186const parser = new MarkdownParser();
91187export 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 }
188 return parser.parse(markdown);
105189}
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.