sourdough

Finished
1// Utility functions for HTML tag manipulation
12function wrap(text, tag) {
23 return `<${tag}>${text}</${tag}>`;
34}
67 return text.startsWith(`<${tag}>`);
78}
89
10// Generic parser for inline formatting elements
911function parser(markdown, delimiter, tag) {
1012 const pattern = new RegExp(`${delimiter}(.+)${delimiter}`);
1113 const replacement = `<${tag}>$1</${tag}>`;
1214 return markdown.replace(pattern, replacement);
1315}
1416
15function parse__(markdown) {
17// Parse bold text (__text__)
18function parseBold(markdown) {
1619 return parser(markdown, '__', 'strong');
1720}
1821
19function parse_(markdown) {
22// Parse italic text (_text_)
23function parseItalic(markdown) {
2024 return parser(markdown, '_', 'em');
2125}
2226
23function parseText(markdown, list) {
24 const parsedText = parse_(parse__(markdown));
25 if (list) {
26 return parsedText;
27// Parse inline formatting (bold and italic) and wrap in paragraph tags if needed
28function parseInlineFormatting(markdown, isListItem = false) {
29 // First parse bold, then italic
30 const formattedText = parseItalic(parseBold(markdown));
31
32 // If this is part of a list item, don't wrap in paragraph tags
33 if (isListItem) {
34 return formattedText;
2735 } else {
28 return wrap(parsedText, 'p');
36 return wrap(formattedText, 'p');
2937 }
3038}
3139
32function parseHeader(markdown, list) {
33 let count = 0;
40// Parse header elements (# Header)
41function parseHeader(markdown, currentlyInList) {
42 // Count the number of # characters at the beginning
43 let headerLevel = 0;
3444 for (let i = 0; i < markdown.length; i++) {
3545 if (markdown[i] === '#') {
36 count += 1;
46 headerLevel += 1;
3747 } else {
3848 break;
3949 }
4050 }
41 if (count === 0 || count > 6) {
42 return [null, list];
51
52 // Headers must be between 1 and 6 # characters
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 // Create the appropriate header tag (h1, h2, etc.)
58 const headerTag = `h${headerLevel}`;
59 const headerContent = markdown.substring(headerLevel + 1); // Skip # and space
60 const headerHtml = wrap(headerContent, headerTag);
61
62 // If we were in a list, close it before the header
63 if (currentlyInList) {
4764 return [`</ul>${headerHtml}`, false];
4865 } else {
4966 return [headerHtml, false];
5067 }
5168}
5269
53function parseLineItem(markdown, list) {
70// Parse list items (* item)
71function parseListItem(markdown, currentlyInList) {
5472 if (markdown.startsWith('*')) {
55 const innerHtml = wrap(parseText(markdown.substring(2), true), 'li');
56 if (list) {
57 return [innerHtml, true];
73 // Parse the content of the list item with inline formatting
74 const itemContent = markdown.substring(2); // Skip "* "
75 const formattedContent = wrap(parseInlineFormatting(itemContent, true), 'li');
76
77 // If we're already in a list, just add the item
78 // Otherwise, start a new list
79 if (currentlyInList) {
80 return [formattedContent, true];
5881 } else {
59 return [`<ul>${innerHtml}`, true];
82 return [`<ul>${formattedContent}`, true];
6083 }
6184 }
62 return [null, list];
85 return [null, currentlyInList];
6386}
6487
65function parseParagraph(markdown, list) {
66 if (!list) {
67 return [parseText(markdown, false), false];
88// Parse paragraph elements (default case)
89function parseParagraph(markdown, currentlyInList) {
90 // If we're not in a list, wrap in paragraph tags
91 // If we were in a list, close it first
92 if (!currentlyInList) {
93 return [parseInlineFormatting(markdown, false), false];
6894 } else {
69 return [`</ul>${parseText(markdown, false)}`, false];
95 return [`</ul>${parseInlineFormatting(markdown, false)}`, false];
7096 }
7197}
7298
73function parseLine(markdown, list) {
74 let [result, inListAfter] = parseHeader(markdown, list);
99// Parse a single line of markdown
100function parseLine(markdown, currentlyInList) {
101 // Try to parse as header first
102 let [result, newListState] = parseHeader(markdown, currentlyInList);
103
104 // If not a header, try as list item
75105 if (result === null) {
76 [result, inListAfter] = parseLineItem(markdown, list);
106 [result, newListState] = parseListItem(markdown, currentlyInList);
77107 }
108
109 // If not a list item, treat as paragraph
78110 if (result === null) {
79 [result, inListAfter] = parseParagraph(markdown, list);
111 [result, newListState] = parseParagraph(markdown, currentlyInList);
80112 }
113
114 // This should never happen with valid input
81115 if (result === null) {
82 throw new Error('Remove this line and implement the function');
116 throw new Error('Unable to parse line: ' + markdown);
83117 }
84 return [result, inListAfter];
118
119 return [result, newListState];
85120}
86121
87122/**
88 * @param {string} markdown
89 * @returns {string}
123 * Parse markdown text and convert to HTML
124 * @param {string} markdown - The markdown text to parse
125 * @returns {string} - The resulting HTML
90126 */
91127export function parse(markdown) {
128 // Split the markdown into individual lines
92129 const lines = markdown.split('\n');
93 let result = '';
94 let list = false;
130 let htmlResult = '';
131 let inList = false;
132
133 // Process each line
95134 for (let i = 0; i < lines.length; i++) {
96 let [lineResult, newList] = parseLine(lines[i], list);
97 result += lineResult;
98 list = newList;
135 let [lineHtml, newListState] = parseLine(lines[i], inList);
136 htmlResult += lineHtml;
137 inList = newListState;
99138 }
100 if (list) {
101 return result + '</ul>';
139
140 // If we ended while still in a list, close it
141 if (inList) {
142 return htmlResult + '</ul>';
102143 } else {
103 return result;
144 return htmlResult;
104145 }
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.