let it go

Finished
1/**
2 * Wraps text in an HTML tag
3 * @param {string} text - The text to wrap
4 * @param {string} tag - The HTML tag name
5 * @returns {string} The wrapped HTML
6 */
17function wrap(text, tag) {
28 return `<${tag}>${text}</${tag}>`;
39}
410
11/**
12 * Checks if text starts with a specific HTML tag
13 * @param {string} text - The text to check
14 * @param {string} tag - The HTML tag name
15 * @returns {boolean} True if text starts with the tag
16 */
517function isTag(text, tag) {
618 return text.startsWith(`<${tag}>`);
719}
820
21/**
22 * Parses markdown syntax and converts it to HTML
23 * @param {string} markdown - The markdown text to parse
24 * @param {string} delimiter - The markdown delimiter (e.g., __ or _)
25 * @param {string} tag - The HTML tag to wrap the content with
26 * @returns {string} The parsed HTML
27 */
928function parser(markdown, delimiter, tag) {
10 const pattern = new RegExp(`${delimiter}(.+)${delimiter}`);
29 // Use non-greedy matching to handle nested markdown correctly
30 const pattern = new RegExp(`${delimiter}(.+?)${delimiter}`);
1131 const replacement = `<${tag}>$1</${tag}>`;
1232 return markdown.replace(pattern, replacement);
1333}
1434
15function parse__(markdown) {
35/**
36 * Parses strong (bold) markdown syntax (__text__)
37 * @param {string} markdown - The markdown text to parse
38 * @returns {string} The parsed HTML with <strong> tags
39 */
40function parseStrong(markdown) {
1641 return parser(markdown, '__', 'strong');
1742}
1843
19function parse_(markdown) {
44/**
45 * Parses emphasis (italic) markdown syntax (_text_)
46 * @param {string} markdown - The markdown text to parse
47 * @returns {string} The parsed HTML with <em> tags
48 */
49function parseEmphasis(markdown) {
2050 return parser(markdown, '_', 'em');
2151}
2252
53/**
54 * Parses inline markdown (strong and emphasis) and wraps in paragraph if needed
55 * @param {string} markdown - The markdown text to parse
56 * @param {boolean} list - Whether this text is part of a list
57 * @returns {string} The parsed HTML
58 */
2359function parseText(markdown, list) {
24 const parsedText = parse_(parse__(markdown));
60 // Parse inline markdown (strong before emphasis to avoid conflicts)
61 let parsedText = parseStrong(markdown);
62 parsedText = parseEmphasis(parsedText);
63
64 // Handle empty input case
65 if (!list && parsedText === '') {
66 return '';
67 }
68
69 // Wrap in paragraph if not in a list, otherwise return as-is
2570 if (list) {
2671 return parsedText;
2772 } else {
2873 return wrap(parsedText, 'p');
29 }
74 }
3075}
3176
77/**
78 * Parses header markdown syntax (# Header)
79 * @param {string} markdown - The markdown text to parse
80 * @param {boolean} list - Whether we're currently in a list
81 * @returns {[string|null, boolean]} The parsed HTML and whether we're in a list
82 */
3283function parseHeader(markdown, list) {
33 let count = 0;
84 // Count leading # characters to determine header level
85 let headerLevel = 0;
3486 for (let i = 0; i < markdown.length; i++) {
3587 if (markdown[i] === '#') {
36 count += 1;
88 headerLevel += 1;
3789 } else {
3890 break;
3991 }
4092 }
41 if (count === 0 || count > 6) {
93
94 // Return null if not a valid header (no # or more than 6)
95 if (headerLevel === 0 || headerLevel > 6) {
4296 return [null, list];
4397 }
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 }
98
99 // Extract header content and create HTML
100 const headerContent = markdown.substring(headerLevel + 1).trim();
101 const headerTag = `h${headerLevel}`;
102 const headerHtml = wrap(headerContent, headerTag);
103
104 // Close list if we're currently in one, headers cannot be in lists
105 const listState = new ListState(list);
106 return [listState.closeListIfNeeded() + headerHtml, false];
51107}
52108
109/**
110 * Parses list item markdown syntax (* item)
111 * @param {string} markdown - The markdown text to parse
112 * @param {boolean} list - Whether we're currently in a list
113 * @returns {[string|null, boolean]} The parsed HTML and whether we're in a list
114 */
53115function 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 }
116 // Check if this is a list item (starts with '*')
117 if (!markdown.startsWith('*')) {
118 return [null, list];
119 }
120
121 // Extract list item content and parse inline markdown
122 const itemContent = markdown.substring(2).trim(); // Skip '* ' prefix
123 const parsedContent = parseText(itemContent, true); // true indicates we're in a list
124 const listItemHtml = wrap(parsedContent, 'li');
125
126 // Open list if we're not already in one
127 const listState = new ListState(list);
128 if (list) {
129 // Already in a list, just return the list item
130 return [listItemHtml, true];
131 } else {
132 // Not in a list, open a new list
133 return [listState.openListIfNeeded() + listItemHtml, true];
61134 }
62 return [null, list];
63135}
64136
137/**
138 * Parses paragraph markdown (plain text)
139 * @param {string} markdown - The markdown text to parse
140 * @param {boolean} list - Whether we're currently in a list
141 * @returns {[string|null, boolean]} The parsed HTML and whether we're in a list
142 */
65143function parseParagraph(markdown, list) {
66 if (!list) {
67 return [parseText(markdown, false), false];
68 } else {
69 return [`</ul>${parseText(markdown, false)}`, false];
70 }
144 // Parse the paragraph content (not in a list context)
145 const paragraphHtml = parseText(markdown, false);
146
147 // Close list if we're currently in one, paragraphs cannot be in lists
148 const listState = new ListState(list);
149 return [listState.closeListIfNeeded() + paragraphHtml, false];
71150}
72151
73function parseLine(markdown, list) {
74 let [result, inListAfter] = parseHeader(markdown, list);
75 if (result === null) {
76 [result, inListAfter] = parseLineItem(markdown, list);
152/**
153 * Helper class to manage list state transitions
154 * Encapsulates the logic for opening and closing HTML lists
155 */
156class ListState {
157 /**
158 * @param {boolean} isInList - Whether we're currently inside a list
159 */
160 constructor(isInList = false) {
161 this.isInList = isInList;
162 }
163
164 /**
165 * Generate HTML to close a list if we're currently in one
166 * @returns {string} The closing </ul> tag if needed, empty string otherwise
167 */
168 closeListIfNeeded() {
169 return this.isInList ? '</ul>' : '';
77170 }
78 if (result === null) {
79 [result, inListAfter] = parseParagraph(markdown, list);
171
172 /**
173 * Generate HTML to open a list if we're not currently in one
174 * @returns {string} The opening <ul> tag if needed, empty string otherwise
175 */
176 openListIfNeeded() {
177 return this.isInList ? '' : '<ul>';
80178 }
81 if (result === null) {
82 throw new Error('Remove this line and implement the function');
179}
180
181// Checkpoint: After refactoring parsing functions and introducing ListState class
182// Checkpoint: After adding comprehensive documentation to all functions
183// Checkpoint: After fixing empty input handling and regex pattern for nested markdown
184// Checkpoint: After final testing - all basic functionality working correctly
185
186// Define parsing strategies in order of priority
187// Headers are checked first, then list items, then paragraphs
188const PARSING_STRATEGIES = [
189 parseHeader,
190 parseLineItem,
191 parseParagraph
192];
193
194/**
195 * Parses a single line of markdown using the defined strategies
196 * @param {string} markdown - The markdown line to parse
197 * @param {ListState} listState - The current list state
198 * @returns {[string, boolean]} The parsed HTML and whether we're in a list
199 */
200function parseLine(markdown, listState) {
201 // Try each parsing strategy in order
202 for (const strategy of PARSING_STRATEGIES) {
203 const [result, inListAfter] = strategy(markdown, listState.isInList);
204 if (result !== null) {
205 return [result, inListAfter];
206 }
83207 }
84 return [result, inListAfter];
208 throw new Error('Unable to parse markdown line: ' + markdown);
85209}
86210
87211/**
88 * @param {string} markdown
89 * @returns {string}
212 * Parses markdown text and converts it to HTML
213 * @param {string} markdown - The markdown text to parse
214 * @returns {string} The parsed HTML
90215 */
91216export function parse(markdown) {
217 // Handle empty input case
218 if (markdown.trim() === '') {
219 return '';
220 }
221
92222 const lines = markdown.split('\n');
93223 let result = '';
94 let list = false;
224 const listState = new ListState(false);
225
95226 for (let i = 0; i < lines.length; i++) {
96 let [lineResult, newList] = parseLine(lines[i], list);
227 // Parse each line and update the list state
228 let [lineResult, newList] = parseLine(lines[i], listState);
97229 result += lineResult;
98 list = newList;
99 }
100 if (list) {
101 return result + '</ul>';
102 } else {
103 return result;
230 listState.isInList = newList;
104231 }
232
233 // Close any open list at the end
234 return result + listState.closeListIfNeeded();
105235}
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.