# core/parsers/metadata.parser.ts

> 89 lines of code and 22 definitions.

Tree: GovLab Docs
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/docs#file-docs-core-parsers-metadata-parser-ts
Source text: https://banes-lab.com/source/docs/core/parsers/metadata.parser.ts.txt

Listed in [core/parsers](https://banes-lab.com/api/source/docs/core/parsers.md), after [core/parsers/markdown.parser.ts](https://banes-lab.com/source/docs/core/parsers/markdown.parser.ts.md) and before [core/parsers/ontology.parser.ts](https://banes-lab.com/source/docs/core/parsers/ontology.parser.ts.md).

## Definitions

- `fileTokens` (lexical_declaration, line 95, exported)
- `listValues` (lexical_declaration, line 43, exported)
- `splitOn` (lexical_declaration, line 79, exported)
- `LIST_OPEN` (lexical_declaration, line 5)
- `LIST_CLOSE` (lexical_declaration, line 6)
- `LIST_SEPARATOR` (lexical_declaration, line 7)
- `KEY_SEPARATOR` (lexical_declaration, line 8)
- `TOKEN_SEPARATORS` (lexical_declaration, line 9)
- `ABSENT` (lexical_declaration, line 26)
- `parseFields` (lexical_declaration, line 28)
- `fields` (lexical_declaration, line 29)
- `value` (lexical_declaration, line 44, exported)
- `inner` (lexical_declaration, line 48, exported)
- `parseFrontmatter` (lexical_declaration, line 58, exported)
- `{ fields, end }` (lexical_declaration, line 63, exported)
- `fieldLine` (lexical_declaration, line 67, exported)
- `lines` (lexical_declaration, line 68, exported)
- `at` (lexical_declaration, line 69, exported)
- `line` (lexical_declaration, line 70, exported)
- `colon` (lexical_declaration, line 71, exported)
- `tokens` (lexical_declaration, line 80, exported)
- `current` (lexical_declaration, line 81, exported)

## Contained in

- [core/parsers](https://banes-lab.com/anatomy/docs/folder-docs-core-parsers.md)

## Used by

- [core/analyzers/reference.analyzer.ts](https://banes-lab.com/source/docs/core/analyzers/reference.analyzer.ts.md)
- [core/converters/document.converter.ts](https://banes-lab.com/source/docs/core/converters/document.converter.ts.md)
- [core/validators/reference.validator.ts](https://banes-lab.com/source/docs/core/validators/reference.validator.ts.md)

## Linked from

- [core/analyzers](https://banes-lab.com/anatomy/docs/folder-docs-core-analyzers.md)
- [core/converters](https://banes-lab.com/anatomy/docs/folder-docs-core-converters.md)
- [core/parsers](https://banes-lab.com/anatomy/docs/folder-docs-core-parsers.md)
- [core/validators](https://banes-lab.com/anatomy/docs/folder-docs-core-validators.md)

## Source

```typescript
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));
};
```
