uv

Finished
1function wrap(text, tag) {
1// Utility function to wrap text in HTML tags
2function wrapTextInTag(text, tag) {
23 return `<${tag}>${text}</${tag}>`;
34}
45
5function isTag(text, tag) {
6// Utility function to check if text starts with a specific HTML tag
7function startsWithTag(text, tag) {
68 return text.startsWith(`<${tag}>`);
79}
810
9function parser(markdown, delimiter, tag) {
11// Generic parser for text formatting (bold, emphasis, etc.)
12function parseTextFormatting(markdown, delimiter, htmlTag) {
1013 const pattern = new RegExp(`${delimiter}(.+)${delimiter}`);
11 const replacement = `<${tag}>$1</${tag}>`;
14 const replacement = `<${htmlTag}>$1</${htmlTag}>`;
1215 return markdown.replace(pattern, replacement);
1316}
1417
15function parse__(markdown) {
16 return parser(markdown, '__', 'strong');
18// Parse bold text (using __ delimiter)
19function parseBoldText(markdown) {
20 return parseTextFormatting(markdown, '__', 'strong');
1721}
1822
19function parse_(markdown) {
20 return parser(markdown, '_', 'em');
23// Parse emphasis/italic text (using _ delimiter)
24function parseEmphasisText(markdown) {
25 return parseTextFormatting(markdown, '_', 'em');
2126}
2227
23function parseText(markdown, list) {
24 const parsedText = parse_(parse__(markdown));
25 if (list) {
26 return parsedText;
28// Parse text content with formatting (bold and emphasis)
29// Returns either plain text (if in a list) or wrapped in paragraph tags
30function parseTextContent(markdown, isInList) {
31 // Apply text formatting: first bold, then emphasis
32 const formattedText = parseEmphasisText(parseBoldText(markdown));
33
34 if (isInList) {
35 return formattedText; // Don't wrap in <p> if already in a list
2736 } else {
28 return wrap(parsedText, 'p');
37 return wrapTextInTag(formattedText, 'p'); // Wrap in paragraph tags
2938 }
3039}
3140
32function parseHeader(markdown, list) {
33 let count = 0;
41// Parse markdown headers (lines starting with #)
42// Returns [htmlContent, isInList] tuple
43function parseHeader(markdown, isInList) {
44 // Count consecutive # characters at the start
45 let headerLevel = 0;
3446 for (let i = 0; i < markdown.length; i++) {
3547 if (markdown[i] === '#') {
36 count += 1;
48 headerLevel += 1;
3749 } else {
3850 break;
3951 }
4052 }
41 if (count === 0 || count > 6) {
42 return [null, list];
53
54 // Valid headers have 1-6 # characters
55 if (headerLevel === 0 || headerLevel > 6) {
56 return [null, isInList]; // Not a header
4357 }
44 const headerTag = `h${count}`;
45 const headerHtml = wrap(markdown.substring(count + 1), headerTag);
46 if (list) {
58
59 const headerTag = `h${headerLevel}`;
60 const headerContent = markdown.substring(headerLevel + 1).trim();
61 const headerHtml = wrapTextInTag(headerContent, headerTag);
62
63 // If we were in a list, close it before the header
64 if (isInList) {
4765 return [`</ul>${headerHtml}`, false];
4866 } else {
4967 return [headerHtml, false];
5068 }
5169}
5270
53function parseLineItem(markdown, list) {
71// Parse list items (lines starting with *)
72// Returns [htmlContent, isInList] tuple
73function parseListItem(markdown, isInList) {
5474 if (markdown.startsWith('*')) {
55 const innerHtml = wrap(parseText(markdown.substring(2), true), 'li');
56 if (list) {
57 return [innerHtml, true];
75 // Extract the content after the * and space
76 const itemContent = markdown.substring(2).trim();
77 const listItemHtml = wrapTextInTag(parseTextContent(itemContent, true), 'li');
78
79 if (isInList) {
80 // Already in a list, just add the list item
81 return [listItemHtml, true];
5882 } else {
59 return [`<ul>${innerHtml}`, true];
83 // Start a new list
84 return [`<ul>${listItemHtml}`, true];
6085 }
6186 }
62 return [null, list];
87
88 // Not a list item
89 return [null, isInList];
6390}
6491
65function parseParagraph(markdown, list) {
66 if (!list) {
67 return [parseText(markdown, false), false];
92// Parse paragraph content (default for lines that don't match other patterns)
93// Returns [htmlContent, isInList] tuple
94function parseParagraph(markdown, isInList) {
95 const paragraphHtml = parseTextContent(markdown, false);
96
97 // If we were in a list, close it before the paragraph
98 if (isInList) {
99 return [`</ul>${paragraphHtml}`, false];
68100 } else {
69 return [`</ul>${parseText(markdown, false)}`, false];
101 return [paragraphHtml, false];
70102 }
71103}
72104
73function parseLine(markdown, list) {
74 let [result, inListAfter] = parseHeader(markdown, list);
105// Parse a single line of markdown by trying different parsers in order
106// Returns [htmlContent, isInList] tuple
107function parseLine(markdown, isInList) {
108 // Try parsing as header first
109 let [result, newListState] = parseHeader(markdown, isInList);
110
111 // If not a header, try parsing as list item
75112 if (result === null) {
76 [result, inListAfter] = parseLineItem(markdown, list);
113 [result, newListState] = parseListItem(markdown, isInList);
77114 }
115
116 // If neither header nor list item, parse as paragraph
78117 if (result === null) {
79 [result, inListAfter] = parseParagraph(markdown, list);
118 [result, newListState] = parseParagraph(markdown, isInList);
80119 }
120
121 // This should never happen with valid markdown
81122 if (result === null) {
82 throw new Error('Remove this line and implement the function');
123 throw new Error('Unable to parse markdown line');
83124 }
84 return [result, inListAfter];
125
126 return [result, newListState];
85127}
86128
87129/**
88 * @param {string} markdown
89 * @returns {string}
130 * Parse markdown text and convert it to HTML
131 * @param {string} markdown - The markdown text to parse
132 * @returns {string} - The resulting HTML
90133 */
91134export function parse(markdown) {
135 // Split the markdown into individual lines
92136 const lines = markdown.split('\n');
93 let result = '';
94 let list = false;
137 let htmlResult = '';
138 let isInList = false; // Track if we're currently inside a list
139
140 // Process each line of markdown
95141 for (let i = 0; i < lines.length; i++) {
96 let [lineResult, newList] = parseLine(lines[i], list);
97 result += lineResult;
98 list = newList;
142 // Parse the current line and get the HTML result and updated list state
143 let [lineHtml, newListState] = parseLine(lines[i], isInList);
144 htmlResult += lineHtml;
145 isInList = newListState;
99146 }
100 if (list) {
101 return result + '</ul>';
147
148 // If we're still in a list at the end, close it
149 if (isInList) {
150 return htmlResult + '</ul>';
102151 } else {
103 return result;
152 return htmlResult;
104153 }
105154}
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.