fortitude

Finished
1// Utility function to wrap text in HTML tags
12function wrap(text, tag) {
23 return `<${tag}>${text}</${tag}>`;
34}
45
6// Utility function to check if text starts with a specific HTML tag
57function isTag(text, tag) {
68 return text.startsWith(`<${tag}>`);
79}
810
9function parser(markdown, delimiter, tag) {
11// Generic parser for markdown delimiters (bold, italic, etc.)
12function parseMarkdownDelimiter(markdown, delimiter, tag) {
1013 const pattern = new RegExp(`${delimiter}(.+)${delimiter}`);
1114 const replacement = `<${tag}>$1</${tag}>`;
1215 return markdown.replace(pattern, replacement);
1316}
1417
15function parse__(markdown) {
16 return parser(markdown, '__', 'strong');
18// Parse bold text (double underscores)
19function parseBold(markdown) {
20 return parseMarkdownDelimiter(markdown, '__', 'strong');
1721}
1822
19function parse_(markdown) {
20 return parser(markdown, '_', 'em');
23// Parse italic text (single underscore)
24function parseItalic(markdown) {
25 return parseMarkdownDelimiter(markdown, '_', 'em');
2126}
2227
23function parseText(markdown, list) {
24 const parsedText = parse_(parse__(markdown));
25 if (list) {
26 return parsedText;
27 } else {
28 return wrap(parsedText, 'p');
29 }
28// Parse inline formatting (bold and italic) and optionally wrap in paragraph tags
29function parseInlineFormatting(markdown, wrapInParagraph = true) {
30 const parsedText = parseItalic(parseBold(markdown));
31 return wrapInParagraph ? wrap(parsedText, 'p') : parsedText;
3032}
3133
32function parseHeader(markdown, list) {
34// Helper function to count leading # characters
35function countLeadingHashes(text) {
3336 let count = 0;
34 for (let i = 0; i < markdown.length; i++) {
35 if (markdown[i] === '#') {
36 count += 1;
37 for (let i = 0; i < text.length; i++) {
38 if (text[i] === '#') {
39 count++;
3740 } else {
3841 break;
3942 }
4043 }
41 if (count === 0 || count > 6) {
42 return [null, list];
44 return count;
45}
46
47// Parse headers (lines starting with #)
48// Returns [html, inList] tuple where html is the parsed HTML or null if not a header
49function parseHeader(markdown, currentlyInList) {
50 const headerLevel = countLeadingHashes(markdown);
51
52 // Not a header if no hashes or too many hashes
53 if (headerLevel === 0 || headerLevel > 6) {
54 return [null, currentlyInList];
4355 }
44 const headerTag = `h${count}`;
45 const headerHtml = wrap(markdown.substring(count + 1), headerTag);
46 if (list) {
56
57 const headerTag = `h${headerLevel}`;
58 const headerContent = markdown.substring(headerLevel + 1).trim();
59 const headerHtml = wrap(headerContent, headerTag);
60
61 // If we were in a list, close it before the header
62 if (currentlyInList) {
4763 return [`</ul>${headerHtml}`, false];
4864 } else {
4965 return [headerHtml, false];
5066 }
5167}
5268
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 }
69// Parse list items (lines starting with *)
70// Returns [html, inList] tuple where html is the parsed HTML or null if not a list item
71function parseLineItem(markdown, currentlyInList) {
72 if (!markdown.startsWith('*')) {
73 return [null, currentlyInList];
74 }
75
76 const listItemContent = markdown.substring(2).trim();
77 const listItemHtml = wrap(parseInlineFormatting(listItemContent, false), 'li');
78
79 if (currentlyInList) {
80 return [listItemHtml, true];
81 } else {
82 return [`<ul>${listItemHtml}`, true];
6183 }
62 return [null, list];
6384}
6485
65function parseParagraph(markdown, list) {
66 if (!list) {
67 return [parseText(markdown, false), false];
86// Parse paragraph (regular text lines)
87// Returns [html, inList] tuple
88function parseParagraph(markdown, currentlyInList) {
89 const paragraphHtml = parseInlineFormatting(markdown, true);
90
91 // If we were in a list, close it before the paragraph
92 if (currentlyInList) {
93 return [`</ul>${paragraphHtml}`, false];
6894 } else {
69 return [`</ul>${parseText(markdown, false)}`, false];
95 return [paragraphHtml, false];
7096 }
7197}
7298
73function parseLine(markdown, list) {
74 let [result, inListAfter] = parseHeader(markdown, list);
99// Parse a single line of markdown by trying different parsers in order
100// Returns [html, inList] tuple
101function parseLine(markdown, currentlyInList) {
102 // Try to parse as header first
103 let [result, inListAfter] = parseHeader(markdown, currentlyInList);
104
105 // If not a header, try to parse as list item
75106 if (result === null) {
76 [result, inListAfter] = parseLineItem(markdown, list);
107 [result, inListAfter] = parseLineItem(markdown, currentlyInList);
77108 }
109
110 // If not a list item, parse as paragraph
78111 if (result === null) {
79 [result, inListAfter] = parseParagraph(markdown, list);
112 [result, inListAfter] = parseParagraph(markdown, currentlyInList);
80113 }
114
115 // At this point, result should never be null
81116 if (result === null) {
82 throw new Error('Remove this line and implement the function');
117 throw new Error('Failed to parse markdown line');
83118 }
119
84120 return [result, inListAfter];
85121}
86122
87123/**
88 * @param {string} markdown
89 * @returns {string}
124 * Parse markdown text and convert it to HTML
125 * @param {string} markdown - The markdown text to parse
126 * @returns {string} - The resulting HTML
90127 */
91128export function parse(markdown) {
92129 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;
130 let htmlResult = '';
131 let currentlyInList = false;
132
133 // Process each line of the markdown text
134 for (const line of lines) {
135 const [lineHtml, newListState] = parseLine(line, currentlyInList);
136 htmlResult += lineHtml;
137 currentlyInList = newListState;
99138 }
100 if (list) {
101 return result + '</ul>';
102 } else {
103 return result;
139
140 // Close any open list tag at the end
141 if (currentlyInList) {
142 htmlResult += '</ul>';
104143 }
144
145 return htmlResult;
105146}
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.