Immune

Finished
1// Helper function to wrap text in HTML tags
12function wrap(text, tag) {
23 return `<${tag}>${text}</${tag}>`;
34}
45
6// Check if text starts with a specific HTML tag
57function isTag(text, tag) {
68 return text.startsWith(`<${tag}>`);
79}
810
11// Generic parser function for inline formatting
912function parser(markdown, delimiter, tag) {
10 const pattern = new RegExp(`${delimiter}(.+)${delimiter}`);
13 // Use non-greedy matching to correctly parse markdown delimiters
14 // This ensures we match the first delimiter with the closest last delimiter
15 // Escape special regex characters in the delimiter to avoid regex errors
16 // Use global flag to replace all occurrences, not just the first one
17 const escapedDelimiter = delimiter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
18 const pattern = new RegExp(`${escapedDelimiter}(.+?)${escapedDelimiter}`, 'g');
1119 const replacement = `<${tag}>$1</${tag}>`;
1220 return markdown.replace(pattern, replacement);
1321}
1422
15function parse__(markdown) {
16 return parser(markdown, '__', 'strong');
23// Parse bold text (**text** -> <strong>text</strong>)
24function parseBold(markdown) {
25 return parser(markdown, '**', 'strong');
1726}
1827
19function parse_(markdown) {
20 return parser(markdown, '_', 'em');
28// Parse italic text (*text* -> <em>text</em>)
29function parseItalic(markdown) {
30 return parser(markdown, '*', 'em');
2131}
2232
23function parseText(markdown, list) {
24 const parsedText = parse_(parse__(markdown));
25 if (list) {
33// Parse inline formatting (bold and italic) in text
34// Process bold before italic to avoid conflicts with asterisk characters
35function parseText(markdown, inList) {
36 // Process bold formatting first, then italic formatting
37 const parsedText = parseItalic(parseBold(markdown));
38
39 // Wrap in paragraph tags if not in a list
40 if (inList) {
2641 return parsedText;
2742 } else {
2843 return wrap(parsedText, 'p');
2944 }
3045}
3146
32function parseHeader(markdown, list) {
33 let count = 0;
34 for (let i = 0; i < markdown.length; i++) {
35 if (markdown[i] === '#') {
36 count += 1;
47// Parse markdown headers (# Header -> <h1>Header</h1>)
48// Supports headers from h1 to h6
49function parseHeader(markdown, inList) {
50 // Count consecutive # characters at the start
51 let headerLevel = 0;
52 for (const char of markdown) {
53 if (char === '#') {
54 headerLevel++;
3755 } else {
3856 break;
3957 }
4058 }
41 if (count === 0 || count > 6) {
42 return [null, list];
59
60 // Valid headers must have 1-6 # characters
61 if (headerLevel === 0 || headerLevel > 6) {
62 return [null, inList];
4363 }
44 const headerTag = `h${count}`;
45 const headerHtml = wrap(markdown.substring(count + 1), headerTag);
46 if (list) {
64
65 const headerTag = `h${headerLevel}`;
66 const headerContent = markdown.substring(headerLevel + 1).trim();
67 const headerHtml = wrap(headerContent, headerTag);
68
69 // Close any open list before adding header
70 if (inList) {
4771 return [`</ul>${headerHtml}`, false];
4872 } else {
4973 return [headerHtml, false];
5074 }
5175}
5276
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];
77// Parse markdown list items (* Item -> <li>Item</li>)
78// Only matches lines starting with "* " (asterisk followed by space)
79// This prevents matching bold/italic text that starts with * but isn't a list item
80function parseLineItem(markdown, inList) {
81 if (markdown.startsWith('* ')) {
82 // Remove the "* " prefix and parse the remaining text
83 const itemContent = markdown.substring(2);
84 const parsedContent = parseText(itemContent, true);
85 const listItemHtml = wrap(parsedContent, 'li');
86
87 if (inList) {
88 // Already in a list, just add the list item
89 return [listItemHtml, true];
5890 } else {
59 return [`<ul>${innerHtml}`, true];
91 // Start a new list
92 return [`<ul>${listItemHtml}`, true];
6093 }
6194 }
62 return [null, list];
95
96 return [null, inList];
6397}
6498
65function parseParagraph(markdown, list) {
66 if (!list) {
67 return [parseText(markdown, false), false];
99// Parse regular paragraphs (text -> <p>text</p>)
100// This is the fallback when no other markdown elements are matched
101function parseParagraph(markdown, inList) {
102 const parsedText = parseText(markdown, false);
103
104 if (inList) {
105 // Close the list before adding the paragraph
106 return [`</ul>${parsedText}`, false];
68107 } else {
69 return [`</ul>${parseText(markdown, false)}`, false];
108 return [parsedText, false];
70109 }
71110}
72111
73function parseLine(markdown, list) {
74 let [result, inListAfter] = parseHeader(markdown, list);
112// Parse a single line of markdown and determine its type
113// Tries to parse as header, then list item, then paragraph (fallback)
114function parseLine(markdown, inList) {
115 // Try to parse as a header first
116 let [result, newInList] = parseHeader(markdown, inList);
117
118 // If not a header, try to parse as a list item
75119 if (result === null) {
76 [result, inListAfter] = parseLineItem(markdown, list);
120 [result, newInList] = parseLineItem(markdown, inList);
77121 }
122
123 // If not a list item, parse as a paragraph (fallback)
78124 if (result === null) {
79 [result, inListAfter] = parseParagraph(markdown, list);
125 [result, newInList] = parseParagraph(markdown, inList);
80126 }
127
128 // This should never happen since paragraph parsing always succeeds
81129 if (result === null) {
82 throw new Error('Remove this line and implement the function');
130 throw new Error('Failed to parse markdown line');
83131 }
84 return [result, inListAfter];
132
133 return [result, newInList];
85134}
86135
87136/**
88 * @param {string} markdown
89 * @returns {string}
137 * Main markdown parser function
138 * Converts markdown text to HTML
139 * @param {string} markdown - The markdown text to parse
140 * @returns {string} - The resulting HTML
90141 */
91142export function parse(markdown) {
92143 const lines = markdown.split('\n');
93144 let result = '';
94 let list = false;
95 for (let i = 0; i < lines.length; i++) {
96 let [lineResult, newList] = parseLine(lines[i], list);
145 let inList = false;
146
147 // Process each line of the markdown
148 for (const line of lines) {
149 const [lineResult, newInList] = parseLine(line, inList);
97150 result += lineResult;
98 list = newList;
151 inList = newInList;
99152 }
100 if (list) {
101 return result + '</ul>';
102 } else {
103 return result;
153
154 // Close any open list tags
155 if (inList) {
156 result += '</ul>';
104157 }
158
159 return result;
105160}
Test NameStatus
parsing italics
Fail
parsing bold text
Fail
mixed normal, italics and bold text
Fail
with a little bit of everything
Fail
parses normal text as a paragraph
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 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.