Finished
1// Helper function to wrap text with HTML tags
12function wrap(text, tag) {
23 return `<${tag}>${text}</${tag}>`;
34}
45
5function isTag(text, tag) {
6 return text.startsWith(`<${tag}>`);
7}
8
6// Generic parser for inline formatting (bold, italic)
97function parser(markdown, delimiter, tag) {
108 const pattern = new RegExp(`${delimiter}(.+)${delimiter}`);
119 const replacement = `<${tag}>$1</${tag}>`;
1210 return markdown.replace(pattern, replacement);
1311}
1412
15function parse__(markdown) {
13// Parse bold text (__text__ -> <strong>text</strong>)
14function parseBold(markdown) {
1615 return parser(markdown, '__', 'strong');
1716}
1817
19function parse_(markdown) {
18// Parse italic text (_text_ -> <em>text</em>)
19function parseItalic(markdown) {
2020 return parser(markdown, '_', 'em');
2121}
2222
23function parseText(markdown, list) {
24 const parsedText = parse_(parse__(markdown));
25 if (list) {
23// Parse regular text with inline formatting
24function parseText(markdown, inList) {
25 // Apply inline formatting (bold, italic)
26 const parsedText = parseItalic(parseBold(markdown));
27
28 // If we're inside a list, don't wrap in paragraph tags
29 if (inList) {
2630 return parsedText;
2731 } else {
2832 return wrap(parsedText, 'p');
2933 }
3034}
3135
32function parseHeader(markdown, list) {
33 let count = 0;
36// Parse Markdown headers (# ## ### etc.)
37function parseHeader(markdown, inList) {
38 let headerLevel = 0;
39
40 // Count consecutive # characters at the start
3441 for (let i = 0; i < markdown.length; i++) {
3542 if (markdown[i] === '#') {
36 count += 1;
43 headerLevel += 1;
3744 } else {
3845 break;
3946 }
4047 }
41 if (count === 0 || count > 6) {
42 return [null, list];
48
49 // Valid headers have 1-6 # characters
50 if (headerLevel === 0 || headerLevel > 6) {
51 return [null, inList];
4352 }
44 const headerTag = `h${count}`;
45 const headerHtml = wrap(markdown.substring(count + 1), headerTag);
46 if (list) {
53
54 const headerTag = `h${headerLevel}`;
55 const headerContent = markdown.substring(headerLevel + 1).trim();
56 const headerHtml = wrap(headerContent, headerTag);
57
58 // If we were in a list, close it before the header
59 if (inList) {
4760 return [`</ul>${headerHtml}`, false];
4861 } else {
4962 return [headerHtml, false];
5063 }
5164}
5265
53function parseLineItem(markdown, list) {
66// Parse list items (* item)
67function parseLineItem(markdown, inList) {
5468 if (markdown.startsWith('*')) {
55 const innerHtml = wrap(parseText(markdown.substring(2), true), 'li');
56 if (list) {
57 return [innerHtml, true];
69 const listItemContent = markdown.substring(2); // Skip '* ' prefix
70 const formattedContent = parseText(listItemContent, true); // Parse inline formatting
71 const listItemHtml = wrap(formattedContent, 'li');
72
73 // If we're already in a list, just add the list item
74 // Otherwise, start a new unordered list
75 if (inList) {
76 return [listItemHtml, true];
5877 } else {
59 return [`<ul>${innerHtml}`, true];
78 return [`<ul>${listItemHtml}`, true];
6079 }
6180 }
62 return [null, list];
81 return [null, inList];
6382}
6483
65function parseParagraph(markdown, list) {
66 if (!list) {
67 return [parseText(markdown, false), false];
84// Parse regular paragraphs
85function parseParagraph(markdown, inList) {
86 const paragraphHtml = parseText(markdown, false);
87
88 // If we were in a list, close it before the paragraph
89 if (inList) {
90 return [`</ul>${paragraphHtml}`, false];
6891 } else {
69 return [`</ul>${parseText(markdown, false)}`, false];
92 return [paragraphHtml, false];
7093 }
7194}
7295
73function parseLine(markdown, list) {
74 let [result, inListAfter] = parseHeader(markdown, list);
96// Parse a single line of Markdown
97function parseLine(markdown, inList) {
98 // Try to parse as header
99 let [result, inListAfter] = parseHeader(markdown, inList);
100
101 // If not a header, try to parse as list item
75102 if (result === null) {
76 [result, inListAfter] = parseLineItem(markdown, list);
103 [result, inListAfter] = parseLineItem(markdown, inList);
77104 }
105
106 // If not a list item, parse as regular paragraph
78107 if (result === null) {
79 [result, inListAfter] = parseParagraph(markdown, list);
108 [result, inListAfter] = parseParagraph(markdown, inList);
80109 }
110
111 // This should never happen with valid input
81112 if (result === null) {
82113 throw new Error('Remove this line and implement the function');
83114 }
115
84116 return [result, inListAfter];
85117}
86118
87119/**
88 * @param {string} markdown
89 * @returns {string}
120 * Parse Markdown text and convert it to HTML
121 * @param {string} markdown - The Markdown text to parse
122 * @returns {string} - The resulting HTML
90123 */
91124export function parse(markdown) {
92125 const lines = markdown.split('\n');
93 let result = '';
94 let list = false;
126 let htmlResult = '';
127 let inList = false; // Track if we're currently inside a list
128
129 // Process each line of the Markdown text
95130 for (let i = 0; i < lines.length; i++) {
96 let [lineResult, newList] = parseLine(lines[i], list);
97 result += lineResult;
98 list = newList;
131 let [lineHtml, newListState] = parseLine(lines[i], inList);
132 htmlResult += lineHtml;
133 inList = newListState;
99134 }
100 if (list) {
101 return result + '</ul>';
135
136 // If we're still in a list at the end, close it
137 if (inList) {
138 return htmlResult + '</ul>';
102139 } else {
103 return result;
140 return htmlResult;
104141 }
105142}
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.