presentation/components/source.component.ts

presentation/components/source.component.ts is a file in Bane's Lab Site. 198 lines of code and 37 definitions.

import { CODE_LINE_ATTRIBUTE, CODE_LINE_CLASS } from "#configuration/constants/syntax.constants";
import type { CandidateLocation, CodeTarget, SourceReferences } from "#types/code.types";
import {
    INLINE_CODE_SELECTOR,
    REFERENCE_CLASS_NAME,
    REFERENCE_NAME_ATTRIBUTE,
    REFERENCE_SELECTOR,
    STRING_TOKEN_SELECTOR,
} from "#configuration/constants/anatomy.constants";
import { NO_CLASS, OPEN_CLASS } from "#configuration/constants/element.constants";
import {
    REFERENCE_CLASS,
    REFERENCE_EDGES_CLASS,
    REFERENCE_RELATION_CLASS,
    REFERENCE_TITLE_CLASS,
} from "#configuration/constants/reference.constants";
import { clearChildren, createElement } from "#core/factories/element.factory";
import { folderHref, nodeHref, pathLabel } from "#domain/converters/source.converter";
import { siteKey, siteTargetsOf } from "#core/converters/binding.converter";
import { targetIn, unquoted, wordsOf } from "#core/converters/code.converter";
import { ESCAPE_KEY } from "#configuration/constants/overlay.constants";
import { LINE_MARK } from "#configuration/strings/folder.strings";
import { SOURCE_PANEL_ID } from "#core/ids/anatomy.ids";
import { createLink } from "#presentation/components/link.component";
import { hrefOf } from "#domain/converters/ontology.converter";
import { placePanel } from "#presentation/components/reference.component";

const CODE_LINE_SELECTOR = `.${CODE_LINE_CLASS}`;

const candidates = new WeakMap<Element, readonly CandidateLocation[]>();

const referenceLink = function referenceLink(text: string, href: string): HTMLElement {
    return createLink(href, REFERENCE_CLASS_NAME, [text]);
};

const targetNode = function targetNode(text: string, target: CodeTarget | null): HTMLElement | null {
    switch (target?.kind) {
        case "definition": {
            return referenceLink(text, nodeHref(target.location.file, target.location.line));
        }
        case "file": {
            return referenceLink(text, nodeHref(target.path, null));
        }
        case "folder": {
            return referenceLink(text, folderHref(target.path));
        }
        case "record": {
            const href = hrefOf(target.ref);
            return href === null ? null : referenceLink(text, href);
        }
        case "candidates": {
            const span = createElement("span", {
                attributes: { [REFERENCE_NAME_ATTRIBUTE]: text },
                className: REFERENCE_CLASS_NAME,
                text,
            });
            candidates.set(span, target.locations);
            return span;
        }
        case undefined: {
            return null;
        }
        default: {
            return null;
        }
    }
};

interface Placed {
    readonly column: number;
    readonly node: Text;
}

const placedIn = function placedIn(line: Element): readonly Placed[] {
    const placed: Placed[] = [];
    const walker = document.createTreeWalker(line, NodeFilter.SHOW_TEXT);
    let column = 0;
    let current = walker.nextNode();
    while (current !== null) {
        if (current instanceof Text) {
            placed.push({ column, node: current });
            column += current.data.length;
        }
        current = walker.nextNode();
    }
    return placed;
};

const markWords = function markWords(
    placed: Placed,
    lookup: (word: string, column: number) => CodeTarget | null,
): void {
    let { column } = placed;
    const pieces = wordsOf(placed.node.data).map((part) => {
        const piece = targetNode(part, lookup(part, column)) ?? document.createTextNode(part);
        column += part.length;
        return piece;
    });
    if (pieces.some((piece) => piece instanceof HTMLElement)) {
        placed.node.replaceWith(...pieces);
    }
};

const markString = function markString(token: Element, target: CodeTarget | null): void {
    const link = targetNode(token.textContent, target);
    if (link !== null) {
        link.classList.add(...token.classList);
        token.replaceWith(link);
    }
};

const markLine = function markLine(
    line: Element,
    references: SourceReferences,
    sites: ReadonlyMap<string, CodeTarget>,
): void {
    const number = Number(line.getAttribute(CODE_LINE_ATTRIBUTE));
    const strings = new Map<Element, number>();
    const words: Placed[] = [];
    for (const piece of placedIn(line)) {
        const token = piece.node.parentElement?.closest(STRING_TOKEN_SELECTOR) ?? null;
        if (token === null) {
            words.push(piece);
        } else {
            strings.set(token, strings.get(token) ?? piece.column);
        }
    }
    for (const [token, column] of strings) {
        const site = sites.get(siteKey(number, column));
        markString(token, site ?? targetIn(references.strings, unquoted(token.textContent)));
    }
    for (const piece of words) {
        markWords(piece, (word, column) => sites.get(siteKey(number, column)) ?? targetIn(references.words, word));
    }
};

export const markReferences = function markReferences(code: Element, references: SourceReferences): void {
    const sites = siteTargetsOf(references);
    for (const line of code.querySelectorAll(CODE_LINE_SELECTOR)) {
        markLine(line, references, sites);
    }
};

export const markDocumentReferences = function markDocumentReferences(
    rendered: Element,
    references: SourceReferences,
): void {
    for (const code of rendered.querySelectorAll(INLINE_CODE_SELECTOR)) {
        const reference = targetNode(code.textContent, targetIn(references.spans, code.textContent));
        if (reference !== null) {
            code.replaceChildren(reference);
        }
    }
};

const sharedPanel = function sharedPanel(): HTMLElement {
    const existing = document.getElementById(SOURCE_PANEL_ID);
    if (existing !== null) {
        return existing;
    }
    const panel = createElement("div", { attributes: { id: SOURCE_PANEL_ID }, className: REFERENCE_CLASS });
    document.body.append(panel);
    const hide = function hide(): void {
        panel.classList.remove(OPEN_CLASS);
        clearChildren(panel);
    };
    document.addEventListener("keydown", (event) => {
        if (event.key === ESCAPE_KEY) {
            hide();
        }
    });
    document.addEventListener("pointerdown", (event) => {
        if (!(event.target instanceof Node) || !panel.contains(event.target)) {
            hide();
        }
    });
    return panel;
};

const candidateLink = function candidateLink(location: CandidateLocation): HTMLElement {
    const line = location.line === null ? "" : LINE_MARK + String(location.line);
    const link = createLink(nodeHref(location.file, location.line), NO_CLASS, [pathLabel(location.file) + line]);
    return createElement("div", {
        children: [createElement("span", { children: [link], className: REFERENCE_EDGES_CLASS })],
        className: REFERENCE_RELATION_CLASS,
    });
};

const offerCandidates = function offerCandidates(
    name: string,
    locations: readonly CandidateLocation[],
    anchor: Element,
): void {
    const panel = sharedPanel();
    clearChildren(panel);
    panel.append(
        createElement("div", { className: REFERENCE_TITLE_CLASS, text: name }),
        ...locations.map(candidateLink),
    );
    panel.classList.add(OPEN_CLASS);
    placePanel(panel, anchor);
};

export const enableSourceNavigation = function enableSourceNavigation(holder: HTMLElement): void {
    holder.addEventListener("click", (event) => {
        const reference = event.target instanceof Element ? event.target.closest(REFERENCE_SELECTOR) : null;
        const locations = reference === null ? undefined : candidates.get(reference);
        if (reference === null || locations === undefined) {
            return;
        }
        offerCandidates(reference.getAttribute(REFERENCE_NAME_ATTRIBUTE) ?? "", locations, reference);
    });
};