# core/converters/link.converter.ts

> 64 lines of code and 20 definitions.

Tree: Bane's Lab Build Scripts
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/build-scripts#file-build-scripts-core-converters-link-converter-ts
Source text: https://banes-lab.com/source/build-scripts/core/converters/link.converter.ts.txt

Listed in [core/converters](https://banes-lab.com/api/source/build-scripts/core/converters.md), after [core/converters/lexicon.converter.ts](https://banes-lab.com/source/build-scripts/core/converters/lexicon.converter.ts.md) and before [core/converters/location.converter.ts](https://banes-lab.com/source/build-scripts/core/converters/location.converter.ts.md).

## Definitions

- `relationGroups` (lexical_declaration, line 43, exported)
- `rewriteTargets` (lexical_declaration, line 25, exported)
- `rewriteLine` (lexical_declaration, line 10)
- `linkKey` (lexical_declaration, line 39)
- `nodeLinks` (lexical_declaration, line 55, exported)
- `FENCE` (lexical_declaration, line 5)
- `LINE_END` (lexical_declaration, line 6)
- `TARGET_OPEN` (lexical_declaration, line 7)
- `TARGET_CLOSE` (lexical_declaration, line 8)
- `out` (lexical_declaration, line 11)
- `cursor` (lexical_declaration, line 12)
- `open` (lexical_declaration, line 14)
- `close` (lexical_declaration, line 15)
- `start` (lexical_declaration, line 19)
- `fenced` (lexical_declaration, line 26, exported)
- `merged` (lexical_declaration, line 44, exported)
- `held` (lexical_declaration, line 46, exported)
- `seen` (lexical_declaration, line 47, exported)
- `href` (lexical_declaration, line 61, exported)
- `identity` (lexical_declaration, line 62, exported)

## Contained in

- [core/converters](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-converters.md)

## Used by

- [core/converters/section.converter.ts](https://banes-lab.com/source/build-scripts/core/converters/section.converter.ts.md)
- [core/converters/source.converter.ts](https://banes-lab.com/source/build-scripts/core/converters/source.converter.ts.md)
- [core/coordinators/catalog.coordinator.ts](https://banes-lab.com/source/build-scripts/core/coordinators/catalog.coordinator.ts.md)
- [core/resolvers/link.resolver.ts](https://banes-lab.com/source/build-scripts/core/resolvers/link.resolver.ts.md)

## Linked from

- [core/converters](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-converters.md)
- [core/coordinators](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-coordinators.md)
- [core/resolvers](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-resolvers.md)

## Source

````typescript
import type { Link, Linker, Relation } from "#types/catalog.types";
import type { EdgeRef } from "@banes-lab/web/types/link.types.js";
import type { GraphNode } from "@banes-lab/web/types/graph.types.js";

const FENCE = "```";
const LINE_END = "\n";
const TARGET_OPEN = "](";
const TARGET_CLOSE = ")";

const rewriteLine = function rewriteLine(line: string, rewrite: (target: string) => string): string {
    let out = "";
    let cursor = 0;
    for (;;) {
        const open = line.indexOf(TARGET_OPEN, cursor);
        const close = open === -1 ? -1 : line.indexOf(TARGET_CLOSE, open);
        if (close === -1) {
            return out + line.slice(cursor);
        }
        const start = open + TARGET_OPEN.length;
        out += line.slice(cursor, start) + rewrite(line.slice(start, close));
        cursor = close;
    }
};

export const rewriteTargets = function rewriteTargets(markdown: string, rewrite: (target: string) => string): string {
    let fenced = false;
    return markdown
        .split(LINE_END)
        .map((line) => {
            if (line.startsWith(FENCE)) {
                fenced = !fenced;
                return line;
            }
            return fenced ? line : rewriteLine(line, rewrite);
        })
        .join(LINE_END);
};

const linkKey = function linkKey(link: Link): string {
    return link.ref ?? link.href ?? link.label;
};

export const relationGroups = function relationGroups(groups: readonly Relation[]): readonly Relation[] {
    const merged = new Map<string, readonly Link[]>();
    for (const group of groups) {
        const held = merged.get(group.relation) ?? [];
        const seen = new Set(held.map(linkKey));
        merged.set(group.relation, [...held, ...group.links.filter((link) => !seen.has(linkKey(link)))]);
    }
    return [...merged.entries()]
        .filter(([, links]) => links.length > 0)
        .map(([relation, links]) => ({ links, relation }));
};

export const nodeLinks = function nodeLinks(
    linker: Linker,
    nodes: ReadonlyMap<string, GraphNode>,
    edges: readonly EdgeRef[],
): readonly Link[] {
    return edges.map((edge) => {
        const href = edge.ref === null ? null : (nodes.get(edge.ref)?.href ?? null);
        const identity = href === null ? null : linker.byHref(href);
        if (identity !== null) {
            return linker.link(edge.label, identity.ref);
        }
        return href === null
            ? linker.link(edge.label, edge.ref)
            : { href: linker.site + href, json: null, label: edge.label, markdown: null, ref: edge.ref };
    });
};
````
