goodagent

Finished
1/**
2 * Markdown to HTML parser
3 * Refactored for better readability and maintainability
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 (__) to <strong> tags
18 * @param {string} markdown - Markdown text
19 * @returns {string} - HTML with bold tags
20 */
21function parseBold(markdown) {
22 const pattern = /__(.+)__/g;
23 return markdown.replace(pattern, '<strong>$1</strong>');
1724}
1825
19function parse_(markdown) {
20 return parser(markdown, '_', 'em');
26/**
27 * Parses italic text (_) to <em> tags
28 * @param {string} markdown - Markdown text
29 * @returns {string} - HTML with italic tags
30 */
31function parseItalic(markdown) {
32 const pattern = /_(.+)_/g;
33 return markdown.replace(pattern, '<em>$1</em>');
2134}
2235
23function parseText(markdown, list) {
24 const parsedText = parse_(parse__(markdown));
25 if (list) {
26 return parsedText;
27 } else {
28 return wrap(parsedText, 'p');
29 }
36/**
37 * Parses inline formatting (bold and italic)
38 * @param {string} markdown - Markdown text
39 * @returns {string} - HTML with inline formatting
40 */
41function parseInlineFormatting(markdown) {
42 return parseItalic(parseBold(markdown));
3043}
3144
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];
45/**
46 * Parses a header line (# Header) to <h1>-<h6> tags
47 * @param {string} line - Markdown header line
48 * @returns {string|null} - HTML header or null if not a header
49 */
50function parseHeader(line) {
51 // Count leading # characters
52 let level = 0;
53 while (level < line.length && line[level] === '#') {
54 level++;
4355 }
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];
56
57 // Valid headers have 1-6 # characters
58 if (level === 0 || level > 6) {
59 return null;
5060 }
61
62 // Take everything after the hashes, skipping the first character (matches original behavior)
63 const content = line.substring(level + 1);
64 const headerTag = `h${level}`;
65 return wrap(content, headerTag);
5166}
5267
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 }
68/**
69 * Parses a list item (* item) to <li> tags
70 * @param {string} line - Markdown list item
71 * @returns {string|null} - HTML list item or null if not a list item
72 */
73function parseListItem(line) {
74 if (!line.startsWith('* ')) {
75 return null;
6176 }
62 return [null, list];
77
78 const content = line.substring(2);
79 const formattedContent = parseInlineFormatting(content);
80 return wrap(formattedContent, 'li');
6381}
6482
65function parseParagraph(markdown, list) {
66 if (!list) {
67 return [parseText(markdown, false), false];
68 } else {
69 return [`</ul>${parseText(markdown, false)}`, false];
70 }
83/**
84 * Parses a paragraph line to <p> tags
85 * @param {string} line - Markdown paragraph line
86 * @returns {string} - HTML paragraph
87 */
88function parseParagraph(line) {
89 const formattedContent = parseInlineFormatting(line);
90 return wrap(formattedContent, 'p');
7191}
7292
73function parseLine(markdown, list) {
74 let [result, inListAfter] = parseHeader(markdown, list);
75 if (result === null) {
76 [result, inListAfter] = parseLineItem(markdown, list);
77 }
78 if (result === null) {
79 [result, inListAfter] = parseParagraph(markdown, list);
93/**
94 * Parses a single line of markdown
95 * @param {string} line - Markdown line
96 * @returns {Object} - Parsed result with type and content
97 */
98function parseLine(line) {
99 // Try to parse as header
100 const header = parseHeader(line);
101 if (header !== null) {
102 return { type: 'header', content: header };
80103 }
81 if (result === null) {
82 throw new Error('Remove this line and implement the function');
104
105 // Try to parse as list item
106 const listItem = parseListItem(line);
107 if (listItem !== null) {
108 return { type: 'listItem', content: listItem };
83109 }
84 return [result, inListAfter];
110
111 // Default to paragraph
112 return { type: 'paragraph', content: parseParagraph(line) };
85113}
86114
87115/**
88 * @param {string} markdown
89 * @returns {string}
116 * Main markdown parser function
117 * @param {string} markdown - Markdown text to parse
118 * @returns {string} - HTML output
90119 */
91120export function parse(markdown) {
92121 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;
122 let html = '';
123 let inList = false;
124
125 for (const line of lines) {
126 const parsed = parseLine(line);
127
128 // Handle list opening and closing
129 if (parsed.type === 'listItem' && !inList) {
130 // Start a new list
131 html += '<ul>';
132 inList = true;
133 } else if (parsed.type !== 'listItem' && inList) {
134 // Close the current list
135 html += '</ul>';
136 inList = false;
137 }
138
139 // Handle different content types
140 if (parsed.type === 'header' && inList) {
141 // Close list before header
142 html += '</ul>' + parsed.content;
143 inList = false;
144 } else {
145 html += parsed.content;
146 }
99147 }
100 if (list) {
101 return result + '</ul>';
102 } else {
103 return result;
148
149 // Close any open list at the end
150 if (inList) {
151 html += '</ul>';
104152 }
153
154 return html;
105155}
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.