class LineFeed { originalText = undefined; /** * @type {string[]} */ feed = undefined; /** * * @param {string} markdown */ constructor(markdown) { this.originalText = markdown; // TODO: Need to identify line endings this.feed = markdown.split("\n"); } /** * @returns {string} */ peek() { return this.feed[0] ?? undefined; } /** * * @returns {string} */ claim() { const line = this.feed.shift(); if (line === undefined) throw new Error("Feed is empty"); return line; } /** * * @param {string} line */ push(line) { if (line === undefined || line === null) return; this.feed = [line, ...this.feed]; } /** * * @returns {boolean} */ isEmpty() { return this.feed.length === 0; } } class Symbol { static canParse(line) { throw new Error("Not implemented"); } static create(lineFeed) { throw new Error("Not implemented"); } render() { throw new Error("Not implemented"); } } class Heading extends Symbol { /** * @type {string} */ text = ""; /** * @type {number} */ level = 1; /** * * @param {string} line * @returns {boolean} */ static canParse(line) { return line.trim().startsWith("#"); } /** * * @param {LineFeed} lineFeed * @returns {Symbol} */ static create(lineFeed) { const instance = new Heading(); const line = lineFeed.claim(); instance.text = line.replaceAll("#", "").trim(); instance.level = line.split("").reduce((aggregate, current) => { return current === "#" ? aggregate + 1 : aggregate; }, 0); return instance; } render() { return `${this.text}`; } } class Italic extends Symbol { /** * @type {string} */ text = ""; /** * * @param {string} line * @returns {boolean} */ static canParse(line) { const trimmedLine = line.trim(); return trimmedLine.startsWith("_"); } /** * * @param {LineFeed} lineFeed * @returns {Symbol} */ static create(lineFeed) { const instance = new Italic(); const line = lineFeed.claim().trim(); const lineWithoutStartingUnderscore = line.slice(1, line.length); const lineEndsWithUnderscore = lineWithoutStartingUnderscore.endsWith("_"); if (lineEndsWithUnderscore) { const lineWithoutUnderscores = lineWithoutStartingUnderscore.slice( 0, lineWithoutStartingUnderscore.length - 1 ); instance.text = lineWithoutUnderscores; return instance; } const endOfItalicString = lineWithoutStartingUnderscore.indexOf("_"); const lineWithoutUnderscores = lineWithoutStartingUnderscore.slice( 0, endOfItalicString ); const rest = lineWithoutStartingUnderscore.slice(endOfItalicString + 1); lineFeed.push(rest); instance.text = lineWithoutUnderscores; return instance; } render() { return `${this.text}`; } } class Bold extends Symbol { /** * @type {string} */ text = ""; /** * * @param {string} line * @returns {boolean} */ static canParse(line) { const trimmedLine = line.trim(); return trimmedLine.startsWith("**"); } /** * * @param {LineFeed} lineFeed * @returns {Symbol} */ static create(lineFeed) { const instance = new Bold(); const line = lineFeed.claim().trim(); const lineWithoutStartingUnderscore = line.slice(2, line.length); const lineEndsWithStars = lineWithoutStartingUnderscore.endsWith("**"); if (lineEndsWithStars) { const lineWithoutUnderscores = lineWithoutStartingUnderscore.slice( 0, lineWithoutStartingUnderscore.length - 2 ); instance.text = lineWithoutUnderscores; return instance; } const endOfBoldString = lineWithoutStartingUnderscore.indexOf("**"); const lineWithoutUnderscores = lineWithoutStartingUnderscore.slice( 0, endOfBoldString ); const rest = lineWithoutStartingUnderscore.slice(endOfBoldString + 2); lineFeed.push(rest); instance.text = lineWithoutUnderscores; return instance; } render() { return `${this.text}`; } } class SingleLineCode extends Symbol { /** * @type {string} */ text = ""; /** * * @param {string} line * @returns {boolean} */ static canParse(line) { const trimmedLine = line.trim(); return trimmedLine.startsWith("`"); } /** * * @param {LineFeed} lineFeed * @returns {Symbol} */ static create(lineFeed) { const instance = new SingleLineCode(); const line = lineFeed.claim().trim(); const lineWithoutStartingUnderscore = line.slice(1, line.length); const lineEndsWithUnderscore = lineWithoutStartingUnderscore.endsWith("`"); if (lineEndsWithUnderscore) { const lineWithoutUnderscores = lineWithoutStartingUnderscore.slice( 0, lineWithoutStartingUnderscore.length - 1 ); instance.text = lineWithoutUnderscores; return instance; } const endOfItalicString = lineWithoutStartingUnderscore.indexOf("`"); const lineWithoutUnderscores = lineWithoutStartingUnderscore.slice( 0, endOfItalicString ); const rest = lineWithoutStartingUnderscore.slice(endOfItalicString + 1); lineFeed.push(rest); instance.text = lineWithoutUnderscores; return instance; } render() { return `${this.text}`; } } class MultiLineCode extends Symbol { /** * @type {string} */ text = ""; /** * * @param {string} line * @returns {boolean} */ static canParse(line) { const trimmedLine = line.trim(); return trimmedLine.startsWith("```"); } /** * * @param {LineFeed} lineFeed * @returns {Symbol} */ static create(lineFeed) { const instance = new MultiLineCode(); const line = lineFeed.claim().trim(); const lineWithoutStartTag = line.slice(3, line.length); const sameLineContainsEnding = lineWithoutStartTag.includes("```"); if (sameLineContainsEnding) { const endsWithTag = lineWithoutStartTag.endsWith("```"); if (endsWithTag) { instance.text = lineWithoutStartTag.slice( 0, lineWithoutStartTag.length - 3 ); return instance; } const [text, rest] = splitStringAtToken("```", lineWithoutStartTag); lineFeed.push(rest); instance.text = text; return instance; } const lines = [lineWithoutStartTag]; let endTokenFound = false; while (!endTokenFound && !lineFeed.isEmpty()) { const nextLine = lineFeed.claim(); if (!nextLine.includes("```")) { lines.push(nextLine); continue; } if (nextLine.endsWith("```")) { lines.push(nextLine.slice(0, nextLine.length - 3)); break; } const [lastLine, rest] = splitStringAtToken("```", nextLine); lineFeed.push(rest); lines.push(lastLine); break; } instance.text = lines.join("\n"); return instance; } render() { return this.text .split("\n") .map((text) => `${text}`) .join(""); } } class CatchAll extends Symbol { /** * @type {string} */ text = ""; /** * * @param {string} line * @returns {boolean} */ static canParse(line) { return true; } /** * * @param {LineFeed} lineFeed * @returns {Symbol} */ static create(lineFeed) { const instance = new CatchAll(); instance.text = lineFeed.claim(); return instance; } render() { return `${this.text}`; } } /** * * @param {string} token * @param {string} string * @returns {string[]} */ const splitStringAtToken = (token, string) => { const index = string.indexOf(token); return [string.slice(0, index), string.slice(index + token.length)]; }; const Factories = [ Heading, Bold, Italic, MultiLineCode, SingleLineCode, CatchAll, ]; export const toHtml = (markdown) => { const symbols = toSymbols(markdown); const html = symbols.map((s) => s.render()).join(""); return `
${html}
`; }; const toSymbols = (markdown) => { const lineFeed = new LineFeed(markdown); const symbols = []; while (!lineFeed.isEmpty()) { const line = lineFeed.peek(); const factory = Factories.find((factory) => factory.canParse(line)); const symbol = factory.create(lineFeed); symbols.push(symbol); } return symbols; };