# codemods/analyzers/reference.analyzer.ts

> 89 lines of code and 25 definitions.

Tree: Governance tree
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/governance#file-governance-codemods-analyzers-reference-analyzer-ts
Source text: https://banes-lab.com/source/governance/codemods/analyzers/reference.analyzer.ts.txt

Listed in [codemods/analyzers](https://banes-lab.com/api/source/governance/codemods/analyzers.md), after [codemods/analyzers/location.analyzer.ts](https://banes-lab.com/source/governance/codemods/analyzers/location.analyzer.ts.md) and before [codemods/analyzers/specifier.analyzer.ts](https://banes-lab.com/source/governance/codemods/analyzers/specifier.analyzer.ts.md).

## Definitions

- `scanMarkup` (lexical_declaration, line 87)
- `walk` (lexical_declaration, line 10)
- `excluded` (lexical_declaration, line 8)
- `readText` (lexical_declaration, line 19)
- `markupReferences` (lexical_declaration, line 78)
- `collectReferenceFindings` (lexical_declaration, line 96, exported)
- `full` (lexical_declaration, line 14)
- `WHITESPACE` (lexical_declaration, line 23)
- `QUOTES` (lexical_declaration, line 24)
- `ATTRIBUTE_MARKERS` (lexical_declaration, line 25)
- `URL_MARKER` (lexical_declaration, line 26)
- `MARKUP_EXTENSIONS` (lexical_declaration, line 27)
- `skipWhitespace` (lexical_declaration, line 29)
- `readUntil` (lexical_declaration, line 37)
- `attributeValues` (lexical_declaration, line 45)
- `afterEquals` (lexical_declaration, line 49)
- `urlValues` (lexical_declaration, line 62)
- `out` (lexical_declaration, line 63)
- `at` (lexical_declaration, line 64)
- `opened` (lexical_declaration, line 66)
- `quote` (lexical_declaration, line 67)
- `start` (lexical_declaration, line 68)
- `value` (lexical_declaration, line 69)
- `attributes` (lexical_declaration, line 79)
- `urls` (lexical_declaration, line 83)

## Uses

- [shared/registries/location.registry.ts](https://banes-lab.com/source/governance/shared/registries/location.registry.ts.md)

## Source

```typescript
import { readFileSync, readdirSync } from "node:fs";
import { ROOT } from "@ssot/paths";
import type { ReferenceFinding } from "../../types/analyzer.types.ts";
import { excludeMatcher } from "@govlab/quality/config";
import path from "node:path";
import { tokenIn } from "../../shared/registries/location.registry.ts";

const excluded = await excludeMatcher(ROOT);

const walk = function walk(dir: string): string[] {
    return readdirSync(dir, { withFileTypes: true })
        .filter((entry) => !(entry.isDirectory() && excluded(path.join(dir, entry.name))))
        .flatMap((entry) => {
            const full = path.join(dir, entry.name);
            return entry.isDirectory() ? walk(full) : [full];
        });
};

const readText = function readText(file: string): string {
    return readFileSync(file, "utf8");
};

const WHITESPACE = new Set([" ", "\t", "\n", "\r"]);
const QUOTES = new Set(['"', "'"]);
const ATTRIBUTE_MARKERS = ["src=", "href="];
const URL_MARKER = "url(";
const MARKUP_EXTENSIONS = new Set([".html", ".css"]);

const skipWhitespace = function skipWhitespace(text: string, from: number): number {
    let at = from;
    while (at < text.length && WHITESPACE.has(text[at] ?? "")) {
        at += 1;
    }
    return at;
};

const readUntil = function readUntil(text: string, from: number, terminators: ReadonlySet<string>): string | null {
    let at = from;
    while (at < text.length && !terminators.has(text[at] ?? "")) {
        at += 1;
    }
    return at > from && at < text.length ? text.slice(from, at) : null;
};

const attributeValues = function attributeValues(text: string, marker: string): string[] {
    const out: string[] = [];
    let at = text.indexOf(marker);
    while (at !== -1) {
        const afterEquals = skipWhitespace(text, at + marker.length);
        const quote = text[afterEquals] ?? "";
        if (QUOTES.has(quote)) {
            const value = readUntil(text, afterEquals + 1, new Set([quote]));
            if (value !== null) {
                out.push(value);
            }
        }
        at = text.indexOf(marker, at + marker.length);
    }
    return out;
};

const urlValues = function urlValues(text: string): string[] {
    const out: string[] = [];
    let at = text.indexOf(URL_MARKER);
    while (at !== -1) {
        const opened = skipWhitespace(text, at + URL_MARKER.length);
        const quote = text[opened] ?? "";
        const start = QUOTES.has(quote) ? opened + 1 : opened;
        const value = readUntil(text, start, new Set(['"', "'", ")"]));
        if (value !== null) {
            out.push(value.trim());
        }
        at = text.indexOf(URL_MARKER, at + URL_MARKER.length);
    }
    return out;
};

const markupReferences = function markupReferences(text: string): { construct: string; value: string }[] {
    const attributes = ATTRIBUTE_MARKERS.flatMap((marker) => attributeValues(text, marker)).map((value) => ({
        construct: "html src/href",
        value,
    }));
    const urls = urlValues(text).map((value) => ({ construct: "css url()", value }));
    return [...attributes, ...urls];
};

const scanMarkup = function scanMarkup(file: string, rel: string): ReferenceFinding[] {
    if (!MARKUP_EXTENSIONS.has(path.extname(file).toLowerCase())) {
        return [];
    }
    return markupReferences(readText(file))
        .map((hit) => ({ construct: hit.construct, file: rel, token: tokenIn(hit.value), value: hit.value }))
        .filter((finding): finding is ReferenceFinding => finding.token !== null);
};

export const collectReferenceFindings = function collectReferenceFindings(): ReferenceFinding[] {
    return walk(ROOT)
        .flatMap((file) => scanMarkup(file, path.relative(ROOT, file).split(path.sep).join("/")))
        .toSorted((a, b) => a.file.localeCompare(b.file) || a.value.localeCompare(b.value));
};
```
