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.children.map((c) => c.render()).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}`;
}
}
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 TextRow extends Symbol {
/**
* @type {string}
*/
text = "";
/**
* @type {Symbol[]}
*/
children = [];
/**
* @type {Symbol[]}
*/
allowedChildren = [
Bold,
Italic,
ImageLink,
Link,
MultiLineCode,
SingleLineCode,
TextRow,
];
/**
*
* @param {string} line
* @returns {boolean}
*/
static canParse(line) {
return true;
}
/**
*
* @param {LineFeed} lineFeed
* @returns {Symbol}
*/
static create(lineFeed) {
const instance = new TextRow();
instance.text = lineFeed.claim();
return instance;
}
/**
*
* @param {string} assetDirectory
* @returns {void}
*/
process(assetDirectory) {
for (let i = 0; i < this.text.length; i++) {
if (i === this.text.length - 1) {
const plainText = new PlainText();
plainText.text = this.text;
this.children = [plainText];
break;
}
const line = this.text.slice(i);
const symbols = toSymbols(line, assetDirectory, this.allowedChildren);
const symbolsIsOnlyOneTextRow =
symbols.length === 1 && symbols[0] instanceof TextRow;
if (!symbols.length || symbolsIsOnlyOneTextRow) {
continue;
}
const plainText = new PlainText();
plainText.text = this.text.slice(0, i);
this.children = [plainText, ...symbols];
break;
}
this.children.forEach((c) => c.process(assetDirectory));
}
render() {
return this.children.map((c) => c.render()).join("");
}
}
class PlainText extends Symbol {
/**
* @type {string}
*/
text = "";
/**
* @type {Symbol[]}
*/
children = [];
/**
*
* @param {string} line
* @returns {boolean}
*/
static canParse(line) {
return true;
}
/**
*
* @param {LineFeed} lineFeed
* @returns {Symbol}
*/
static create(lineFeed) {
const instance = new PlainText();
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 AllSymbols = [
JustALineBreak,
Heading,
UnorderedListItem,
OrderedListItem,
Bold,
Italic,
ImageLink,
Link,
MultiLineCode,
SingleLineCode,
TextRow,
];
/**
*
* @param {string} markdown
* @param {string} assetDirectory
* @returns {string}
*/
export const toHtml = (markdown, assetDirectory) => {
// Stage one, markdown to symbols
const symbols = toSymbols(markdown, assetDirectory);
// Stage two, expanding symbols
symbols.forEach((s) => s.process(assetDirectory));
// Stage three, operations based on symbol relationship
const stageThree = stageThreeProcessing(symbols);
// Stage four, rendering
const html = stageThree
// .filter((s) => !(s instanceof JustALineBreak))
.map((s) => s.render())
.join("");
return `