awesome

Finished
1// Helper function to wrap text in HTML tags
12function wrap(text, tag) {
23 return `<${tag}>${text}</${tag}>`;
34}
45
5function isTag(text, tag) {
6 return text.startsWith(`<${tag}>`);
6// Helper function to parse bold text (__text__ -> <strong>text</strong>)
7function parseBold(markdown) {
8 return markdown.replace(/__(.+)__/g, '<strong>$1</strong>');
79}
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);
11// Helper function to parse italic text (_text_ -> <em>text</em>)
12function parseItalic(markdown) {
13 return markdown.replace(/_(.+)_/g, '<em>$1</em>');
1314}
1415
15function parse__(markdown) {
16 return parser(markdown, '__', 'strong');
16// Parse inline formatting (__ for bold, _ for italic)
17function parseInlineFormatting(markdown) {
18 // Apply both formatting rules, with bold processed first to handle nested formatting correctly
19 return parseItalic(parseBold(markdown));
1720}
1821
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 }
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;
22// Parse header (# Header -> <h1>Header</h1>)
23function parseHeader(line, currentlyInList) {
24 // Count consecutive # characters at the start of the line
25 let headerLevel = 0;
26 for (let i = 0; i < line.length; i++) {
27 if (line[i] === '#') {
28 headerLevel++;
3729 } else {
3830 break;
3931 }
4032 }
41 if (count === 0 || count > 6) {
42 return [null, list];
33
34 // Valid headers are h1-h6 (1-6 # characters)
35 if (headerLevel === 0 || headerLevel > 6) {
36 return [null, currentlyInList]; // Not a header
4337 }
44 const headerTag = `h${count}`;
45 const headerHtml = wrap(markdown.substring(count + 1), headerTag);
46 if (list) {
38
39 // Extract header content (text after # characters and space)
40 const headerContent = line.substring(headerLevel + 1);
41
42 // Create header HTML
43 const headerHtml = wrap(headerContent, `h${headerLevel}`);
44
45 // If we were in a list, close it first
46 if (currentlyInList) {
4747 return [`</ul>${headerHtml}`, false];
4848 } else {
4949 return [headerHtml, false];
5050 }
5151}
5252
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];
53// Parse list item (* item -> <li>item</li>)
54function parseListItem(line, currentlyInList) {
55 // Check if line starts with "* " (asterisk followed by space)
56 if (line.startsWith('* ')) {
57 // Extract list item content (text after "* ")
58 const itemContent = line.substring(2);
59
60 // Parse inline formatting in the list item content
61 const formattedContent = parseInlineFormatting(itemContent);
62
63 // Wrap in <li> tags
64 const listItemHtml = wrap(formattedContent, 'li');
65
66 // If we're already in a list, just add the list item
67 // Otherwise, start a new list
68 if (currentlyInList) {
69 return [listItemHtml, true];
5870 } else {
59 return [`<ul>${innerHtml}`, true];
71 return [`<ul>${listItemHtml}`, true];
6072 }
6173 }
62 return [null, list];
74
75 return [null, currentlyInList]; // Not a list item
6376}
6477
65function parseParagraph(markdown, list) {
66 if (!list) {
67 return [parseText(markdown, false), false];
78// Parse paragraph (default case for lines that aren't headers or list items)
79function parseParagraph(line, currentlyInList) {
80 // Parse inline formatting in the paragraph
81 const formattedContent = parseInlineFormatting(line);
82
83 // Wrap in <p> tags
84 const paragraphHtml = wrap(formattedContent, 'p');
85
86 // If we were in a list, close it first
87 if (currentlyInList) {
88 return [`</ul>${paragraphHtml}`, false];
6889 } else {
69 return [`</ul>${parseText(markdown, false)}`, false];
90 return [paragraphHtml, false];
7091 }
7192}
7293
73function parseLine(markdown, list) {
74 let [result, inListAfter] = parseHeader(markdown, list);
94// Parse a single line of markdown
95function parseLine(line, currentlyInList) {
96 // Try to parse as header first
97 let [result, newListState] = parseHeader(line, currentlyInList);
98
99 // If not a header, try to parse as list item
75100 if (result === null) {
76 [result, inListAfter] = parseLineItem(markdown, list);
101 [result, newListState] = parseListItem(line, currentlyInList);
77102 }
103
104 // If not a list item, parse as paragraph (default case)
78105 if (result === null) {
79 [result, inListAfter] = parseParagraph(markdown, list);
106 [result, newListState] = parseParagraph(line, currentlyInList);
80107 }
108
109 // This should never happen with valid input
81110 if (result === null) {
82 throw new Error('Remove this line and implement the function');
111 throw new Error('Unable to parse line: ' + line);
83112 }
84 return [result, inListAfter];
113
114 return [result, newListState];
85115}
86116
87117/**
88 * @param {string} markdown
89 * @returns {string}
118 * Parse markdown text and convert to HTML
119 * @param {string} markdown - The markdown text to parse
120 * @returns {string} - The resulting HTML
90121 */
91122export function parse(markdown) {
123 // Split input into lines
92124 const lines = markdown.split('\n');
125
126 // Result HTML string
93127 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;
128
129 // Track whether we're currently inside a list
130 let inList = false;
131
132 // Process each line
133 for (const line of lines) {
134 // Parse the line and get the HTML result plus updated list state
135 const [lineHtml, newInListState] = parseLine(line, inList);
136
137 // Add the line's HTML to the result
138 result += lineHtml;
139
140 // Update our list state
141 inList = newInListState;
99142 }
100 if (list) {
101 return result + '</ul>';
102 } else {
103 return result;
143
144 // If we're still in a list at the end, close it
145 if (inList) {
146 result += '</ul>';
104147 }
148
149 return result;
105150}
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.