# core/converters/text.converter.ts

> 182 lines of code and 57 definitions.

Tree: Site tree
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/tree#file-core-converters-text-converter-ts
Source text: https://banes-lab.com/assets/sources/source.61795c368343bcd539ed4bb6429927f702c5ff84bfe9d48d802a900d84c17e74.generated.txt

## Definitions

- `childrenJoined` (lexical_declaration, line 109)
- `nounFor` (lexical_declaration, line 205, exported)
- `nodeText` (lexical_declaration, line 164)
- `convertText` (lexical_declaration, line 188, exported)
- `formatter` (lexical_declaration, line 178)
- `partsLabel` (lexical_declaration, line 59)
- `childrenText` (lexical_declaration, line 119)
- `exportText` (lexical_declaration, line 201, exported)
- `isComposite` (lexical_declaration, line 55)
- `fenceText` (lexical_declaration, line 73)
- `wrapped` (lexical_declaration, line 86)
- `codeText` (lexical_declaration, line 90)
- `headingText` (lexical_declaration, line 68)
- `bulletText` (lexical_declaration, line 115)
- `blockText` (lexical_declaration, line 133)
- `ICON_CLASS` (lexical_declaration, line 6)
- `PARAGRAPH` (lexical_declaration, line 7)
- `FENCE` (lexical_declaration, line 8)
- `FENCE_OPEN` (lexical_declaration, line 9)
- `BULLET` (lexical_declaration, line 10)
- `CELL` (lexical_declaration, line 11)
- `RULE` (lexical_declaration, line 12)
- `PIPE` (lexical_declaration, line 13)
- `HEADING_MARK` (lexical_declaration, line 14)
- `BOLD` (lexical_declaration, line 15)
- `TERM_SEPARATOR` (lexical_declaration, line 16)
- `CODE_SELECTOR` (lexical_declaration, line 17)
- `CODE_TITLE_SELECTOR` (lexical_declaration, line 18)
- `BLOCK_TAGS` (lexical_declaration, line 19)
- `Formatter` (type_alias_declaration, line 36)
- `cellsOf` (lexical_declaration, line 38)
- `tableText` (lexical_declaration, line 42)
- `text` (lexical_declaration, line 43)
- `rows` (lexical_declaration, line 44)
- `cells` (lexical_declaration, line 46)
- `level` (lexical_declaration, line 69)
- `code` (lexical_declaration, line 74)
- `block` (lexical_declaration, line 75)
- `language` (lexical_declaration, line 76)
- `title` (lexical_declaration, line 77)
- `caption` (lexical_declaration, line 78)
- `noText` (lexical_declaration, line 82)
- `EMPHASIS_MARK` (lexical_declaration, line 94)
- `emphasisText` (lexical_declaration, line 96)
- `inner` (lexical_declaration, line 100)
- `trimmed` (lexical_declaration, line 101)
- `start` (lexical_declaration, line 105)
- `breakText` (lexical_declaration, line 123)
- `termText` (lexical_declaration, line 127)
- `parts` (lexical_declaration, line 128)
- `linkText` (lexical_declaration, line 137)
- `href` (lexical_declaration, line 138)
- `label` (lexical_declaration, line 139)
- `FORMATTERS` (lexical_declaration, line 143)
- `settled` (lexical_declaration, line 171)
- `shown` (lexical_declaration, line 174)
- `clone` (lexical_declaration, line 189, exported)

## Uses

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

## Used by

