# core/normalizers/text.normalizer.ts

> 65 lines of code and 20 definitions.

Tree: Site tree
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/tree#file-core-normalizers-text-normalizer-ts
Source text: https://banes-lab.com/assets/sources/source.2e63e9c7c1bd2eb585d821f2622596914181174fa404339fdd15ebdbb393e853.generated.txt

## Definitions

- `collapseSpaces` (lexical_declaration, line 47, exported)
- `isWordChar` (lexical_declaration, line 15)
- `isSpaced` (lexical_declaration, line 20)
- `needsSpace` (lexical_declaration, line 24)
- `joinParts` (lexical_declaration, line 31, exported)
- `isLetter` (lexical_declaration, line 10)
- `collapseBreaks` (lexical_declaration, line 61, exported)
- `WHITESPACE` (lexical_declaration, line 6)
- `MAX_BREAKS` (lexical_declaration, line 7)
- `ASCII_LIMIT` (lexical_declaration, line 8)
- `lower` (lexical_declaration, line 11)
- `code` (lexical_declaration, line 16)
- `pieces` (lexical_declaration, line 32, exported)
- `last` (lexical_declaration, line 33, exported)
- `previous` (lexical_declaration, line 34, exported)
- `next` (lexical_declaration, line 36, exported)
- `result` (lexical_declaration, line 48, exported)
- `pending` (lexical_declaration, line 49, exported)
- `characters` (lexical_declaration, line 62, exported)
- `run` (lexical_declaration, line 63, exported)

## Used by

- [core/converters/text.converter.ts](https://banes-lab.com/source/tree/core/converters/text.converter.ts.md)

## Source

```typescript
import { LINE_BREAK } from "#configuration/constants/code.constants";
import { SPACE } from "#configuration/constants/document.constants";

import type { TextPart } from "#types/markup.types";

const WHITESPACE = new Set([SPACE, LINE_BREAK, "\t", "\r"]);
const MAX_BREAKS = 2;
const ASCII_LIMIT = 128;

const isLetter = function isLetter(char: string): boolean {
    const lower = char.toLowerCase();
    return lower >= "a" && lower <= "z";
};

const isWordChar = function isWordChar(char: string): boolean {
    const code = char.codePointAt(0) ?? 0;
    return isLetter(char) || (char >= "0" && char <= "9") || code >= ASCII_LIMIT;
};

const isSpaced = function isSpaced(char: string): boolean {
    return WHITESPACE.has(char);
};

const needsSpace = function needsSpace(last: string, next: string, elements: boolean): boolean {
    if (isSpaced(last) || isSpaced(next)) {
        return false;
    }
    return elements || (isWordChar(last) && isWordChar(next));
};

export const joinParts = function joinParts(parts: readonly TextPart[]): string {
    const pieces: string[] = [];
    let last = SPACE;
    let previous = false;
    for (const part of parts.filter((entry) => entry.text.length > 0)) {
        const next = part.text.at(0) ?? SPACE;
        if (needsSpace(last, next, previous && part.element)) {
            pieces.push(SPACE);
        }
        pieces.push(part.text);
        last = part.text.at(-1) ?? last;
        previous = part.element;
    }
    return pieces.join("");
};

export const collapseSpaces = function collapseSpaces(text: string): string {
    let result = "";
    let pending = false;
    for (const character of text) {
        if (WHITESPACE.has(character)) {
            pending = true;
            continue;
        }
        result += (pending ? SPACE : "") + character;
        pending = false;
    }
    return pending ? result + SPACE : result;
};

export const collapseBreaks = function collapseBreaks(text: string): string {
    const characters: string[] = [];
    let run = 0;
    for (const character of text) {
        run = character === LINE_BREAK ? run + 1 : 0;
        if (character === LINE_BREAK && characters.at(-1) === SPACE) {
            characters.pop();
        }
        if (run <= MAX_BREAKS && !(character === SPACE && characters.at(-1) === LINE_BREAK)) {
            characters.push(character);
        }
    }
    return characters.join("").trim();
};
```
