@ngel

Finished
66 return text.startsWith(`<${tag}>`);
77}
88
9function parser(markdown, delimiter, tag) {
10 const pattern = new RegExp(`${delimiter}(.+)${delimiter}`);
9// Parse emphasis formatting (__ for bold, _ for italic)
10// Uses non-greedy matching to handle multiple instances correctly
11function parseEmphasis(markdown, delimiter, tag) {
12 // Escape special regex characters in delimiter
13 const escapedDelimiter = delimiter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
14 // Use non-greedy matching to avoid overlapping issues
15 const pattern = new RegExp(`${escapedDelimiter}(.+?)${escapedDelimiter}`, 'g');
1116 const replacement = `<${tag}>$1</${tag}>`;
1217 return markdown.replace(pattern, replacement);
1318}
1419
15function parse__(markdown) {
16 return parser(markdown, '__', 'strong');
20// Parse bold text (__text__ -> <strong>text</strong>)
21function parseBold(markdown) {
22 return parseEmphasis(markdown, '__', 'strong');
1723}
1824
19function parse_(markdown) {
20 return parser(markdown, '_', 'em');
25// Parse italic text (_text_ -> <em>text</em>)
26function parseItalic(markdown) {
27 return parseEmphasis(markdown, '_', 'em');
2128}
2229
30// Parse text with emphasis formatting and wrap in paragraph tags if not in a list
2331function parseText(markdown, list) {
24 const parsedText = parse_(parse__(markdown));
32 // Apply emphasis formatting (bold, then italic)
33 const parsedText = parseItalic(parseBold(markdown));
2534 if (list) {
2635 return parsedText;
2736 } else {
2938 }
3039}
3140
41// Parse markdown headers (# to ######)
42// Returns [html, inList] tuple
3243function 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 }
44 // Count consecutive # characters at the start
45 let headerLevel = 0;
46 for (let i = 0; i < markdown.length && markdown[i] === '#'; i++) {
47 headerLevel++;
4048 }
41 if (count === 0 || count > 6) {
49
50 // Valid headers are 1-6 # characters
51 if (headerLevel === 0 || headerLevel > 6) {
4252 return [null, list];
4353 }
44 const headerTag = `h${count}`;
45 const headerHtml = wrap(markdown.substring(count + 1), headerTag);
54
55 const headerTag = `h${headerLevel}`;
56 const headerContent = markdown.substring(headerLevel + 1).trim();
57 const headerHtml = wrap(headerContent, headerTag);
58
59 // Close any open list before header
4660 if (list) {
4761 return [`</ul>${headerHtml}`, false];
4862 } else {
5064 }
5165}
5266
67// Parse list items starting with *
68// Returns [html, inList] tuple
5369function parseLineItem(markdown, list) {
5470 if (markdown.startsWith('*')) {
55 const innerHtml = wrap(parseText(markdown.substring(2), true), 'li');
71 // Extract list item content (after "* ")
72 const itemContent = markdown.substring(2).trim();
73 const listItemHtml = wrap(parseText(itemContent, true), 'li');
74
5675 if (list) {
57 return [innerHtml, true];
76 // Already in a list, just add the list item
77 return [listItemHtml, true];
5878 } else {
59 return [`<ul>${innerHtml}`, true];
79 // Start a new list
80 return [`<ul>${listItemHtml}`, true];
6081 }
6182 }
6283 return [null, list];
6384}
6485
86// Parse regular paragraphs
87// Returns [html, inList] tuple
6588function parseParagraph(markdown, list) {
6689 if (!list) {
90 // Not in a list, just parse as paragraph
6791 return [parseText(markdown, false), false];
6892 } else {
93 // Close the list and start a paragraph
6994 return [`</ul>${parseText(markdown, false)}`, false];
7095 }
7196}
7297
98// Main line parser - tries different parsers in order
99// Returns [html, inList] tuple
73100function parseLine(markdown, list) {
101 // Try parsing as header first
74102 let [result, inListAfter] = parseHeader(markdown, list);
103
104 // If not a header, try parsing as list item
75105 if (result === null) {
76106 [result, inListAfter] = parseLineItem(markdown, list);
77107 }
108
109 // If not a list item, parse as paragraph
78110 if (result === null) {
79111 [result, inListAfter] = parseParagraph(markdown, list);
80112 }
113
114 // This should never happen as one of the parsers should always match
81115 if (result === null) {
82 throw new Error('Remove this line and implement the function');
116 throw new Error('Unable to parse markdown line');
83117 }
118
84119 return [result, inListAfter];
85120}
86121
87122/**
88 * @param {string} markdown
89 * @returns {string}
123 * Main markdown parser function
124 * @param {string} markdown - The markdown string to parse
125 * @returns {string} - The equivalent HTML
90126 */
91127export function parse(markdown) {
128 // Split input into lines for processing
92129 const lines = markdown.split('\n');
93130 let result = '';
94 let list = false;
131 let inList = false; // Track whether we're currently inside a list
132
133 // Process each line sequentially
95134 for (let i = 0; i < lines.length; i++) {
96 let [lineResult, newList] = parseLine(lines[i], list);
135 const [lineResult, newInListState] = parseLine(lines[i], inList);
97136 result += lineResult;
98 list = newList;
137 inList = newInListState;
99138 }
100 if (list) {
139
140 // Close any unclosed list at the end
141 if (inList) {
101142 return result + '</ul>';
102143 } else {
103144 return result;
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.