Finished
1// Utility function to wrap text in HTML tags
12function wrap(text, tag) {
23 return `<${tag}>${text}</${tag}>`;
34}
45
6// Check if text starts with a specific HTML tag
57function isTag(text, tag) {
68 return text.startsWith(`<${tag}>`);
79}
810
9function parser(markdown, delimiter, tag) {
10 const pattern = new RegExp(`${delimiter}(.+)${delimiter}`);
11// Generic parser for delimiter-based markdown (bold, italic, etc.)
12function parseDelimitedText(markdown, delimiter, tag) {
13 const pattern = new RegExp(`${delimiter}(.+?)${delimiter}`);
1114 const replacement = `<${tag}>$1</${tag}>`;
1215 return markdown.replace(pattern, replacement);
1316}
1417
15function parse__(markdown) {
16 return parser(markdown, '__', 'strong');
18// Parse bold text (using __ delimiter)
19function parseBoldText(markdown) {
20 return parseDelimitedText(markdown, '__', 'strong');
1721}
1822
19function parse_(markdown) {
20 return parser(markdown, '_', 'em');
23// Parse italic text (using _ delimiter)
24function parseItalicText(markdown) {
25 return parseDelimitedText(markdown, '_', 'em');
2126}
2227
23function parseText(markdown, list) {
24 const parsedText = parse_(parse__(markdown));
25 if (list) {
26 return parsedText;
27 } else {
28 return wrap(parsedText, 'p');
29 }
28// Parse inline formatting (bold and italic) within text
29function parseInlineFormatting(markdown) {
30 // Parse bold first, then italic to handle nested formatting correctly
31 return parseItalicText(parseBoldText(markdown));
3032}
3133
32function parseHeader(markdown, list) {
33 let count = 0;
34 for (let i = 0; i < markdown.length; i++) {
35 if (markdown[i] === '#') {
36 count += 1;
37 } else {
38 break;
39 }
40 }
41 if (count === 0 || count > 6) {
42 return [null, list];
34// Parse text content with optional paragraph wrapping
35function parseTextContent(markdown, skipParagraphWrap = false) {
36 const parsedText = parseInlineFormatting(markdown);
37 return skipParagraphWrap ? parsedText : wrap(parsedText, 'p');
38}
39
40// Parse markdown headers (#, ##, ###, etc.)
41function parseHeader(markdown, currentlyInList) {
42 // Count consecutive # characters at the start of the line
43 const headerMatch = markdown.match(/^(#{1,6})\s+(.+)$/);
44
45 // If no valid header pattern found, return null
46 if (!headerMatch) {
47 return [null, currentlyInList];
4348 }
44 const headerTag = `h${count}`;
45 const headerHtml = wrap(markdown.substring(count + 1), headerTag);
46 if (list) {
49
50 const headerLevel = headerMatch[1].length; // Number of # characters
51 const headerText = headerMatch[2]; // Text content after #
52
53 const headerTag = `h${headerLevel}`;
54 const headerHtml = wrap(headerText, headerTag);
55
56 // If we were in a list, close it before starting the header
57 if (currentlyInList) {
4758 return [`</ul>${headerHtml}`, false];
4859 } else {
4960 return [headerHtml, false];
5061 }
5162}
5263
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 }
64// Parse list items (lines starting with *)
65function parseListItem(markdown, currentlyInList) {
66 // Check if line starts with a list item marker
67 if (!markdown.startsWith('*')) {
68 return [null, currentlyInList];
69 }
70
71 // Extract the list item content (skip "* " prefix)
72 const listItemContent = markdown.substring(2);
73
74 // Parse inline formatting within the list item
75 const formattedContent = parseTextContent(listItemContent, true);
76
77 // Wrap in list item tag
78 const listItemHtml = wrap(formattedContent, 'li');
79
80 // If we weren't already in a list, start a new unordered list
81 if (!currentlyInList) {
82 return [`<ul>${listItemHtml}`, true];
83 } else {
84 return [listItemHtml, true];
6185 }
62 return [null, list];
6386}
6487
65function parseParagraph(markdown, list) {
66 if (!list) {
67 return [parseText(markdown, false), false];
88// Parse regular paragraphs (fallback when not header or list item)
89function parseParagraph(markdown, currentlyInList) {
90 const paragraphHtml = parseTextContent(markdown, false);
91
92 // If we were in a list, close it before starting the paragraph
93 if (currentlyInList) {
94 return [`</ul>${paragraphHtml}`, false];
6895 } else {
69 return [`</ul>${parseText(markdown, false)}`, false];
96 return [paragraphHtml, false];
7097 }
7198}
7299
73function parseLine(markdown, list) {
74 let [result, inListAfter] = parseHeader(markdown, list);
100// Parse a single line of markdown, determining its type and converting to HTML
101function parseLine(markdown, currentlyInList) {
102 // Try to parse as header first (highest priority)
103 let [result, inListAfter] = parseHeader(markdown, currentlyInList);
104
105 // If not a header, try to parse as list item
75106 if (result === null) {
76 [result, inListAfter] = parseLineItem(markdown, list);
107 [result, inListAfter] = parseListItem(markdown, currentlyInList);
77108 }
109
110 // If not a list item, parse as paragraph (fallback)
78111 if (result === null) {
79 [result, inListAfter] = parseParagraph(markdown, list);
112 [result, inListAfter] = parseParagraph(markdown, currentlyInList);
80113 }
114
115 // This should never happen since parseParagraph always returns a result
81116 if (result === null) {
82 throw new Error('Remove this line and implement the function');
117 throw new Error('Unable to parse markdown line');
83118 }
119
84120 return [result, inListAfter];
85121}
86122
87123/**
88 * @param {string} markdown
89 * @returns {string}
124 * Parse markdown text and convert it to HTML
125 * @param {string} markdown - The markdown text to parse
126 * @returns {string} The equivalent HTML
90127 */
91128export function parse(markdown) {
92129 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;
130 let htmlResult = '';
131 let currentlyInList = false;
132
133 // Process each line of markdown
134 for (const line of lines) {
135 const [lineHtml, stillInList] = parseLine(line, currentlyInList);
136 htmlResult += lineHtml;
137 currentlyInList = stillInList;
99138 }
100 if (list) {
101 return result + '</ul>';
102 } else {
103 return result;
139
140 // If we ended while still in a list, close it
141 if (currentlyInList) {
142 htmlResult += '</ul>';
104143 }
144
145 return htmlResult;
105146}
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.