core/analyzers/search.analyzer.ts

core/analyzers/search.analyzer.ts is a file in Bane's Lab Site. 113 lines of code and 20 definitions.

import type { Block, GlossaryBlock } from "#types/block.types";
import { pagePath, tabLink } from "#core/assets/link.assets";
import { ANCHOR_PREFIX } from "#configuration/constants/document.constants";
import type { Evidence } from "#types/evidence.types";
import { SEARCH_IGNORED_KEYS } from "#configuration/constants/search.constants";
import type { SearchSource } from "#types/search.types";
import type { Section } from "#types/document.types";
import { convertMarkup } from "#core/converters/markup.converter";

export const UNPLACED = Number.POSITIVE_INFINITY;

export const FOLLOWS = 0.5;

export const textsOf = function textsOf(value: unknown): readonly string[] {
    if (typeof value === "string") {
        return [value];
    }
    if (Array.isArray(value)) {
        return value.flatMap((item: unknown) => textsOf(item));
    }
    if (typeof value === "object" && value !== null) {
        return Object.entries(value).flatMap(([key, inner]: readonly [string, unknown]) =>
            SEARCH_IGNORED_KEYS.has(key) ? [] : textsOf(inner),
        );
    }
    return [];
};

const pageAnchorPath = function pageAnchorPath(page: string, section: string): string {
    return pagePath(page) + ANCHOR_PREFIX + section;
};

export const anchorPath = function anchorPath(source: SearchSource, section: string): string {
    return source.first || source.layout === "document"
        ? pageAnchorPath(source.page, section)
        : tabLink(source.page, source.tab.id, section);
};

const chapterPath = function chapterPath(
    subject: { readonly page: string; readonly section: string; readonly tab: string },
    firstTabs: ReadonlyMap<string, string>,
): string {
    return firstTabs.get(subject.page) === subject.tab
        ? pageAnchorPath(subject.page, subject.section)
        : tabLink(subject.page, subject.tab, subject.section);
};

const earliest = function earliest(positions: Map<string, number>, key: string, at: number): void {
    positions.set(key, Math.min(positions.get(key) ?? UNPLACED, at));
};

export const routeOf = function routeOf(route: readonly string[]): ReadonlyMap<string, number> {
    const positions = new Map<string, number>();
    for (const [at, path] of route.entries()) {
        earliest(positions, path, at);
    }
    return positions;
};

const anchorOf = function anchorOf(href: string): string | null {
    const cut = href.indexOf(ANCHOR_PREFIX);
    return cut === -1 ? null : href.slice(cut + ANCHOR_PREFIX.length);
};

export const anchorsLinkedFrom = function anchorsLinkedFrom(section: Section): readonly string[] {
    return textsOf(section)
        .flatMap((text) => convertMarkup(text))
        .flatMap((run) => {
            const anchor = run.href === undefined ? null : anchorOf(run.href);
            return anchor === null ? [] : [anchor];
        });
};

export const linksOf = function linksOf(
    sources: readonly SearchSource[],
    route: ReadonlyMap<string, number>,
): ReadonlyMap<string, number> {
    const positions = new Map<string, number>();
    for (const source of sources) {
        for (const section of source.tab.sections) {
            const at = route.get(anchorPath(source, section.id));
            if (at === undefined) {
                continue;
            }
            for (const anchor of anchorsLinkedFrom(section)) {
                earliest(positions, anchor, at + FOLLOWS);
            }
        }
    }
    return positions;
};

export const evidenceOf = function evidenceOf(
    evidence: readonly Evidence[],
    route: ReadonlyMap<string, number>,
    firstTabs: ReadonlyMap<string, string>,
): ReadonlyMap<string, number> {
    const positions = new Map<string, number>();
    for (const { nodes, subject } of evidence) {
        const at = subject.kind === "chapter" ? route.get(chapterPath(subject, firstTabs)) : undefined;
        if (at === undefined) {
            continue;
        }
        for (const node of nodes) {
            if (node.kind === "definition") {
                earliest(positions, node.name, at + FOLLOWS);
            }
        }
    }
    return positions;
};

export const isGlossary = function isGlossary(block: Block): block is GlossaryBlock {
    return block.kind === "glossary";
};

export const anchorsOf = function anchorsOf(section: Section): readonly string[] {
    const blocks = [...(section.blocks ?? []), ...section.subsections.flatMap((sub) => sub.blocks ?? [])];
    return [
        section.id,
        ...section.subsections.flatMap((sub) => (sub.id === undefined ? [] : [sub.id])),
        ...blocks.filter(isGlossary).flatMap((block) => block.entries.flatMap((entry) => entry.id ?? [])),
    ];
};

export const placeOf = function placeOf(anchors: readonly string[], links: ReadonlyMap<string, number>): number {
    return Math.min(UNPLACED, ...anchors.map((anchor) => links.get(anchor) ?? UNPLACED));
};