xas

Finished
1/**
2 * A refactored Markdown parser that converts Markdown syntax to HTML.
3 *
4 * Features supported:
5 * - Headers (# H1, ## H2, etc.)
6 * - Paragraphs (separated by blank lines)
7 * - Unordered lists (* item)
8 * - Bold text (__bold__)
9 * - Italic text (_italic_)
10 */
11
12/**
13 * Wraps text with HTML tags
14 * @param {string} text - Text to wrap
15 * @param {string} tag - HTML tag to use
16 * @returns {string} - Text wrapped in HTML tags
17 */
118function wrap(text, tag) {
219 return `<${tag}>${text}</${tag}>`;
320}
421
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');
29 }
22/**
23 * Parses inline formatting elements (__ for strong, _ for emphasis)
24 * @param {string} markdown - Text to parse
25 * @returns {string} - HTML with inline formatting applied
26 */
27function parseInlineFormatting(markdown) {
28 // Parse bold (__text__) first
29 let result = markdown.replace(/__(.+?)__/g, '<strong>$1</strong>');
30 // Then parse italic (_text_)
31 result = result.replace(/_(.+?)_/g, '<em>$1</em>');
32 return result;
3033}
3134
32function parseHeader(markdown, list) {
35/**
36 * Parses a header line
37 * @param {string} line - Line to parse
38 * @returns {string|null} - HTML header or null if not a header
39 */
40function parseHeader(line) {
41 // Count leading # characters
3342 let count = 0;
34 for (let i = 0; i < markdown.length; i++) {
35 if (markdown[i] === '#') {
43 for (let i = 0; i < line.length; i++) {
44 if (line[i] === '#') {
3645 count += 1;
3746 } else {
3847 break;
3948 }
4049 }
50
51 // Valid headers have 1-6 # characters
4152 if (count === 0 || count > 6) {
42 return [null, list];
53 return null;
4354 }
55
56 // Extract header content (skip # and space)
57 const content = line.substring(count + 1);
4458 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 }
59 return wrap(parseInlineFormatting(content), headerTag);
5160}
5261
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];
60 }
62/**
63 * Parses a list item line
64 * @param {string} line - Line to parse
65 * @returns {string|null} - HTML list item or null if not a list item
66 */
67function parseListItem(line) {
68 if (!line.startsWith('*')) {
69 return null;
6170 }
62 return [null, list];
71
72 // Extract list item content (skip * and space)
73 const content = line.substring(2);
74 return wrap(parseInlineFormatting(content), 'li');
6375}
6476
65function parseParagraph(markdown, list) {
66 if (!list) {
67 return [parseText(markdown, false), false];
68 } else {
69 return [`</ul>${parseText(markdown, false)}`, false];
70 }
77/**
78 * Parses a paragraph line
79 * @param {string} line - Line to parse
80 * @returns {string} - HTML paragraph
81 */
82function parseParagraph(line) {
83 return wrap(parseInlineFormatting(line), 'p');
7184}
7285
73function parseLine(markdown, list) {
74 let [result, inListAfter] = parseHeader(markdown, list);
75 if (result === null) {
76 [result, inListAfter] = parseLineItem(markdown, list);
86/**
87 * Parses a single line of markdown
88 * @param {string} line - Line to parse
89 * @param {boolean} inList - Whether we're currently in a list
90 * @returns {[string, boolean]} - [HTML output, whether we're in a list after this line]
91 */
92function parseLine(line, inList) {
93 // Try to parse as header
94 const header = parseHeader(line);
95 if (header !== null) {
96 // Close list if we were in one
97 if (inList) {
98 return [`</ul>${header}`, false];
99 }
100 return [header, false];
77101 }
78 if (result === null) {
79 [result, inListAfter] = parseParagraph(markdown, list);
102
103 // Try to parse as list item
104 const listItem = parseListItem(line);
105 if (listItem !== null) {
106 if (inList) {
107 // Continue existing list
108 return [listItem, true];
109 } else {
110 // Start new list
111 return [`<ul>${listItem}`, true];
112 }
80113 }
81 if (result === null) {
82 throw new Error('Remove this line and implement the function');
114
115 // Parse as paragraph
116 const paragraph = parseParagraph(line);
117 if (inList) {
118 // Close list before paragraph
119 return [`</ul>${paragraph}`, false];
83120 }
84 return [result, inListAfter];
121 return [paragraph, false];
85122}
86123
87124/**
88 * @param {string} markdown
89 * @returns {string}
125 * Main function to parse markdown text into HTML
126 * @param {string} markdown - Markdown text to parse
127 * @returns {string} - Generated HTML
90128 */
91129export function parse(markdown) {
92130 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;
131 let html = '';
132 let inList = false;
133
134 // Process each line
135 for (const line of lines) {
136 const [lineHtml, newListState] = parseLine(line, inList);
137 html += lineHtml;
138 inList = newListState;
99139 }
100 if (list) {
101 return result + '</ul>';
102 } else {
103 return result;
140
141 // Close any open list at the end
142 if (inList) {
143 html += '</ul>';
104144 }
145
146 return html;
105147}
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.