# shared/matchers/vocabulary.matcher.ts

> 115 lines of code and 27 definitions.

Tree: Governance tree
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/governance#file-governance-shared-matchers-vocabulary-matcher-ts
Source text: https://banes-lab.com/source/governance/shared/matchers/vocabulary.matcher.ts.txt

Listed in [shared/matchers](https://banes-lab.com/api/source/governance/shared/matchers.md), after [shared/matchers/segment.matcher.ts](https://banes-lab.com/source/governance/shared/matchers/segment.matcher.ts.md) and before [shared/matchers/word.matcher.ts](https://banes-lab.com/source/governance/shared/matchers/word.matcher.ts.md).

## Definitions

- `isBoundary` (lexical_declaration, line 22)
- `wordAt` (lexical_declaration, line 44)
- `occursAsWord` (lexical_declaration, line 26)
- `firstTermIn` (lexical_declaration, line 39, exported)
- `replaceKnown` (lexical_declaration, line 106, exported)
- `casedLike` (lexical_declaration, line 75)
- `firstKnownIn` (lexical_declaration, line 90, exported)
- `knownAt` (lexical_declaration, line 81)
- `numberedPhraseIn` (lexical_declaration, line 60, exported)
- `TERM_STOPS` (lexical_declaration, line 1)
- `from` (lexical_declaration, line 27)
- `after` (lexical_declaration, line 30)
- `before` (lexical_declaration, line 45)
- `DIGITS` (lexical_declaration, line 49)
- `SPACE` (lexical_declaration, line 50)
- `digitsFrom` (lexical_declaration, line 52)
- `noun` (lexical_declaration, line 63, exported)
- `start` (lexical_declaration, line 64, exported)
- `end` (lexical_declaration, line 66, exported)
- `first` (lexical_declaration, line 76)
- `capital` (lexical_declaration, line 77)
- `longest` (lexical_declaration, line 96, exported)
- `lower` (lexical_declaration, line 111, exported)
- `terms` (lexical_declaration, line 112, exported)
- `output` (lexical_declaration, line 113, exported)
- `at` (lexical_declaration, line 114, exported)
- `term` (lexical_declaration, line 116, exported)

## Used by

- [rules/eslint/strings-vocabulary.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/strings-vocabulary.eslint.rule.ts.md)
- [shared/analyzers/document.analyzer.ts](https://banes-lab.com/source/governance/shared/analyzers/document.analyzer.ts.md)

## Source

```typescript
const TERM_STOPS: ReadonlySet<string> = new Set([
    " ",
    "\n",
    "\t",
    ",",
    ".",
    ";",
    ":",
    "!",
    "?",
    "(",
    ")",
    '"',
    "'",
    "—",
    "-",
    "*",
    "`",
    "/",
]);

const isBoundary = function isBoundary(char: string): boolean {
    return char.length === 0 || TERM_STOPS.has(char);
};

const occursAsWord = function occursAsWord(lower: string, term: string): boolean {
    let from = lower.indexOf(term);
    while (from !== -1) {
        const before = from === 0 ? "" : lower.charAt(from - 1);
        const after = lower.charAt(from + term.length);
        if (isBoundary(before) && isBoundary(after)) {
            return true;
        }
        from = lower.indexOf(term, from + term.length);
    }
    return false;
};

export const firstTermIn = function firstTermIn(text: string, terms: readonly string[]): string | null {
    const lower = text.toLowerCase();
    return terms.find((term) => occursAsWord(lower, term)) ?? null;
};

const wordAt = function wordAt(lower: string, at: number, term: string): boolean {
    const before = at === 0 ? "" : lower.charAt(at - 1);
    return lower.startsWith(term, at) && isBoundary(before) && isBoundary(lower.charAt(at + term.length));
};

const DIGITS = "0123456789";
const SPACE = " ";

const digitsFrom = function digitsFrom(text: string, at: number): number {
    let end = at;
    while (end < text.length && DIGITS.includes(text.charAt(end))) {
        end += 1;
    }
    return end;
};

export const numberedPhraseIn = function numberedPhraseIn(text: string, nouns: readonly string[]): string | null {
    const lower = text.toLowerCase();
    for (let at = 0; at < lower.length; at += 1) {
        const noun = nouns.find((candidate) => wordAt(lower, at, candidate));
        const start = noun === undefined ? -1 : at + noun.length + SPACE.length;
        if (noun !== undefined && lower.charAt(at + noun.length) === SPACE) {
            const end = digitsFrom(lower, start);
            if (end > start && isBoundary(lower.charAt(end))) {
                return text.slice(at, end);
            }
        }
    }
    return null;
};

const casedLike = function casedLike(original: string, replacement: string): string {
    const first = original.charAt(0);
    const capital = first !== first.toLowerCase();
    return capital ? replacement.charAt(0).toUpperCase() + replacement.slice(1) : replacement;
};

const knownAt = function knownAt(
    lower: string,
    at: number,
    terms: readonly string[],
    names: readonly string[],
): string | undefined {
    return names.some((name) => wordAt(lower, at, name)) ? undefined : terms.find((term) => wordAt(lower, at, term));
};

export const firstKnownIn = function firstKnownIn(
    text: string,
    terms: readonly string[],
    names: readonly string[] = [],
): string | null {
    const lower = text.toLowerCase();
    const longest = terms.toSorted((left, right) => right.length - left.length);
    for (let at = 0; at < lower.length; at += 1) {
        const term = knownAt(lower, at, longest, names);
        if (term !== undefined) {
            return term;
        }
    }
    return null;
};

export const replaceKnown = function replaceKnown(
    text: string,
    known: ReadonlyMap<string, string>,
    names: readonly string[] = [],
): string {
    const lower = text.toLowerCase();
    const terms = [...known.keys()].toSorted((left, right) => right.length - left.length);
    let output = "";
    let at = 0;
    while (at < text.length) {
        const term = knownAt(lower, at, terms, names);
        if (term === undefined) {
            output += text.charAt(at);
            at += 1;
            continue;
        }
        output += casedLike(text.slice(at, at + term.length), known.get(term) ?? term);
        at += term.length;
    }
    return output;
};
```
