Finished
1function wrap(text, tag) {
1// Helper functions for wrapping text in HTML tags
2function wrapText(text, tag) {
23 return `<${tag}>${text}</${tag}>`;
34}
45
5function isTag(text, tag) {
6 return text.startsWith(`<${tag}>`);
6// Text formatting functions
7function formatBold(text) {
8 // Match double underscores, but not if they're inside single underscores
9 const pattern = new RegExp(`__([^_]+)__`, 'g');
10 const replacement = `<strong>$1</strong>`;
11 return text.replace(pattern, replacement);
712}
813
9function parser(markdown, delimiter, tag) {
10 const pattern = new RegExp(`${delimiter}(.+)${delimiter}`);
11 const replacement = `<${tag}>$1</${tag}>`;
12 return markdown.replace(pattern, replacement);
14function formatItalic(text) {
15 // Match single underscores, but not if they're inside double underscores
16 // Using negative lookahead and lookbehind to avoid matching underscores inside bold markers
17 const pattern = new RegExp(`(?<!_)_([^_]+)_(?!_)`, 'g');
18 const replacement = `<em>$1</em>`;
19 return text.replace(pattern, replacement);
1320}
1421
15function parse__(markdown) {
16 return parser(markdown, '__', 'strong');
17}
18
19function parse_(markdown) {
20 return parser(markdown, '_', 'em');
21}
22
23function parseText(markdown, list) {
24 const parsedText = parse_(parse__(markdown));
25 if (list) {
26 return parsedText;
27 } else {
28 return wrap(parsedText, 'p');
22function formatText(text, isListItem = false) {
23 // Apply formatting (bold, italic) to text
24 let formattedText = formatBold(text);
25 formattedText = formatItalic(formattedText);
26
27 // Wrap in paragraph tag if not a list item
28 if (!isListItem) {
29 return wrapText(formattedText, 'p');
2930 }
31
32 return formattedText;
3033}
3134
32function parseHeader(markdown, list) {
33 let count = 0;
35// Header parsing function
36function parseHeader(markdown, currentlyInList) {
37 // Count leading # characters to determine header level
38 let headerLevel = 0;
3439 for (let i = 0; i < markdown.length; i++) {
3540 if (markdown[i] === '#') {
36 count += 1;
41 headerLevel += 1;
3742 } else {
3843 break;
3944 }
4045 }
41 if (count === 0 || count > 6) {
42 return [null, list];
46
47 // Return null if not a valid header (no # or more than 6 #)
48 if (headerLevel === 0 || headerLevel > 6) {
49 return [null, currentlyInList];
4350 }
44 const headerTag = `h${count}`;
45 const headerHtml = wrap(markdown.substring(count + 1), headerTag);
46 if (list) {
51
52 // Extract header content (skip # and space)
53 const headerContent = markdown.substring(headerLevel + 1);
54 // Apply text formatting to header content (bold first, then italic)
55 const formattedContent = formatItalic(formatBold(headerContent));
56 const headerTag = `h${headerLevel}`;
57 const headerHtml = wrapText(formattedContent, headerTag);
58
59 // Close any open list before starting header
60 if (currentlyInList) {
4761 return [`</ul>${headerHtml}`, false];
4862 } else {
4963 return [headerHtml, false];
5064 }
5165}
5266
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 }
67// List item parsing function
68function parseListItem(markdown, currentlyInList) {
69 // Check if line starts with list marker
70 if (!markdown.startsWith('*')) {
71 return [null, currentlyInList];
72 }
73
74 // Extract list item content (skip '* ')
75 const itemContent = markdown.substring(2);
76 const formattedItem = formatText(itemContent, true);
77 const listItemHtml = wrapText(formattedItem, 'li');
78
79 if (currentlyInList) {
80 // Continue existing list
81 return [listItemHtml, true];
82 } else {
83 // Start new list
84 return [`<ul>${listItemHtml}`, true];
6185 }
62 return [null, list];
6386}
6487
65function parseParagraph(markdown, list) {
66 if (!list) {
67 return [parseText(markdown, false), false];
88// Paragraph parsing function
89function parseParagraph(markdown, currentlyInList) {
90 // Handle empty input case
91 if (markdown === '') {
92 return [null, currentlyInList];
93 }
94
95 const paragraphHtml = formatText(markdown, false);
96
97 if (currentlyInList) {
98 // Close list before starting paragraph
99 return [`</ul>${paragraphHtml}`, false];
68100 } else {
69 return [`</ul>${parseText(markdown, false)}`, false];
101 // Just return paragraph
102 return [paragraphHtml, false];
70103 }
71104}
72105
73function parseLine(markdown, list) {
74 let [result, inListAfter] = parseHeader(markdown, list);
106// Main line parsing function - determines line type and delegates to appropriate parser
107function parseLine(markdown, currentlyInList) {
108 // Try to parse as header first
109 let [result, inListAfter] = parseHeader(markdown, currentlyInList);
110
111 // If not a header, try to parse as list item
75112 if (result === null) {
76 [result, inListAfter] = parseLineItem(markdown, list);
113 [result, inListAfter] = parseListItem(markdown, currentlyInList);
77114 }
115
116 // If not a list item, parse as paragraph (default case)
78117 if (result === null) {
79 [result, inListAfter] = parseParagraph(markdown, list);
118 [result, inListAfter] = parseParagraph(markdown, currentlyInList);
80119 }
120
121 // This should never happen as paragraph parsing always returns a result
81122 if (result === null) {
82 throw new Error('Remove this line and implement the function');
123 throw new Error('Failed to parse markdown line');
83124 }
125
84126 return [result, inListAfter];
85127}
86128
87129/**
88 * @param {string} markdown
89 * @returns {string}
130 * Main Markdown parser function
131 * @param {string} markdown - The markdown text to parse
132 * @returns {string} - The resulting HTML
90133 */
91134export function parse(markdown) {
135 // Handle empty input case
136 if (markdown === '') {
137 return '';
138 }
139
140 // Split input into lines for processing
92141 const lines = markdown.split('\n');
93 let result = '';
94 let list = false;
142 let htmlResult = '';
143 let currentlyInList = false;
144
145 // Process each line
95146 for (let i = 0; i < lines.length; i++) {
96 let [lineResult, newList] = parseLine(lines[i], list);
97 result += lineResult;
98 list = newList;
147 const [lineHtml, newListState] = parseLine(lines[i], currentlyInList);
148 // Only add to result if we got actual HTML content
149 if (lineHtml !== null) {
150 htmlResult += lineHtml;
151 }
152 currentlyInList = newListState;
99153 }
100 if (list) {
101 return result + '</ul>';
102 } else {
103 return result;
154
155 // Close any open list at the end of document
156 if (currentlyInList) {
157 htmlResult += '</ul>';
104158 }
159
160 return htmlResult;
105161}
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.