core/parsers/metadata.parser.ts
core/parsers/metadata.parser.ts is a file in GovLab Docs. 89 lines of code and 22 definitions.
import type { Frontmatter, FrontmatterFields } from "#types/metadata.types";
import { FRONTMATTER_FENCE } from "#configuration/constants/document.constants";
import { splitLines } from "#core/parsers/markdown.parser";
const LIST_OPEN = "[";
const LIST_CLOSE = "]";
const LIST_SEPARATOR = ",";
const KEY_SEPARATOR = ":";
const TOKEN_SEPARATORS: ReadonlySet<string> = new Set([
"[",
"]",
",",
":",
" ",
"\t",
"\r",
'"',
"'",
"(",
")",
"`",
"<",
">",
]);
const ABSENT: Frontmatter = { bodyStart: 0, fields: {}, present: false };
const parseFields = function parseFields(lines: readonly string[]): FrontmatterFields {
const fields: Record<string, string> = {};
for (let at = 1; at < lines.length; at += 1) {
const line = lines[at] ?? "";
if (line === FRONTMATTER_FENCE) {
return { end: at, fields };
}
const colon = line.indexOf(KEY_SEPARATOR);
if (colon > 0) {
fields[line.slice(0, colon).trim()] = line.slice(colon + 1).trim();
}
}
return { end: -1, fields };
};
export const listValues = function listValues(raw: string): string[] | null {
const value = raw.trim();
if (!value.startsWith(LIST_OPEN) || !value.endsWith(LIST_CLOSE)) {
return null;
}
const inner = value.slice(1, -1).trim();
if (inner.length === 0) {
return [];
}
return inner
.split(LIST_SEPARATOR)
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0);
};
export const parseFrontmatter = function parseFrontmatter(source: string): Frontmatter {
const lines = splitLines(source);
if ((lines[0] ?? "") !== FRONTMATTER_FENCE) {
return ABSENT;
}
const { fields, end } = parseFields(lines);
return end === -1 ? ABSENT : { bodyStart: end + 1, fields, present: true };
};
export const fieldLine = function fieldLine(source: string, field: string): number {
const lines = splitLines(source);
for (let at = 0; at < lines.length; at += 1) {
const line = lines[at] ?? "";
const colon = line.indexOf(KEY_SEPARATOR);
if (colon > 0 && line.slice(0, colon).trim() === field) {
return at + 1;
}
}
return 1;
};
export const splitOn = function splitOn(text: string, isSeparator: (char: string) => boolean): string[] {
const tokens: string[] = [];
let current = "";
for (const char of text) {
if (!isSeparator(char)) {
current += char;
continue;
}
if (current.length > 0) {
tokens.push(current);
current = "";
}
}
return current.length > 0 ? [...tokens, current] : tokens;
};
export const fileTokens = function fileTokens(line: string): string[] {
return splitOn(line, (char) => TOKEN_SEPARATORS.has(char));
};