armor

Finished
1// ===== UTILITY FUNCTIONS =====
2
3/**
4 * Wraps text in an HTML tag
5 * @param {string} text - The text to wrap
6 * @param {string} tag - The HTML tag name
7 * @returns {string} The wrapped text
8 */
19function wrap(text, tag) {
210 return `<${tag}>${text}</${tag}>`;
311}
412
13/**
14 * Checks if text starts with a specific HTML tag
15 * @param {string} text - The text to check
16 * @param {string} tag - The tag to look for
17 * @returns {boolean} True if text starts with the tag
18 */
519function isTag(text, tag) {
620 return text.startsWith(`<${tag}>`);
721}
822
9function parser(markdown, delimiter, tag) {
23// ===== TEXT FORMATTING FUNCTIONS =====
24
25/**
26 * Generic parser for markdown formatting using delimiters
27 * @param {string} markdown - The markdown text to parse
28 * @param {string} delimiter - The delimiter to search for
29 * @param {string} tag - The HTML tag to replace with
30 * @returns {string} The formatted HTML
31 */
32function parseWithDelimiter(markdown, delimiter, tag) {
1033 const pattern = new RegExp(`${delimiter}(.+)${delimiter}`);
1134 const replacement = `<${tag}>$1</${tag}>`;
1235 return markdown.replace(pattern, replacement);
1336}
1437
15function parse__(markdown) {
16 return parser(markdown, '__', 'strong');
38/**
39 * Parses bold text using __ delimiters
40 * @param {string} markdown - The markdown text
41 * @returns {string} HTML with <strong> tags
42 */
43function parseBold(markdown) {
44 return parseWithDelimiter(markdown, '__', 'strong');
45}
46
47/**
48 * Parses italic text using _ delimiters
49 * @param {string} markdown - The markdown text
50 * @returns {string} HTML with <em> tags
51 */
52function parseItalic(markdown) {
53 return parseWithDelimiter(markdown, '_', 'em');
1754}
1855
19function parse_(markdown) {
20 return parser(markdown, '_', 'em');
56/**
57 * Applies all text formatting (bold and italic) to markdown text
58 * @param {string} markdown - The markdown text to format
59 * @returns {string} The formatted HTML text
60 */
61function parseTextFormatting(markdown) {
62 // Apply bold formatting first, then italic formatting
63 return parseItalic(parseBold(markdown));
2164}
2265
66/**
67 * Parses text content (handles inline formatting)
68 * @param {string} markdown - The markdown text
69 * @param {boolean} list - Whether this is part of a list
70 * @returns {string} The parsed HTML
71 */
2372function parseText(markdown, list) {
24 const parsedText = parse_(parse__(markdown));
73 const parsedText = parseTextFormatting(markdown);
2574 if (list) {
2675 return parsedText;
2776 } else {
2978 }
3079}
3180
81// ===== LINE TYPE PARSERS =====
82
83/**
84 * Parses header lines (starting with # characters)
85 * @param {string} markdown - The markdown line to parse
86 * @param {boolean} list - Whether we are currently in a list
87 * @returns {Array} [html, inListAfter] - The parsed HTML and list state
88 */
3289function parseHeader(markdown, list) {
33 let count = 0;
90 // Count consecutive # characters at the start
91 let headerLevel = 0;
3492 for (let i = 0; i < markdown.length; i++) {
3593 if (markdown[i] === '#') {
36 count += 1;
94 headerLevel += 1;
3795 } else {
3896 break;
3997 }
4098 }
41 if (count === 0 || count > 6) {
99
100 // Valid headers have 1-6 # characters
101 if (headerLevel === 0 || headerLevel > 6) {
42102 return [null, list];
43103 }
44 const headerTag = `h${count}`;
45 const headerHtml = wrap(markdown.substring(count + 1), headerTag);
104
105 const headerTag = `h${headerLevel}`;
106 const headerContent = markdown.substring(headerLevel + 1).trim();
107 const headerHtml = wrap(headerContent, headerTag);
108
109 // If we were in a list, close it before the header
46110 if (list) {
47111 return [`</ul>${headerHtml}`, false];
48112 } else {
50114 }
51115}
52116
117/**
118 * Parses list items (starting with *)
119 * @param {string} markdown - The markdown line to parse
120 * @param {boolean} list - Whether we are currently in a list
121 * @returns {Array} [html, inListAfter] - The parsed HTML and list state
122 */
53123function parseLineItem(markdown, list) {
54124 if (markdown.startsWith('*')) {
55 const innerHtml = wrap(parseText(markdown.substring(2), true), 'li');
125 // Remove the '* ' prefix and parse the remaining text
126 const itemContent = markdown.substring(2).trim();
127 const listItemHtml = wrap(parseText(itemContent, true), 'li');
128
56129 if (list) {
57 return [innerHtml, true];
130 // Continue existing list
131 return [listItemHtml, true];
58132 } else {
59 return [`<ul>${innerHtml}`, true];
133 // Start a new list
134 return [`<ul>${listItemHtml}`, true];
60135 }
61136 }
62137 return [null, list];
63138}
64139
140/**
141 * Parses regular paragraph text
142 * @param {string} markdown - The markdown line to parse
143 * @param {boolean} list - Whether we are currently in a list
144 * @returns {Array} [html, inListAfter] - The parsed HTML and list state
145 */
65146function parseParagraph(markdown, list) {
147 const paragraphHtml = parseText(markdown, false);
148
66149 if (!list) {
67 return [parseText(markdown, false), false];
150 // Not in a list, just return the paragraph
151 return [paragraphHtml, false];
68152 } else {
69 return [`</ul>${parseText(markdown, false)}`, false];
153 // Close the list and start the paragraph
154 return [`</ul>${paragraphHtml}`, false];
70155 }
71156}
72157
158/**
159 * Determines the type of markdown line and delegates to appropriate parser
160 * @param {string} markdown - The markdown line to parse
161 * @param {boolean} list - Whether we are currently in a list
162 * @returns {Array} [html, inListAfter] - The parsed HTML and list state
163 */
73164function parseLine(markdown, list) {
165 // Try to parse as header first
74166 let [result, inListAfter] = parseHeader(markdown, list);
167
168 // If not a header, try to parse as list item
75169 if (result === null) {
76170 [result, inListAfter] = parseLineItem(markdown, list);
77171 }
172
173 // If not a list item, parse as paragraph
78174 if (result === null) {
79175 [result, inListAfter] = parseParagraph(markdown, list);
80176 }
177
178 // This should never happen as one of the parsers should always succeed
81179 if (result === null) {
82 throw new Error('Remove this line and implement the function');
180 throw new Error('Unable to parse markdown line');
83181 }
182
84183 return [result, inListAfter];
85184}
86185
87186/**
88 * @param {string} markdown
89 * @returns {string}
187 * Main entry point for parsing markdown to HTML
188 * @param {string} markdown - The complete markdown text to parse
189 * @returns {string} The resulting HTML
90190 */
91191export function parse(markdown) {
192 // Split the markdown into individual lines
92193 const lines = markdown.split('\n');
93 let result = '';
94 let list = false;
194 let htmlResult = '';
195 let inList = false;
196
197 // Process each line individually
95198 for (let i = 0; i < lines.length; i++) {
96 let [lineResult, newList] = parseLine(lines[i], list);
97 result += lineResult;
98 list = newList;
199 const [lineHtml, newListState] = parseLine(lines[i], inList);
200 htmlResult += lineHtml;
201 inList = newListState;
99202 }
100 if (list) {
101 return result + '</ul>';
203
204 // Close any open list at the end
205 if (inList) {
206 return htmlResult + '</ul>';
102207 } else {
103 return result;
208 return htmlResult;
104209 }
105210}
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.