Files
blog/markdown.js

384 lines
8.3 KiB
JavaScript

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 `<h${this.level}>${this.text}</h${this.level}>`;
}
}
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 `<i>${this.text}</i>`;
}
}
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 `<b>${this.text}</b>`;
}
}
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 `<code>${this.text}</code>`;
}
}
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) => `<code style="display: block;">${text}</code>`)
.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 `<span>${this.text}</span>`;
}
}
/**
*
* @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 `<div>${html}</div>`;
};
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;
};