Restructured toolchain to its own workspace
This commit is contained in:
3
packages/@zblog/toolchain/.gitignore
vendored
Normal file
3
packages/@zblog/toolchain/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
bin
|
||||
dist
|
||||
17
packages/@zblog/toolchain/package.json
Normal file
17
packages/@zblog/toolchain/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@zblog/toolchain",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"module": "bin/zblog-toolchain.js",
|
||||
"main": "bin/zblog-toolchain.js",
|
||||
"bin": {
|
||||
"zblog-toolchain": "./bin/zblog-toolchain.js"
|
||||
},
|
||||
"types": "./bin/zblog-toolchain.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
||||
965
packages/@zblog/toolchain/src/markdown.ts
Normal file
965
packages/@zblog/toolchain/src/markdown.ts
Normal file
@@ -0,0 +1,965 @@
|
||||
class LineFeed {
|
||||
originalText: string | undefined = undefined;
|
||||
/**
|
||||
* @type {string[]}
|
||||
*/
|
||||
feed: string[] | undefined = undefined;
|
||||
/**
|
||||
*
|
||||
* @param {string} markdown
|
||||
*/
|
||||
constructor(markdown: string) {
|
||||
this.originalText = markdown;
|
||||
// TODO: Need to identify line endings
|
||||
this.feed = markdown.split("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
peek(): string {
|
||||
return this.feed?.[0] ?? "";
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
claim(): string {
|
||||
const line = this.feed?.shift();
|
||||
if (line === undefined) throw new Error("Feed is empty");
|
||||
return line;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} line
|
||||
*/
|
||||
push(line: string | null | undefined) {
|
||||
if (line === undefined || line === null) return;
|
||||
this.feed = [line, ...(this.feed ?? [])];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isEmpty(): boolean {
|
||||
return this.feed?.length === 0;
|
||||
}
|
||||
}
|
||||
|
||||
class Symbol {
|
||||
static canParse(line: string) {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
static create(lineFeed: LineFeed, assetDirectory: string): Symbol {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {string} assetDirectory
|
||||
* @returns {void}
|
||||
*/
|
||||
process(assetDirectory: string): void {
|
||||
throw new Error("Not Implemented");
|
||||
}
|
||||
render() {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
class Heading extends Symbol {
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
text: string = "";
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
level: number = 1;
|
||||
/**
|
||||
*
|
||||
* @param {string} line
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static canParse(line: string): boolean {
|
||||
return line.trim().startsWith("#");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {LineFeed} lineFeed
|
||||
* @returns {Symbol}
|
||||
*/
|
||||
static create(lineFeed: LineFeed): Symbol {
|
||||
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: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
render() {
|
||||
return `<h${this.level}>${this.text}</h${this.level}>`;
|
||||
}
|
||||
}
|
||||
|
||||
class JustALineBreak extends Symbol {
|
||||
/**
|
||||
*
|
||||
* @param {string} line
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static canParse(line: string): boolean {
|
||||
return line === "";
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {LineFeed} lineFeed
|
||||
* @returns {Symbol}
|
||||
*/
|
||||
static create(lineFeed: LineFeed): Symbol {
|
||||
const instance = new JustALineBreak();
|
||||
lineFeed.claim();
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} assetDirectory
|
||||
* @returns {void}
|
||||
*/
|
||||
process(assetDirectory: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
render() {
|
||||
throw new Error("Symbol cannot be rendered directly");
|
||||
}
|
||||
}
|
||||
|
||||
class Paragraph extends Symbol {
|
||||
/**
|
||||
* @type {string[]}
|
||||
*/
|
||||
lines: string[] = [];
|
||||
/**
|
||||
* @type {Symbol[]}
|
||||
*/
|
||||
children: Symbol[] = [];
|
||||
/**
|
||||
*
|
||||
* @param {string} line
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static canParse(line: string): boolean {
|
||||
return line === "";
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {LineFeed} lineFeed
|
||||
* @returns {Symbol}
|
||||
*/
|
||||
static create(lineFeed: LineFeed): Symbol {
|
||||
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: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
render() {
|
||||
return `<p>${this.children.map((c) => c.render()).join("")}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
class UnorderedListItem extends Symbol {
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
text: string = "";
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
level: number = 0;
|
||||
/**
|
||||
*
|
||||
* @param {string} line
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static canParse(line: string): boolean {
|
||||
return line.trim().startsWith("- ");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {LineFeed} lineFeed
|
||||
* @returns {Symbol}
|
||||
*/
|
||||
static create(lineFeed: LineFeed): Symbol {
|
||||
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: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
render() {
|
||||
return `<li class="indent-${this.level}">${this.text} indentation level ${this.level}</li>`;
|
||||
}
|
||||
}
|
||||
|
||||
class OrderedListItem extends Symbol {
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
text: string = "";
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
level: number = 0;
|
||||
/**
|
||||
*
|
||||
* @param {string} line
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static canParse(line: string): boolean {
|
||||
return new RegExp(/^\d\. /).test(line.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {LineFeed} lineFeed
|
||||
* @returns {Symbol}
|
||||
*/
|
||||
static create(lineFeed: LineFeed): Symbol {
|
||||
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: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
render() {
|
||||
return `<li class="indent-${this.level}">${this.text} indentation level ${this.level}</li>`;
|
||||
}
|
||||
}
|
||||
class Link extends Symbol {
|
||||
/**
|
||||
* @type {RegExp}
|
||||
*/
|
||||
static canParseRegExp: RegExp = new RegExp(/^\[.*\]\(.*\)/);
|
||||
/**
|
||||
* @type {RegExp}
|
||||
*/
|
||||
static textAndLinkRegExp: RegExp = new RegExp(
|
||||
/\[(?<text>.*)\]\((?<link>.*)\)/
|
||||
);
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
text: string = "";
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
link: string = "";
|
||||
/**
|
||||
*
|
||||
* @param {string} line
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static canParse(line: string): boolean {
|
||||
return Link.canParseRegExp.test(line.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {LineFeed} lineFeed
|
||||
* @returns {Symbol}
|
||||
*/
|
||||
static create(lineFeed: LineFeed): Symbol {
|
||||
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: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
render() {
|
||||
return `<a href="${this.link}">${this.text}</a>`;
|
||||
}
|
||||
}
|
||||
|
||||
class ImageLink extends Symbol {
|
||||
/**
|
||||
* @type {RegExp}
|
||||
*/
|
||||
static canParseRegExp: RegExp = new RegExp(/^!\[.*\]\(.*\)/);
|
||||
/**
|
||||
* @type {RegExp}
|
||||
*/
|
||||
static textAndLinkRegExp: RegExp = new RegExp(
|
||||
/!\[(?<text>.*)\]\((?<link>.*)\)/
|
||||
);
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
static assetDirectoryToken: string = "@asset/";
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
alt: string = "";
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
link: string = "";
|
||||
/**
|
||||
*
|
||||
* @param {string} line
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static canParse(line: string): boolean {
|
||||
return ImageLink.canParseRegExp.test(line.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {LineFeed} lineFeed
|
||||
* @returns {Symbol}
|
||||
*/
|
||||
static create(lineFeed: LineFeed, assetDirectory: string): Symbol {
|
||||
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: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
render() {
|
||||
return `<img src="${this.link}" alt="${this.alt}"/>`;
|
||||
}
|
||||
}
|
||||
|
||||
class Italic extends Symbol {
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
text: string = "";
|
||||
/**
|
||||
*
|
||||
* @param {string} line
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static canParse(line: string): boolean {
|
||||
return line.startsWith("_");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {LineFeed} lineFeed
|
||||
* @returns {Symbol}
|
||||
*/
|
||||
static create(lineFeed: LineFeed): Symbol {
|
||||
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: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
render() {
|
||||
return `<i>${this.text}</i>`;
|
||||
}
|
||||
}
|
||||
|
||||
class Bold extends Symbol {
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
text: string = "";
|
||||
/**
|
||||
*
|
||||
* @param {string} line
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static canParse(line: string): boolean {
|
||||
return line.startsWith("**");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {LineFeed} lineFeed
|
||||
* @returns {Symbol}
|
||||
*/
|
||||
static create(lineFeed: LineFeed): Symbol {
|
||||
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: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
render() {
|
||||
return `<b>${this.text}</b>`;
|
||||
}
|
||||
}
|
||||
|
||||
class SingleLineCode extends Symbol {
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
text: string = "";
|
||||
/**
|
||||
*
|
||||
* @param {string} line
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static canParse(line: string): boolean {
|
||||
return line.startsWith("`");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {LineFeed} lineFeed
|
||||
* @returns {Symbol}
|
||||
*/
|
||||
static create(lineFeed: LineFeed): Symbol {
|
||||
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: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
render() {
|
||||
return `<code>${this.text}</code>`;
|
||||
}
|
||||
}
|
||||
|
||||
class MultiLineCode extends Symbol {
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
text: string = "";
|
||||
/**
|
||||
*
|
||||
* @param {string} line
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static canParse(line: string): boolean {
|
||||
const trimmedLine = line.trim();
|
||||
return trimmedLine.startsWith("```");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {LineFeed} lineFeed
|
||||
* @returns {Symbol}
|
||||
*/
|
||||
static create(lineFeed: LineFeed): Symbol {
|
||||
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: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
render() {
|
||||
return `<pre><code>${this.text}</code></pre>`;
|
||||
}
|
||||
}
|
||||
|
||||
class TextRow extends Symbol {
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
text: string = "";
|
||||
/**
|
||||
* @type {Symbol[]}
|
||||
*/
|
||||
children: Symbol[] = [];
|
||||
/**
|
||||
* @type {Symbol[]}
|
||||
*/
|
||||
allowedChildren: (typeof Symbol)[] = [
|
||||
Bold,
|
||||
Italic,
|
||||
ImageLink,
|
||||
Link,
|
||||
MultiLineCode,
|
||||
SingleLineCode,
|
||||
TextRow,
|
||||
];
|
||||
/**
|
||||
*
|
||||
* @param {string} line
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static canParse(line: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {LineFeed} lineFeed
|
||||
* @returns {Symbol}
|
||||
*/
|
||||
static create(lineFeed: LineFeed): Symbol {
|
||||
const instance = new TextRow();
|
||||
instance.text = lineFeed.claim();
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} assetDirectory
|
||||
* @returns {void}
|
||||
*/
|
||||
process(assetDirectory: string): void {
|
||||
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: string = "";
|
||||
/**
|
||||
* @type {Symbol[]}
|
||||
*/
|
||||
children: Symbol[] = [];
|
||||
/**
|
||||
*
|
||||
* @param {string} line
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static canParse(line: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {LineFeed} lineFeed
|
||||
* @returns {Symbol}
|
||||
*/
|
||||
static create(lineFeed: LineFeed): Symbol {
|
||||
const instance = new PlainText();
|
||||
instance.text = lineFeed.claim();
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} assetDirectory
|
||||
* @returns {void}
|
||||
*/
|
||||
process(assetDirectory: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
render() {
|
||||
return `<span>${this.text}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
const isRegExp = (value: unknown): value is RegExp => value instanceof RegExp;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string|RexExp} token
|
||||
* @param {string} string
|
||||
* @returns {string[]}
|
||||
*/
|
||||
const splitStringAtToken = (
|
||||
token: string | RegExp,
|
||||
string: string
|
||||
): string[] => {
|
||||
let index = 0;
|
||||
let length = 0;
|
||||
if (isRegExp(token)) {
|
||||
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|RegExp} token
|
||||
* @param {string} string
|
||||
* @returns {string[]}
|
||||
*/
|
||||
const extractTokenAndRest = (
|
||||
token: string | RegExp,
|
||||
string: string
|
||||
): string[] => {
|
||||
let index = 0;
|
||||
let length = 0;
|
||||
if (isRegExp(token)) {
|
||||
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, string: 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: (typeof Symbol)[] = [
|
||||
JustALineBreak,
|
||||
Heading,
|
||||
UnorderedListItem,
|
||||
OrderedListItem,
|
||||
Bold,
|
||||
Italic,
|
||||
ImageLink,
|
||||
Link,
|
||||
MultiLineCode,
|
||||
SingleLineCode,
|
||||
TextRow,
|
||||
];
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} markdown
|
||||
* @param {string} assetDirectory
|
||||
* @returns {string}
|
||||
*/
|
||||
export const toHtml = (markdown: string, assetDirectory: string): string => {
|
||||
// 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 `<div>${html}</div>`;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Symbol[]} symbols
|
||||
* @returns {Symbol[]}
|
||||
*/
|
||||
const stageThreeProcessing = (symbols: Symbol[]): Symbol[] => {
|
||||
const localSymbols = [...symbols];
|
||||
// Turn line break pairs into paragraphs
|
||||
while (localSymbols.filter((s) => s instanceof JustALineBreak).length !== 0) {
|
||||
const startIndex = localSymbols.findIndex(
|
||||
(s) => s instanceof JustALineBreak
|
||||
);
|
||||
const subEndIndex = localSymbols
|
||||
.slice(startIndex + 1)
|
||||
.findIndex((s) => s instanceof JustALineBreak);
|
||||
const endIndex =
|
||||
subEndIndex === -1
|
||||
? localSymbols.length - 1
|
||||
: startIndex + 1 + subEndIndex;
|
||||
const lastItemIsNotLineBreak = subEndIndex === -1;
|
||||
const children = localSymbols.slice(
|
||||
startIndex + 1,
|
||||
lastItemIsNotLineBreak ? localSymbols.length : endIndex
|
||||
);
|
||||
const paragraph = new Paragraph();
|
||||
paragraph.children = children;
|
||||
localSymbols.splice(startIndex, children.length + 1, paragraph);
|
||||
}
|
||||
// Fix indentation of bullet elements
|
||||
return localSymbols;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} markdown
|
||||
* @param {string} assetDirectory
|
||||
* @param {Symbol[]} allowedSymbols
|
||||
* @returns {Symbol[]}
|
||||
*/
|
||||
const toSymbols = (
|
||||
markdown: string,
|
||||
assetDirectory: string,
|
||||
allowedSymbols: (typeof Symbol)[] = AllSymbols
|
||||
): Symbol[] => {
|
||||
const lineFeed = new LineFeed(markdown);
|
||||
const symbols: Symbol[] = [];
|
||||
|
||||
while (!lineFeed.isEmpty()) {
|
||||
const line = lineFeed.peek();
|
||||
const factory = allowedSymbols.find((factory) => factory.canParse(line));
|
||||
if (factory === undefined) {
|
||||
const textRow = TextRow.create(lineFeed);
|
||||
symbols.push(textRow);
|
||||
continue;
|
||||
}
|
||||
const symbol = factory.create(lineFeed, assetDirectory);
|
||||
symbols.push(symbol);
|
||||
}
|
||||
|
||||
return symbols;
|
||||
};
|
||||
102
packages/@zblog/toolchain/src/zblog-toolchain.ts
Normal file
102
packages/@zblog/toolchain/src/zblog-toolchain.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import * as fs from "fs";
|
||||
import { toHtml } from "./markdown.js";
|
||||
|
||||
export type Paths = {
|
||||
outputDirectory: string;
|
||||
notes: {
|
||||
rootPath: string;
|
||||
assetsDirectoryName: string;
|
||||
noteFileName: string;
|
||||
};
|
||||
templates: {
|
||||
rootPath: string;
|
||||
noteTemplateName: string;
|
||||
indexTemplateName: string;
|
||||
notFoundName: string;
|
||||
};
|
||||
};
|
||||
|
||||
export class ToolchainBuilder {
|
||||
private paths: Paths;
|
||||
constructor(paths: Paths) {
|
||||
this.paths = paths;
|
||||
}
|
||||
|
||||
build() {
|
||||
if (fs.readdirSync(".").includes(this.paths.outputDirectory)) {
|
||||
fs.rmSync(this.paths.outputDirectory, { recursive: true, force: true });
|
||||
console.log("Starting rebuild");
|
||||
} else {
|
||||
console.log("Starting build");
|
||||
}
|
||||
fs.mkdirSync(this.paths.outputDirectory);
|
||||
console.log("Indexing content..");
|
||||
const directoryNames = fs.readdirSync(this.paths.notes.rootPath);
|
||||
const manifest = directoryNames.map((noteDirectory) => ({
|
||||
directoryName: noteDirectory,
|
||||
name: noteDirectory,
|
||||
markdown: fs.readFileSync(
|
||||
`${this.paths.notes.rootPath}/${noteDirectory}/${this.paths.notes.noteFileName}`,
|
||||
{ encoding: "utf-8" }
|
||||
),
|
||||
assetDirectoryPath: `${this.paths.notes.rootPath}/${noteDirectory}/${this.paths.notes.assetsDirectoryName}`,
|
||||
publicAssetDirectoryPath: `/${noteDirectory}_`,
|
||||
}));
|
||||
console.log("Parsing content..");
|
||||
manifest.forEach((m) => {
|
||||
const notePath = `${this.paths.templates.rootPath}/${this.paths.templates.noteTemplateName}`;
|
||||
let htmlTemplate = fs.readFileSync(notePath, {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
htmlTemplate = htmlTemplate.replace(
|
||||
"{{markdown}}",
|
||||
toHtml(m.markdown, m.publicAssetDirectoryPath)
|
||||
);
|
||||
fs.writeFileSync(
|
||||
`${this.paths.outputDirectory}/${m.directoryName}.html`,
|
||||
htmlTemplate,
|
||||
{
|
||||
encoding: "utf-8",
|
||||
flag: "ax",
|
||||
}
|
||||
);
|
||||
if (!fs.existsSync(m.assetDirectoryPath)) return;
|
||||
const assetsList = fs.readdirSync(m.assetDirectoryPath);
|
||||
assetsList.forEach((assetName) =>
|
||||
fs.cpSync(
|
||||
`${m.assetDirectoryPath}/${assetName}`,
|
||||
`${this.paths.outputDirectory}/${m.name}_${assetName}`
|
||||
)
|
||||
);
|
||||
});
|
||||
console.log("Moving static templates..");
|
||||
[this.paths.templates.notFoundName].forEach((filename) => {
|
||||
fs.copyFileSync(
|
||||
`${this.paths.templates.rootPath}/${filename}`,
|
||||
`${this.paths.outputDirectory}/${filename}`
|
||||
);
|
||||
});
|
||||
console.log("Building startpage..");
|
||||
let htmlTemplate = fs.readFileSync(
|
||||
`${this.paths.templates.rootPath}/${this.paths.templates.indexTemplateName}`,
|
||||
{
|
||||
encoding: "utf-8",
|
||||
}
|
||||
);
|
||||
const links = manifest
|
||||
.map((m) => `<a href='/${m.directoryName}.html'>${m.name}</a>`)
|
||||
.reverse();
|
||||
const unorderedListItems = links.map((l) => `<li>${l}</li>`).join("\r\n");
|
||||
const html = `
|
||||
<ul>
|
||||
${unorderedListItems}
|
||||
<ul>
|
||||
`;
|
||||
htmlTemplate = htmlTemplate.replace("{{content}}", html);
|
||||
fs.writeFileSync(`${this.paths.outputDirectory}/index.html`, htmlTemplate, {
|
||||
encoding: "utf-8",
|
||||
flag: "ax",
|
||||
});
|
||||
console.log("Done");
|
||||
}
|
||||
}
|
||||
9
packages/@zblog/toolchain/tsconfig.json
Normal file
9
packages/@zblog/toolchain/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"include": ["src/zblog-toolchain.ts"],
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "bin",
|
||||
"declaration": true,
|
||||
"declarationMap": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user