Merry

Finished
1function wrap(text, tag) {
2 return `<${tag}>${text}</${tag}>`;
3}
4
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');
1/**
2 * Parses Markdown text and converts it to HTML.
3 * Supports headers (# ## ### #### ##### ######),
4 * unordered lists (* item),
5 * bold text (__text__),
6 * and italic text (_text_).
7 *
8 * @param {string} markdown - The Markdown text to parse
9 * @returns {string} - The resulting HTML
10 */
11export function parse(markdown) {
12 const lines = markdown.split('\n');
13
14 // Process all lines and track list state
15 const { html, inList } = lines.reduce(processLine, { html: '', inList: false });
16
17 // Close any open list at the end
18 return inList ? html + '</ul>' : html;
1719}
1820
19function parse_(markdown) {
20 return parser(markdown, '_', 'em');
21/**
22 * Process a single line of markdown and update the parsing state.
23 *
24 * @param {Object} state - The current parsing state { html, inList }
25 * @param {string} line - The line to process
26 * @returns {Object} - The updated parsing state
27 */
28function processLine(state, line) {
29 // Try to parse as header first
30 const headerResult = parseHeader(line, state.inList);
31 if (headerResult !== null) {
32 const [headerHtml, newListState] = headerResult;
33 return {
34 html: state.html + headerHtml,
35 inList: newListState
36 };
37 }
38
39 // Try to parse as list item
40 if (line.startsWith('*')) {
41 return processListItem(state, line);
42 }
43
44 // Default to paragraph
45 return processParagraph(state, line);
2146}
2247
23function parseText(markdown, list) {
24 const parsedText = parse_(parse__(markdown));
25 if (list) {
26 return parsedText;
48/**
49 * Process a line as a list item.
50 *
51 * @param {Object} state - The current parsing state
52 * @param {string} line - The line to process as list item
53 * @returns {Object} - The updated parsing state
54 */
55function processListItem(state, line) {
56 const content = line.substring(2); // Remove '* ' prefix
57
58 if (state.inList) {
59 // Already in a list, just add the item
60 return {
61 html: state.html + `<li>${parseInlineFormatting(content)}</li>`,
62 inList: true
63 };
2764 } else {
28 return wrap(parsedText, 'p');
65 // Need to open a new list
66 return {
67 html: state.html + `<ul><li>${parseInlineFormatting(content)}</li>`,
68 inList: true
69 };
2970 }
3071}
3172
32function parseHeader(markdown, list) {
73/**
74 * Process a line as a paragraph.
75 *
76 * @param {Object} state - The current parsing state
77 * @param {string} line - The line to process as paragraph
78 * @returns {Object} - The updated parsing state
79 */
80function processParagraph(state, line) {
81 // Close list if we were in one
82 const html = state.inList ? state.html + '</ul>' : state.html;
83
84 // Add paragraph
85 return {
86 html: html + `<p>${parseInlineFormatting(line)}</p>`,
87 inList: false
88 };
89}
90
91/**
92 * Parse a header line and return HTML and new list state.
93 *
94 * @param {string} line - The line to parse as header
95 * @param {boolean} inList - Whether we're currently in a list
96 * @returns {Array|null} - [html, newListState] or null if not a header
97 */
98function parseHeader(line, inList) {
99 // Count leading # characters
33100 let count = 0;
34 for (let i = 0; i < markdown.length; i++) {
35 if (markdown[i] === '#') {
36 count += 1;
101 for (let i = 0; i < line.length; i++) {
102 if (line[i] === '#') {
103 count++;
37104 } else {
38105 break;
39106 }
40107 }
108
109 // Valid headers have 1-6 # characters
41110 if (count === 0 || count > 6) {
42 return [null, list];
111 return null;
43112 }
113
114 // Extract content (everything after the # characters and optional space)
115 const content = line.substring(count + 1);
44116 const headerTag = `h${count}`;
45 const headerHtml = wrap(markdown.substring(count + 1), headerTag);
46 if (list) {
117
118 const headerHtml = `<${headerTag}>${parseInlineFormatting(content)}</${headerTag}>`;
119
120 // Close list if we were in one
121 if (inList) {
47122 return [`</ul>${headerHtml}`, false];
48123 } else {
49124 return [headerHtml, false];
50125 }
51126}
52127
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 }
61 }
62 return [null, list];
63}
64
65function parseParagraph(markdown, list) {
66 if (!list) {
67 return [parseText(markdown, false), false];
68 } else {
69 return [`</ul>${parseText(markdown, false)}`, false];
70 }
71}
72
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);
80 }
81 if (result === null) {
82 throw new Error('Remove this line and implement the function');
83 }
84 return [result, inListAfter];
85}
86
87128/**
88 * @param {string} markdown
89 * @returns {string}
129 * Parses inline formatting like bold (__text__) and italic (_text_).
130 *
131 * @param {string} text - The text to parse
132 * @returns {string} - The text with inline formatting converted to HTML
90133 */
91export function parse(markdown) {
92 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;
99 }
100 if (list) {
101 return result + '</ul>';
102 } else {
103 return result;
104 }
134function parseInlineFormatting(text) {
135 // Parse bold formatting (__text__ -> <strong>text</strong>)
136 let result = text;
137
138 // Handle bold (__text__)
139 result = result.replace(/__(.+)__/g, '<strong>$1</strong>');
140
141 // Handle italic (_text_)
142 result = result.replace(/_(.+)_/g, '<em>$1</em>');
143
144 return result;
105145}
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.