let it go

Finished
1// Constants for better maintainability
2const MARKDOWN_CONSTANTS = {
3 HEADER_CHAR: '#',
4 LIST_CHAR: '*',
5 BOLD_DELIMITER: '__',
6 ITALIC_DELIMITER: '_',
7 MAX_HEADER_LEVEL: 6,
8 HTML_TAGS: {
9 PARAGRAPH: 'p',
10 LIST: 'ul',
11 LIST_ITEM: 'li',
12 BOLD: 'strong',
13 ITALIC: 'em'
14 }
15};
16
17// Helper function to wrap text in HTML tags
118function wrap(text, tag) {
219 return `<${tag}>${text}</${tag}>`;
320}
421
22// Helper function to check if text starts with a specific tag
523function isTag(text, tag) {
624 return text.startsWith(`<${tag}>`);
725}
826
9function parser(markdown, delimiter, tag) {
27// Helper function to count consecutive characters at the start of a string
28function countLeadingChars(text, char) {
29 let count = 0;
30 for (let i = 0; i < text.length; i++) {
31 if (text[i] === char) {
32 count++;
33 } else {
34 break;
35 }
36 }
37 return count;
38}
39
40// Helper function to trim whitespace from both ends
41function trim(text) {
42 return text.trim();
43}
44
45// Generic parser for inline elements (bold, italic)
46function parseInlineElement(markdown, delimiter, tag) {
1047 const pattern = new RegExp(`${delimiter}(.+)${delimiter}`);
1148 const replacement = `<${tag}>$1</${tag}>`;
1249 return markdown.replace(pattern, replacement);
1350}
1451
15function parse__(markdown) {
16 return parser(markdown, '__', 'strong');
52// Parse bold text (__text__ -> <strong>text</strong>)
53function parseBold(markdown) {
54 return parseInlineElement(markdown, MARKDOWN_CONSTANTS.BOLD_DELIMITER, MARKDOWN_CONSTANTS.HTML_TAGS.BOLD);
1755}
1856
19function parse_(markdown) {
20 return parser(markdown, '_', 'em');
57// Parse italic text (_text_ -> <em>text</em>)
58function parseItalic(markdown) {
59 return parseInlineElement(markdown, MARKDOWN_CONSTANTS.ITALIC_DELIMITER, MARKDOWN_CONSTANTS.HTML_TAGS.ITALIC);
2160}
2261
23function parseText(markdown, list) {
24 const parsedText = parse_(parse__(markdown));
62// Parse text with inline formatting (bold, italic)
63// If list is true, don't wrap in <p> tags
64function parseFormattedText(markdown, list) {
65 const parsedText = parseItalic(parseBold(markdown));
2566 if (list) {
2667 return parsedText;
2768 } else {
28 return wrap(parsedText, 'p');
69 return wrap(parsedText, MARKDOWN_CONSTANTS.HTML_TAGS.PARAGRAPH);
2970 }
3071}
3172
73// Parse header elements (# Header -> <h1>Header</h1>)
74// Returns [html, newListState]
3275function 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) {
76 // Count consecutive # characters at the start
77 const count = countLeadingChars(markdown, MARKDOWN_CONSTANTS.HEADER_CHAR);
78
79 // Headers must have 1-MAX_HEADER_LEVEL # characters
80 if (count === 0 || count > MARKDOWN_CONSTANTS.MAX_HEADER_LEVEL) {
4281 return [null, list];
4382 }
83
4484 const headerTag = `h${count}`;
45 const headerHtml = wrap(markdown.substring(count + 1), headerTag);
85 // Extract header content (skip # characters and the following space)
86 const headerContent = trim(markdown.substring(count + 1));
87 const headerHtml = wrap(headerContent, headerTag);
88
89 // If we were in a list, close it before the header
4690 if (list) {
47 return [`</ul>${headerHtml}`, false];
91 return [`</${MARKDOWN_CONSTANTS.HTML_TAGS.LIST}>${headerHtml}`, false];
4892 } else {
4993 return [headerHtml, false];
5094 }
5195}
5296
53function parseLineItem(markdown, list) {
54 if (markdown.startsWith('*')) {
55 const innerHtml = wrap(parseText(markdown.substring(2), true), 'li');
97// Parse list items (* item -> <li>item</li>)
98// Returns [html, newListState]
99function parseListItem(markdown, list) {
100 if (markdown.startsWith(MARKDOWN_CONSTANTS.LIST_CHAR)) {
101 // Extract item content (skip * character and the following space)
102 const itemContent = trim(markdown.substring(2));
103 const innerHtml = wrap(parseFormattedText(itemContent, true), MARKDOWN_CONSTANTS.HTML_TAGS.LIST_ITEM);
104
105 // If we're already in a list, just add the item
106 // Otherwise, start a new list
56107 if (list) {
57108 return [innerHtml, true];
58109 } else {
59 return [`<ul>${innerHtml}`, true];
110 return [`<${MARKDOWN_CONSTANTS.HTML_TAGS.LIST}>${innerHtml}`, true];
60111 }
61112 }
62113 return [null, list];
63114}
64115
116// Parse paragraph elements
117// Returns [html, newListState]
65118function parseParagraph(markdown, list) {
66 if (!list) {
67 return [parseText(markdown, false), false];
119 // If we were in a list, close it before the paragraph
120 if (list) {
121 return [`</${MARKDOWN_CONSTANTS.HTML_TAGS.LIST}>${parseFormattedText(markdown, false)}`, false];
68122 } else {
69 return [`</ul>${parseText(markdown, false)}`, false];
123 return [parseFormattedText(markdown, false), false];
70124 }
71125}
72126
127// Parse a single line of markdown
128// Returns [html, newListState]
73129function parseLine(markdown, list) {
130 // Try to parse as header first
74131 let [result, inListAfter] = parseHeader(markdown, list);
132
133 // If not a header, try to parse as list item
75134 if (result === null) {
76 [result, inListAfter] = parseLineItem(markdown, list);
135 [result, inListAfter] = parseListItem(markdown, list);
77136 }
137
138 // If not a list item, parse as paragraph
78139 if (result === null) {
79140 [result, inListAfter] = parseParagraph(markdown, list);
80141 }
142
143 // This should never happen with valid input
81144 if (result === null) {
82 throw new Error('Remove this line and implement the function');
145 throw new Error('Unable to parse line: ' + markdown);
83146 }
147
84148 return [result, inListAfter];
85149}
86150
87151/**
88 * @param {string} markdown
89 * @returns {string}
152 * Main markdown parser function
153 * Converts markdown text to HTML
154 * @param {string} markdown - The markdown text to parse
155 * @returns {string} - The resulting HTML
90156 */
91157export function parse(markdown) {
92158 const lines = markdown.split('\n');
93159 let result = '';
94 let list = false;
160 let inList = false; // Track whether we're currently inside a list
161
162 // Process each line of the markdown
95163 for (let i = 0; i < lines.length; i++) {
96 let [lineResult, newList] = parseLine(lines[i], list);
164 const currentLine = lines[i];
165 const [lineResult, newInListState] = parseLine(currentLine, inList);
166
97167 result += lineResult;
98 list = newList;
168 inList = newInListState;
99169 }
100 if (list) {
101 return result + '</ul>';
102 } else {
103 return result;
170
171 // If we ended while still in a list, close it
172 if (inList) {
173 result += `</${MARKDOWN_CONSTANTS.HTML_TAGS.LIST}>`;
104174 }
175
176 return result;
105177}
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.