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, assetDirectory) { throw new Error("Not implemented"); } /** * * @param {string} assetDirectory * @returns {void} */ process(assetDirectory) { 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; } /** * * @param {string} assetDirectory * @returns {void} */ process(assetDirectory) { return; } render() { return `${this.text}`; } } class Paragraph extends Symbol { /** * @type {string[]} */ lines = []; /** * @type {Symbol[]} */ children = []; /** * * @param {string} line * @returns {boolean} */ static canParse(line) { return line === ""; } /** * * @param {LineFeed} lineFeed * @returns {Symbol} */ static create(lineFeed) { const instance = new Paragraph(); lineFeed.claim(); const lines = []; let endOfParagraph = false; while (!endOfParagraph && !lineFeed.isEmpty()) { const line = lineFeed.peek(); if (line === "") { endOfParagraph = true; continue; } lines.push(lineFeed.claim()); } instance.lines = lines; return instance; } /** * * @param {string} assetDirectory * @returns {void} */ process(assetDirectory) { return; if (this.lines.length === 0) return; const symbols = toSymbols(this.lines.join("\n"), assetDirectory); const lastIterationSymbolsAsJson = JSON.stringify(symbols); // do { // symbols.forEach((s) => s.process()); // } while (JSON.stringify(symbols) !== lastIterationSymbolsAsJson); this.lines = []; this.children = symbols; } render() { return `

${this.lines.join("")}

`; } } class UnorderedListItem extends Symbol { /** * @type {string} */ text = ""; /** * @type {number} */ level = 0; /** * * @param {string} line * @returns {boolean} */ static canParse(line) { return line.trim().startsWith("- "); } /** * * @param {LineFeed} lineFeed * @returns {Symbol} */ static create(lineFeed) { const instance = new UnorderedListItem(); const line = lineFeed.claim(); instance.text = line.replaceAll("-", "").trim(); instance.level = getAmountOfTokenInBeginningOfFile(" ", line); return instance; } /** * * @param {string} assetDirectory * @returns {void} */ process(assetDirectory) { return; } render() { return `
  • ${this.text} indentation level ${this.level}
  • `; } } class OrderedListItem extends Symbol { /** * @type {string} */ text = ""; /** * @type {number} */ level = 0; /** * * @param {string} line * @returns {boolean} */ static canParse(line) { return new RegExp(/^\d\. /).test(line.trim()); } /** * * @param {LineFeed} lineFeed * @returns {Symbol} */ static create(lineFeed) { const instance = new UnorderedListItem(); const line = lineFeed.claim(); instance.text = line.trim().replace(/^\d\. /, ""); instance.level = getAmountOfTokenInBeginningOfFile(" ", line); return instance; } /** * * @param {string} assetDirectory * @returns {void} */ process(assetDirectory) { return; } render() { return `
  • ${this.text} indentation level ${this.level}
  • `; } } class Link extends Symbol { /** * @type {RegExp} */ static canParseRegExp = new RegExp(/^\[.*\]\(.*\)/); /** * @type {RegExp} */ static textAndLinkRegExp = new RegExp(/\[(?.*)\]\((?.*)\)/); /** * @type {string} */ text = ""; /** * @type {string} */ link = ""; /** * * @param {string} line * @returns {boolean} */ static canParse(line) { return Link.canParseRegExp.test(line.trim()); } /** * * @param {LineFeed} lineFeed * @returns {Symbol} */ static create(lineFeed) { const instance = new Link(); const line = lineFeed.claim().trim(); const [linkLine, rest] = extractTokenAndRest(Link.textAndLinkRegExp, line); lineFeed.push(rest); const { text, link } = Link.textAndLinkRegExp.exec(linkLine)?.groups ?? {}; instance.link = link ?? ""; instance.text = text ?? ""; return instance; } /** * * @param {string} assetDirectory * @returns {void} */ process(assetDirectory) { return; } render() { return `${this.text}`; } } class ImageLink extends Symbol { /** * @type {RegExp} */ static canParseRegExp = new RegExp(/^!\[.*\]\(.*\)/); /** * @type {RegExp} */ static textAndLinkRegExp = new RegExp(/!\[(?.*)\]\((?.*)\)/); /** * @type {string} */ static assetDirectoryToken = "@asset/"; /** * @type {string} */ alt = ""; /** * @type {string} */ link = ""; /** * * @param {string} line * @returns {boolean} */ static canParse(line) { return ImageLink.canParseRegExp.test(line.trim()); } /** * * @param {LineFeed} lineFeed * @returns {Symbol} */ static create(lineFeed, assetDirectory) { const instance = new ImageLink(); const line = lineFeed.claim().trim(); const [linkLine, rest] = extractTokenAndRest( ImageLink.textAndLinkRegExp, line ); lineFeed.push(rest); const { text, link } = ImageLink.textAndLinkRegExp.exec(linkLine)?.groups ?? {}; instance.link = link.replace(ImageLink.assetDirectoryToken, assetDirectory) ?? ""; instance.alt = text ?? ""; return instance; } /** * * @param {string} assetDirectory * @returns {void} */ process(assetDirectory) { return; } render() { return `${this.alt}`; } } 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; } /** * * @param {string} assetDirectory * @returns {void} */ process(assetDirectory) { return; } 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; } /** * * @param {string} assetDirectory * @returns {void} */ process(assetDirectory) { return; } 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; } /** * * @param {string} assetDirectory * @returns {void} */ process(assetDirectory) { return; } 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; } /** * * @param {string} assetDirectory * @returns {void} */ process(assetDirectory) { return; } render() { return `
    ${this.text}
    `; } } class Text 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 Text(); instance.text = lineFeed.claim(); return instance; } /** * * @param {string} assetDirectory * @returns {void} */ process(assetDirectory) { return; } render() { return `${this.text}`; } } /** * * @param {string|RexExp} token * @param {string} string * @returns {string[]} */ const splitStringAtToken = (token, string) => { let index = 0; let length = 0; if (typeof token?.exec === "function") { const exp = new RegExp(token); const match = exp.exec(string); index = match?.index ?? -1; length = match?.[0].length ?? 0; } else { index = string.indexOf(token); length = token.length; } const splitTokenNotFound = index === -1; if (splitTokenNotFound) return [string, ""]; return [string.slice(0, index), string.slice(index + length)]; }; /** * * @param {string|RexExp} token * @param {string} string * @returns {string[]} */ const extractTokenAndRest = (token, string) => { let index = 0; let length = 0; if (typeof token?.exec === "function") { const exp = new RegExp(token); const match = exp.exec(string); index = match?.index ?? -1; length = match?.[0].length ?? 0; } else { index = string.indexOf(token); length = token.length; } const splitTokenNotFound = index === -1; if (splitTokenNotFound) return [string, ""]; return [string.slice(0, index + length), string.slice(index + length)]; }; /** * * @param {string} token * @param {string} string */ const getAmountOfTokenInBeginningOfFile = (token, string) => { let count = 0; let searchIndex = 0; while (true) { if (searchIndex + (token.length - 1) > string.length) return count; const index = string.slice(searchIndex).indexOf(token); const indexNotAtStartOfString = index !== 0; if (indexNotAtStartOfString) return count; count++; searchIndex += token.length; } }; /** * @type {Symbol[]} */ const Factories = [ Paragraph, Heading, UnorderedListItem, OrderedListItem, Bold, Italic, ImageLink, Link, MultiLineCode, SingleLineCode, Text, ]; /** * * @param {string} markdown * @param {string} assetDirectory * @returns {string} */ export const toHtml = (markdown, assetDirectory) => { const symbols = toSymbols(markdown, assetDirectory); let lastIterationsSymbolsAsJson = JSON.stringify(symbols); do { console.log("starting processing"); symbols.forEach((s) => s.process(assetDirectory)); } while (JSON.stringify(symbols) !== lastIterationsSymbolsAsJson); const html = symbols.map((s) => s.render()).join(""); return `
    ${html}
    `; }; /** * * @param {string} markdown * @param {string} assetDirectory * @returns {Symbol[]} */ const toSymbols = (markdown, assetDirectory) => { 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, assetDirectory); symbols.push(symbol); } return symbols; };