perfect

Finished
1/**
2 * Parses Markdown syntax and returns equivalent HTML
3 * Supports headers (#), paragraphs, lists (*), bold (__), and italic (_)
4 */
5
6/**
7 * Wraps text with HTML tags
8 * @param {string} text - Text to wrap
9 * @param {string} tag - HTML tag to use
10 * @returns {string} - Text wrapped in HTML tags
11 */
112function wrap(text, tag) {
213 return `<${tag}>${text}</${tag}>`;
314}
415
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');
16/**
17 * Parses bold text (__text__ -> <strong>text</strong>)
18 * @param {string} markdown - Markdown text to parse
19 * @returns {string} - HTML with bold formatting
20 */
21function parseBold(markdown) {
22 // Use non-greedy matching to handle multiple bold sections correctly
23 const pattern = /__(.+?)__/g;
24 return markdown.replace(pattern, '<strong>$1</strong>');
1725}
1826
19function parse_(markdown) {
20 return parser(markdown, '_', 'em');
27/**
28 * Parses italic text (_text_ -> <em>text</em>)
29 * @param {string} markdown - Markdown text to parse
30 * @returns {string} - HTML with italic formatting
31 */
32function parseItalic(markdown) {
33 // Use non-greedy matching to handle multiple italic sections correctly
34 // Also ensure we don't match underscores inside words
35 const pattern = /_(.+?)_/g;
36 return markdown.replace(pattern, '<em>$1</em>');
2137}
2238
23function parseText(markdown, list) {
24 const parsedText = parse_(parse__(markdown));
25 if (list) {
26 return parsedText;
27 } else {
28 return wrap(parsedText, 'p');
29 }
39/**
40 * Parses inline formatting (bold and italic)
41 * @param {string} markdown - Markdown text to parse
42 * @returns {string} - HTML with inline formatting
43 */
44function parseInlineFormatting(markdown) {
45 // Parse bold first, then italic
46 return parseItalic(parseBold(markdown));
3047}
3148
32function parseHeader(markdown, list) {
33 let count = 0;
49/**
50 * Parses header markdown (# Header -> <h1>Header</h1>)
51 * @param {string} markdown - Markdown line to parse
52 * @returns {string|null} - HTML header or null if not a header
53 */
54function parseHeader(markdown) {
55 // Count leading # characters
56 let level = 0;
3457 for (let i = 0; i < markdown.length; i++) {
3558 if (markdown[i] === '#') {
36 count += 1;
59 level++;
3760 } else {
3861 break;
3962 }
4063 }
41 if (count === 0 || count > 6) {
42 return [null, list];
43 }
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];
64
65 // Valid headers have 1-6 # characters
66 if (level === 0 || level > 6) {
67 return null;
5068 }
69
70 // Extract header text (skip # characters and space)
71 const headerText = markdown.substring(level + 1);
72 const headerTag = `h${level}`;
73
74 return wrap(parseInlineFormatting(headerText), headerTag);
5175}
5276
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 }
77/**
78 * Parses list items (* item -> <li>item</li>)
79 * @param {string} markdown - Markdown line to parse
80 * @returns {string|null} - HTML list item or null if not a list item
81 */
82function parseListItem(markdown) {
83 if (!markdown.startsWith('* ')) {
84 return null;
6185 }
62 return [null, list];
86
87 // Extract list item text (skip '* ' prefix)
88 const itemText = markdown.substring(2);
89 return wrap(parseInlineFormatting(itemText), 'li');
6390}
6491
65function parseParagraph(markdown, list) {
66 if (!list) {
67 return [parseText(markdown, false), false];
68 } else {
69 return [`</ul>${parseText(markdown, false)}`, false];
70 }
92/**
93 * Parses a paragraph (plain text -> <p>text</p>)
94 * @param {string} markdown - Markdown line to parse
95 * @returns {string} - HTML paragraph
96 */
97function parseParagraph(markdown) {
98 return wrap(parseInlineFormatting(markdown), 'p');
7199}
72100
73function parseLine(markdown, list) {
74 let [result, inListAfter] = parseHeader(markdown, list);
75 if (result === null) {
76 [result, inListAfter] = parseLineItem(markdown, list);
101/**
102 * Parses a single line of markdown
103 * @param {string} line - Line of markdown to parse
104 * @param {boolean} inList - Whether we're currently in a list
105 * @returns {[string, boolean]} - [HTML result, new inList state]
106 */
107function parseLine(line, inList) {
108 // Try to parse as header
109 const header = parseHeader(line);
110 if (header !== null) {
111 // Close list if we were in one
112 if (inList) {
113 return [`</ul>${header}`, false];
114 }
115 return [header, false];
77116 }
78 if (result === null) {
79 [result, inListAfter] = parseParagraph(markdown, list);
117
118 // Try to parse as list item
119 const listItem = parseListItem(line);
120 if (listItem !== null) {
121 if (inList) {
122 // Continue existing list
123 return [listItem, true];
124 } else {
125 // Start new list
126 return [`<ul>${listItem}`, true];
127 }
80128 }
81 if (result === null) {
82 throw new Error('Remove this line and implement the function');
129
130 // Parse as paragraph
131 const paragraph = parseParagraph(line);
132 if (inList) {
133 // Close list before paragraph
134 return [`</ul>${paragraph}`, false];
83135 }
84 return [result, inListAfter];
136 return [paragraph, false];
85137}
86138
87139/**
88 * @param {string} markdown
89 * @returns {string}
140 * Parses Markdown syntax and returns equivalent HTML
141 * @param {string} markdown - Markdown text to parse
142 * @returns {string} - Equivalent HTML
90143 */
91144export function parse(markdown) {
92145 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;
146 let html = '';
147 let inList = false;
148
149 for (const line of lines) {
150 const [lineHtml, newListState] = parseLine(line, inList);
151 html += lineHtml;
152 inList = newListState;
99153 }
100 if (list) {
101 return result + '</ul>';
102 } else {
103 return result;
154
155 // Close list if still open
156 if (inList) {
157 html += '</ul>';
104158 }
159
160 return html;
105161}
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.