fortitude

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 inline formatting (__ for strong, _ for emphasis)
8 * @param {string} text - Text to parse
9 * @returns {string} - Text with inline formatting converted to HTML
10 */
11 parseInlineFormatting(text) {
12 // Parse strong (__text__)
13 text = text.replace(/__(.+)__/g, '<strong>$1</strong>');
14 // Parse emphasis (_text_)
15 text = text.replace(/_(.+)_/g, '<em>$1</em>');
16 return text;
2917 }
30}
3118
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;
39 }
40 }
41 if (count === 0 || count > 6) {
42 return [null, list];
19 /**
20 * Parse a header (# Header -> <h1>Header</h1>)
21 * @param {string} line - Line to parse
22 * @returns {string|null} - HTML header or null if not a header
23 */
24 parseHeader(line) {
25 const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
26 if (!headerMatch) return null;
27
28 const level = headerMatch[1].length;
29 const content = this.parseInlineFormatting(headerMatch[2]);
30 return `<h${level}>${content}</h${level}>`;
4331 }
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];
50 }
51}
5232
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 }
33 /**
34 * Parse a list item (* item -> <li>item</li>)
35 * @param {string} line - Line to parse
36 * @returns {string|null} - HTML list item or null if not a list item
37 */
38 parseListItem(line) {
39 if (!line.startsWith('* ')) return null;
40
41 const content = line.substring(2);
42 const formattedContent = this.parseInlineFormatting(content);
43 return `<li>${formattedContent}</li>`;
6144 }
62 return [null, list];
63}
6445
65function parseParagraph(markdown, list) {
66 if (!list) {
67 return [parseText(markdown, false), false];
68 } else {
69 return [`</ul>${parseText(markdown, false)}`, false];
46 /**
47 * Parse a paragraph (regular text -> <p>text</p>)
48 * @param {string} line - Line to parse
49 * @returns {string} - HTML paragraph
50 */
51 parseParagraph(line) {
52 const formattedContent = this.parseInlineFormatting(line);
53 return `<p>${formattedContent}</p>`;
7054 }
71}
7255
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);
80 }
81 if (result === null) {
82 throw new Error('Remove this line and implement the function');
56 /**
57 * Parse a single line of Markdown
58 * @param {string} line - Line to parse
59 * @param {boolean} inList - Whether we're currently in a list
60 * @returns {object} - Object with html and newListState properties
61 */
62 parseLine(line, inList) {
63 // Try to parse as header
64 const header = this.parseHeader(line);
65 if (header) {
66 return {
67 html: (inList ? '</ul>' : '') + header,
68 newListState: false
69 };
70 }
71
72 // Try to parse as list item
73 const listItem = this.parseListItem(line);
74 if (listItem) {
75 return {
76 html: (inList ? '' : '<ul>') + listItem,
77 newListState: true
78 };
79 }
80
81 // Parse as paragraph
82 const paragraph = this.parseParagraph(line);
83 return {
84 html: (inList ? '</ul>' : '') + paragraph,
85 newListState: false
86 };
8387 }
84 return [result, inListAfter];
8588}
8689
8790/**
88 * @param {string} markdown
89 * @returns {string}
91 * Parse Markdown text and convert it to HTML
92 * @param {string} markdown - Markdown text to parse
93 * @returns {string} - HTML representation of the Markdown
9094 */
9195export function parse(markdown) {
96 const parser = new MarkdownParser();
9297 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;
98 let html = '';
99 let inList = false;
100
101 for (const line of lines) {
102 const result = parser.parseLine(line, inList);
103 html += result.html;
104 inList = result.newListState;
99105 }
100 if (list) {
101 return result + '</ul>';
102 } else {
103 return result;
106
107 // Close any open list at the end
108 if (inList) {
109 html += '</ul>';
104110 }
111
112 return html;
105113}
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.