- [domain/converters/anatomy.converter.ts](https://banes-lab.com/source/tree/domain/converters/anatomy.converter.ts.md)
- [domain/converters/graph.converter.ts](https://banes-lab.com/source/tree/domain/converters/graph.converter.ts.md)
- [domain/loaders/anatomy.loader.ts](https://banes-lab.com/source/tree/domain/loaders/anatomy.loader.ts.md)
- [presentation/renderers/page.renderer.ts](https://banes-lab.com/source/tree/presentation/renderers/page.renderer.ts.md)
- [presentation/renderers/section.renderer.ts](https://banes-lab.com/source/tree/presentation/renderers/section.renderer.ts.md)

## Source

````typescript
import { EXPORT_IGNORED_SELECTORS, SETTLED_TEXT_ATTRIBUTE, SPACE } from "#configuration/constants/document.constants";
import { LANGUAGE_ATTRIBUTE, LINE_BREAK } from "#configuration/constants/code.constants";
import { collapseBreaks, collapseSpaces, joinParts } from "#core/normalizers/text.normalizer";
import { HREF_ATTRIBUTE } from "#configuration/constants/element.constants";

const ICON_CLASS = "bi";
const PARAGRAPH = "\n\n";
const FENCE = "\n```\n";
const FENCE_OPEN = "\n```";
const BULLET = "\n- ";
const CELL = " | ";
const RULE = "---";
const PIPE = "|";
const HEADING_MARK = "#";
const BOLD = "**";
const TERM_SEPARATOR = " · ";
const CODE_SELECTOR = "code";
const CODE_TITLE_SELECTOR = ".code-title";
const BLOCK_TAGS = new Set([
    "DIV",
    "SECTION",
    "ARTICLE",
    "HEADER",
    "FOOTER",
    "NAV",
    "ASIDE",
    "UL",
    "OL",
    "DL",
    "FIGURE",
    "DETAILS",
    "SUMMARY",
    "BLOCKQUOTE",
]);

type Formatter = (element: Element) => string;

const cellsOf = function cellsOf(row: Element): readonly string[] {
    return [...row.querySelectorAll("th, td")].map((cell) => cell.textContent.trim());
};

const tableText = function tableText(table: Element): string {
    let text = "";
    const rows = [...table.querySelectorAll("tr")];
    for (const [index, row] of rows.entries()) {
        const cells = cellsOf(row);
        text += LINE_BREAK + PIPE + SPACE + cells.join(CELL) + SPACE + PIPE;
        if (index === 0) {
            text += LINE_BREAK + PIPE + cells.map(() => RULE).join(PIPE) + PIPE;
        }
    }
    return text + LINE_BREAK;
};

const isComposite = function isComposite(element: Element): boolean {
    return element.children.length > 1 && [...element.childNodes].every((node) => node instanceof Element);
};

const partsLabel = function partsLabel(element: Element): string {
    return isComposite(element)
        ? [...element.children]
              .map((child) => collapseSpaces(child.textContent).trim())
              .filter((part) => part.length > 0)
              .join(TERM_SEPARATOR)
        : collapseSpaces(element.textContent).trim();
};

const headingText = function headingText(element: Element): string {
    const level = Number(element.tagName.slice(1));
    return PARAGRAPH + HEADING_MARK.repeat(level) + SPACE + partsLabel(element) + LINE_BREAK;
};

const fenceText = function fenceText(element: Element): string {
    const code = element.querySelector(CODE_SELECTOR) ?? element;
    const block = element.closest("pre");
    const language = block?.getAttribute(LANGUAGE_ATTRIBUTE) ?? "";
    const title = block?.querySelector(CODE_TITLE_SELECTOR);
    const caption = title === null || title === undefined ? "" : PARAGRAPH + childrenText(title) + LINE_BREAK;
    return caption + FENCE_OPEN + language + LINE_BREAK + code.textContent + FENCE;
};

const noText = function noText(): string {
    return "";
};

const wrapped = function wrapped(mark: string): Formatter {
    return (element) => mark + element.textContent + mark;
};

const codeText = function codeText(element: Element): string {
    return element.closest("pre") === null ? wrapped("`")(element) : fenceText(element);
};

const EMPHASIS_MARK = "*";

const emphasisText = function emphasisText(element: Element): string {
    if (element.classList.contains(ICON_CLASS)) {
        return "";
    }
    const inner = childrenJoined(element);
    const trimmed = inner.trim();
    if (trimmed.length === 0) {
        return inner;
    }
    const start = inner.indexOf(trimmed);
    return inner.slice(0, start) + EMPHASIS_MARK + trimmed + EMPHASIS_MARK + inner.slice(start + trimmed.length);
};

const childrenJoined = function childrenJoined(element: Element): string {
    return joinParts(
        [...element.childNodes].map((child) => ({ element: child instanceof Element, text: nodeText(child) })),
    );
};

const bulletText = function bulletText(element: Element): string {
    return BULLET + childrenJoined(element).trim();
};

const childrenText = function childrenText(element: Element): string {
    return childrenJoined(element).trim();
};

const breakText = function breakText(): string {
    return LINE_BREAK;
};

const termText = function termText(element: Element): string {
    const parts = [...element.children].map(partsLabel).filter((part) => part.length > 0);
    const label = parts.length === 0 ? childrenText(element) : parts.join(TERM_SEPARATOR);
    return PARAGRAPH + BOLD + label + BOLD;
};

const blockText = function blockText(element: Element): string {
    return PARAGRAPH + childrenText(element);
};

const linkText = function linkText(element: Element): string {
    const href = element.getAttribute(HREF_ATTRIBUTE) ?? "";
    const label = childrenText(element);
    return href.length === 0 || label.length === 0 ? label : `[${label}](${href})`;
};

const FORMATTERS = new Map<string, Formatter>([
    ["H1", headingText],
    ["H2", headingText],
    ["H3", headingText],
    ["H4", headingText],
    ["PRE", fenceText],
    ["CODE", codeText],
    ["LI", bulletText],
    ["DT", termText],
    ["DD", blockText],
    ["FIGCAPTION", blockText],
    ["A", linkText],
    ["STRONG", wrapped(BOLD)],
    ["B", wrapped(BOLD)],
    ["EM", emphasisText],
    ["I", emphasisText],
    ["TABLE", tableText],
    ["BR", breakText],
    ["svg", noText],
]);

const nodeText = function nodeText(node: Node): string {
    if (node.nodeType === Node.TEXT_NODE) {
        return collapseSpaces(node.textContent ?? "");
    }
    if (!(node instanceof Element)) {
        return "";
    }
    const settled = node.getAttribute(SETTLED_TEXT_ATTRIBUTE);
    if (settled !== null) {
        const formatter = FORMATTERS.get(node.tagName);
        const shown = node.cloneNode(false);
        shown.textContent = settled;
        return formatter === undefined || !(shown instanceof Element) ? settled : formatter(shown);
    }
    const formatter = FORMATTERS.get(node.tagName);
    if (formatter !== undefined) {
        return formatter(node);
    }
    if (node.tagName === "P") {
        return PARAGRAPH + childrenJoined(node);
    }
    return BLOCK_TAGS.has(node.tagName) ? LINE_BREAK + childrenJoined(node) + LINE_BREAK : childrenJoined(node);
};

export const convertText = function convertText(element: Element, ignored: readonly string[]): string {
    const clone = element.cloneNode(true);
    if (!(clone instanceof Element)) {
        return "";
    }
    for (const selector of ignored) {
        for (const removed of clone.querySelectorAll(selector)) {
            removed.remove();
        }
    }
    return collapseBreaks(nodeText(clone));
};

export const exportText = function exportText(element: Element): string {
    return convertText(element, EXPORT_IGNORED_SELECTORS);
};

export const nounFor = function nounFor(count: number, one: string, many: string): string {
    return count === 1 ? one : many;
};
````
