let it go

Finished
1// Helper functions for HTML tag wrapping
12function wrap(text, tag) {
3 if (!tag) {
4 throw new Error('Tag is required for wrapping');
5 }
6 // text can be empty string, but not null/undefined
7 if (text === null || text === undefined) {
8 throw new Error('Text cannot be null or undefined');
9 }
210 return `<${tag}>${text}</${tag}>`;
311}
412
5function isTag(text, tag) {
6 return text.startsWith(`<${tag}>`);
7}
13// Constants for better maintainability
14const MARKDOWN_PATTERNS = {
15 STRONG: /__(.+?)__/g,
16 EMPHASIS: /_(.+?)_/g,
17 LIST_ITEM: /^\* /,
18 HEADER: /^#+/
19};
820
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}
21const HTML_TAGS = {
22 STRONG: 'strong',
23 EMPHASIS: 'em',
24 PARAGRAPH: 'p',
25 LIST: 'ul',
26 LIST_ITEM: 'li'
27};
28
29const MAX_HEADER_LEVEL = 6;
1430
15function parse__(markdown) {
16 return parser(markdown, '__', 'strong');
31// Markdown inline element parsers
32function parseStrong(markdown) {
33 const pattern = MARKDOWN_PATTERNS.STRONG;
34 const replacement = `<${HTML_TAGS.STRONG}>$1</${HTML_TAGS.STRONG}>`;
35 return markdown.replace(pattern, replacement);
1736}
1837
19function parse_(markdown) {
20 return parser(markdown, '_', 'em');
38function parseEmphasis(markdown) {
39 const pattern = MARKDOWN_PATTERNS.EMPHASIS;
40 const replacement = `<${HTML_TAGS.EMPHASIS}>$1</${HTML_TAGS.EMPHASIS}>`;
41 return markdown.replace(pattern, replacement);
2142}
2243
23function parseText(markdown, list) {
24 const parsedText = parse_(parse__(markdown));
25 if (list) {
26 return parsedText;
27 } else {
28 return wrap(parsedText, 'p');
29 }
44function parseText(markdown, isListItem = false) {
45 // Parse inline elements (__ for strong, _ for emphasis)
46 const parsedText = parseEmphasis(parseStrong(markdown));
47
48 // Wrap in paragraph tag unless it's a list item
49 return isListItem ? parsedText : wrap(parsedText, HTML_TAGS.PARAGRAPH);
3050}
3151
32function parseHeader(markdown, list) {
33 let count = 0;
34 for (let i = 0; i < markdown.length; i++) {
35 if (markdown[i] === '#') {
36 count += 1;
52// Markdown block element parsers
53function parseHeader(markdown, isCurrentlyInList) {
54 // Count leading # characters to determine header level
55 let headerLevel = 0;
56 for (const char of markdown) {
57 if (char === '#') {
58 headerLevel++;
3759 } else {
3860 break;
3961 }
4062 }
41 if (count === 0 || count > 6) {
42 return [null, list];
63
64 // Return null if not a valid header (no # or more than max allowed)
65 if (headerLevel === 0 || headerLevel > MAX_HEADER_LEVEL) {
66 return [null, isCurrentlyInList];
4367 }
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];
68
69 // Extract header content (skip # and space)
70 const headerContent = markdown.substring(headerLevel + 1);
71 const headerTag = `h${headerLevel}`;
72 const headerHtml = wrap(headerContent, headerTag);
73
74 // Close list if we were in one
75 if (isCurrentlyInList) {
76 return [`</${HTML_TAGS.LIST}>${headerHtml}`, false];
5077 }
78
79 return [headerHtml, false];
5180}
5281
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 }
82function parseLineItem(markdown, isCurrentlyInList) {
83 // Check if line starts with list item marker
84 if (!MARKDOWN_PATTERNS.LIST_ITEM.test(markdown)) {
85 return [null, isCurrentlyInList];
86 }
87
88 // Extract list item content (skip '* ')
89 const itemContent = markdown.substring(2);
90 const itemHtml = wrap(parseText(itemContent, true), HTML_TAGS.LIST_ITEM);
91
92 // Open list if we weren't already in one
93 if (!isCurrentlyInList) {
94 return [`<${HTML_TAGS.LIST}>${itemHtml}`, true];
6195 }
62 return [null, list];
96
97 return [itemHtml, true];
6398}
6499
65function parseParagraph(markdown, list) {
66 if (!list) {
67 return [parseText(markdown, false), false];
68 } else {
69 return [`</ul>${parseText(markdown, false)}`, false];
100function parseParagraph(markdown, isCurrentlyInList) {
101 const paragraphHtml = parseText(markdown, false);
102
103 // Close list if we were in one
104 if (isCurrentlyInList) {
105 return [`</${HTML_TAGS.LIST}>${paragraphHtml}`, false];
70106 }
107
108 return [paragraphHtml, false];
71109}
72110
73function parseLine(markdown, list) {
74 let [result, inListAfter] = parseHeader(markdown, list);
111// Main parsing functions
112function parseLine(markdown, isCurrentlyInList) {
113 // Try parsing as header
114 let [result, newListState] = parseHeader(markdown, isCurrentlyInList);
115
116 // If not a header, try parsing as list item
75117 if (result === null) {
76 [result, inListAfter] = parseLineItem(markdown, list);
118 [result, newListState] = parseLineItem(markdown, isCurrentlyInList);
77119 }
120
121 // If not a list item, parse as paragraph
78122 if (result === null) {
79 [result, inListAfter] = parseParagraph(markdown, list);
123 [result, newListState] = parseParagraph(markdown, isCurrentlyInList);
80124 }
125
126 // This should never happen with valid input
81127 if (result === null) {
82 throw new Error('Remove this line and implement the function');
128 throw new Error('Invalid markdown line');
83129 }
84 return [result, inListAfter];
130
131 return [result, newListState];
85132}
86133
87134/**
88 * @param {string} markdown
89 * @returns {string}
135 * Parses markdown text and returns HTML
136 * @param {string} markdown - The markdown text to parse
137 * @returns {string} The corresponding HTML
90138 */
91139export function parse(markdown) {
140 // Validate input
141 if (typeof markdown !== 'string') {
142 throw new Error('Markdown input must be a string');
143 }
144
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 htmlResult = '';
147 let isInList = false;
148
149 // Process each line of markdown
150 for (const line of lines) {
151 const [lineHtml, newInListState] = parseLine(line, isInList);
152 htmlResult += lineHtml;
153 isInList = newInListState;
99154 }
100 if (list) {
101 return result + '</ul>';
102 } else {
103 return result;
155
156 // Close any unclosed list at the end
157 if (isInList) {
158 htmlResult += `</${HTML_TAGS.LIST}>`;
104159 }
160
161 return htmlResult;
105162}
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.