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);
});
};