core/converters/markdown.converter.ts
core/converters/markdown.converter.ts is a file in Bane's Lab Site. 126 lines of code and 26 definitions.
import { FENCE, QUOTE_MARK, TABLE_PIPE } from "#configuration/constants/markdown.constants";
import {
bodyLines,
bulletText,
cellsOf,
headingLevel,
isRule,
isTableRule,
orderedText,
} from "#core/matchers/markdown.matcher";
import { LINE_BREAK } from "#configuration/constants/code.constants";
import type { MarkdownNode } from "#types/markdown.types";
import { SPACE } from "#configuration/constants/document.constants";
class Parser {
private readonly lines: readonly string[];
private readonly nodes: MarkdownNode[] = [];
private paragraph: string[] = [];
private at = 0;
public constructor(text: string) {
this.lines = bodyLines(text);
}
public parse(): MarkdownNode[] {
while (this.at < this.lines.length) {
this.step(this.lines[this.at] ?? "");
}
this.flush();
return this.nodes;
}
private line(offset = 0): string {
return this.lines[this.at + offset] ?? "";
}
private step(line: string): void {
const trimmed = line.trim();
if (trimmed.length === 0) {
this.flush();
this.at += 1;
return;
}
if (trimmed.startsWith(FENCE)) {
this.fence(trimmed.slice(FENCE.length).trim());
return;
}
if (trimmed.startsWith(QUOTE_MARK)) {
this.quote();
return;
}
if (bulletText(line) !== null || orderedText(line) !== null) {
this.list(orderedText(line) !== null);
return;
}
if (trimmed.includes(TABLE_PIPE) && isTableRule(this.line(1))) {
this.table();
return;
}
this.simple(trimmed);
}
private simple(trimmed: string): void {
const level = headingLevel(trimmed);
if (level > 0) {
this.push({ kind: "heading", level, text: trimmed.slice(level + 1).trim() });
} else if (isRule(trimmed)) {
this.push({ kind: "rule" });
} else {
this.paragraph.push(trimmed);
}
this.at += 1;
}
private push(node: MarkdownNode): void {
this.flush();
this.nodes.push(node);
}
private flush(): void {
if (this.paragraph.length > 0) {
this.nodes.push({ kind: "paragraph", text: this.paragraph.join(SPACE) });
this.paragraph = [];
}
}
private fence(language: string): void {
const code: string[] = [];
this.at += 1;
while (this.at < this.lines.length && this.line().trim() !== FENCE) {
code.push(this.line());
this.at += 1;
}
this.at += 1;
this.push({ code: code.join(LINE_BREAK), kind: "code", language });
}
private quote(): void {
const held: string[] = [];
while (this.at < this.lines.length && this.line().trim().startsWith(QUOTE_MARK)) {
held.push(this.line().trim().slice(1).trim());
this.at += 1;
}
this.push({ kind: "quote", text: held.join(SPACE) });
}
private list(ordered: boolean): void {
const items: string[] = [];
while (this.at < this.lines.length) {
const line = this.line();
const item = ordered ? orderedText(line) : bulletText(line);
const continued = item === null && line.startsWith(SPACE) && line.trim().length > 0 && items.length > 0;
if (item !== null) {
items.push(item);
} else if (continued) {
items[items.length - 1] = `${items.at(-1) ?? ""}${SPACE}${line.trim()}`;
} else {
break;
}
this.at += 1;
}
this.push({ items, kind: "list", ordered });
}
private table(): void {
const headers = cellsOf(this.line());
const rows: string[][] = [];
this.at += 2;
while (this.at < this.lines.length && this.line().includes(TABLE_PIPE)) {
rows.push(cellsOf(this.line()));
this.at += 1;
}
this.push({ headers, kind: "table", rows });
}
}
export const parseMarkdown = function parseMarkdown(text: string): MarkdownNode[] {
return new Parser(text).parse();
};