unicode

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');
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');
1/**
2 * A Markdown parser that converts Markdown syntax to HTML
3 * Refactored for better readability and maintainability
4 */
5class MarkdownParser {
6 /**
7 * Parse Markdown text and convert to HTML
8 * @param {string} markdown - The Markdown text to parse
9 * @returns {string} - The resulting HTML
10 */
11 parse(markdown) {
12 const lines = markdown.split('\n');
13 let html = '';
14 let inList = false;
15
16 for (const line of lines) {
17 const result = this.parseLine(line, inList);
18 html += result.html;
19 inList = result.inList;
20 }
21
22 // Close any open list at the end
23 if (inList) {
24 html += '</ul>';
25 }
26
27 return html;
2928 }
30}
31
32function 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;
29
30 /**
31 * Parse a single line of Markdown
32 * @param {string} line - A single line of Markdown
33 * @param {boolean} inList - Whether we're currently in a list
34 * @returns {Object} - Object containing the HTML result and updated list state
35 */
36 parseLine(line, inList) {
37 // Try to parse as header
38 const headerResult = this.parseHeader(line, inList);
39 if (headerResult.matched) {
40 return headerResult;
3941 }
42
43 // Try to parse as list item
44 const listItemResult = this.parseListItem(line, inList);
45 if (listItemResult.matched) {
46 return listItemResult;
47 }
48
49 // Parse as paragraph
50 return this.parseParagraph(line, inList);
4051 }
41 if (count === 0 || count > 6) {
42 return [null, list];
52
53 /**
54 * Wrap text in HTML tags
55 * @param {string} text - Text to wrap
56 * @param {string} tag - HTML tag to use
57 * @returns {string} - Text wrapped in HTML tags
58 */
59 wrap(text, tag) {
60 return `<${tag}>${text}</${tag}>`;
4361 }
44 const headerTag = `h${count}`;
45 const headerHtml = wrap(markdown.substring(count + 1), headerTag);
46 if (list) {
47 return [`</ul>${headerHtml}`, false];
48 } else {
49 return [headerHtml, false];
62
63 /**
64 * Parse bold text (__) and italic text (_)
65 * @param {string} text - Text to parse
66 * @returns {string} - Text with Markdown syntax converted to HTML
67 */
68 parseTextFormatting(text) {
69 // Parse bold (__text__) first to avoid conflicts with italic
70 let result = text.replace(/__(.+?)__/g, '<strong>$1</strong>');
71 // Parse italic (_text_) after bold to avoid conflicts
72 result = result.replace(/_(.+?)_/g, '<em>$1</em>');
73 return result;
5074 }
51}
52
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];
75
76 /**
77 * Parse header Markdown (# Header)
78 * @param {string} line - Line to parse
79 * @param {boolean} inList - Whether we're currently in a list
80 * @returns {Object} - Parsing result with matched flag, HTML, and list state
81 */
82 parseHeader(line, inList) {
83 // Count leading # characters
84 let count = 0;
85 for (let i = 0; i < line.length; i++) {
86 if (line[i] === '#') {
87 count++;
88 } else {
89 break;
90 }
6091 }
92
93 // Not a header or invalid header level
94 if (count === 0 || count > 6) {
95 return { matched: false };
96 }
97
98 const headerTag = `h${count}`;
99 const headerContent = line.substring(count + 1);
100 const formattedContent = this.parseTextFormatting(headerContent);
101 const headerHtml = this.wrap(formattedContent, headerTag);
102
103 // If we were in a list, close it before the header
104 if (inList) {
105 return {
106 matched: true,
107 html: `</ul>${headerHtml}`,
108 inList: false
109 };
110 }
111
112 return {
113 matched: true,
114 html: headerHtml,
115 inList: false
116 };
61117 }
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);
118
119 /**
120 * Parse list item Markdown (* item)
121 * @param {string} line - Line to parse
122 * @param {boolean} inList - Whether we're currently in a list
123 * @returns {Object} - Parsing result with matched flag, HTML, and list state
124 */
125 parseListItem(line, inList) {
126 if (!line.startsWith('*')) {
127 return { matched: false };
128 }
129
130 const itemContent = line.substring(2); // Skip '* '
131 const formattedContent = this.parseTextFormatting(itemContent);
132 const itemHtml = this.wrap(formattedContent, 'li');
133
134 if (inList) {
135 // Continue existing list
136 return {
137 matched: true,
138 html: itemHtml,
139 inList: true
140 };
141 } else {
142 // Start new list
143 return {
144 matched: true,
145 html: `<ul>${itemHtml}`,
146 inList: true
147 };
148 }
80149 }
81 if (result === null) {
82 throw new Error('Remove this line and implement the function');
150
151 /**
152 * Parse paragraph (default case)
153 * @param {string} line - Line to parse
154 * @param {boolean} inList - Whether we're currently in a list
155 * @returns {Object} - Parsing result with HTML and list state
156 */
157 parseParagraph(line, inList) {
158 const formattedContent = this.parseTextFormatting(line);
159 const paragraphHtml = this.wrap(formattedContent, 'p');
160
161 // If we were in a list, close it before the paragraph
162 if (inList) {
163 return {
164 matched: true, // Always matches as fallback
165 html: `</ul>${paragraphHtml}`,
166 inList: false
167 };
168 }
169
170 return {
171 matched: true, // Always matches as fallback
172 html: paragraphHtml,
173 inList: false
174 };
83175 }
84 return [result, inListAfter];
85176}
86177
87178/**
88 * @param {string} markdown
89 * @returns {string}
179 * Parse Markdown text and convert to HTML
180 * @param {string} markdown - The Markdown text to parse
181 * @returns {string} - The resulting HTML
90182 */
91183export 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 }
184 const parser = new MarkdownParser();
185 return parser.parse(markdown);
105186}
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.