@binclusive/a11y 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +285 -0
- package/bin/a11y.mjs +36 -0
- package/bin/diff-scope.mjs +29 -0
- package/data/baseline-rules.json +892 -0
- package/package.json +68 -0
- package/src/agent-lane.ts +138 -0
- package/src/agents-block.ts +157 -0
- package/src/baseline/gen-baseline.ts +166 -0
- package/src/cli.ts +1026 -0
- package/src/collect-dom.ts +119 -0
- package/src/collect-liquid.ts +103 -0
- package/src/collect-swift.ts +227 -0
- package/src/collect-unity.ts +99 -0
- package/src/collect.ts +54 -0
- package/src/commands.ts +314 -0
- package/src/config-scan.ts +177 -0
- package/src/contract.ts +355 -0
- package/src/core.ts +546 -0
- package/src/detect-stack.ts +207 -0
- package/src/diff-scope-cli.ts +12 -0
- package/src/diff-scope.ts +150 -0
- package/src/emit-contract.ts +181 -0
- package/src/enforce.ts +1125 -0
- package/src/eslint-plugin-jsx-a11y.d.ts +11 -0
- package/src/evidence.ts +308 -0
- package/src/github-identity.ts +201 -0
- package/src/hook.ts +242 -0
- package/src/impact-gate.ts +107 -0
- package/src/imports-resolve.ts +248 -0
- package/src/index.ts +183 -0
- package/src/liquid-ast.ts +203 -0
- package/src/liquid-rules.ts +691 -0
- package/src/mcp.ts +363 -0
- package/src/module-scope.ts +137 -0
- package/src/phone-home.ts +470 -0
- package/src/pr-comment.ts +250 -0
- package/src/pr-summary-cli.ts +182 -0
- package/src/pr-summary.ts +291 -0
- package/src/registry.ts +605 -0
- package/src/reporter/contract.ts +154 -0
- package/src/reporter/finding.ts +87 -0
- package/src/reporter/github-adapter.ts +183 -0
- package/src/reporter/null-adapter.ts +51 -0
- package/src/reporter/registry.ts +22 -0
- package/src/reporter-cli.ts +48 -0
- package/src/resolve-components.ts +579 -0
- package/src/runner/budget.ts +90 -0
- package/src/runner/codegraph-lookup.test.ts +93 -0
- package/src/runner/codegraph-lookup.ts +197 -0
- package/src/runner/index.ts +86 -0
- package/src/runner/lookup.ts +69 -0
- package/src/runner/provider.ts +72 -0
- package/src/runner/providers/anthropic.ts +168 -0
- package/src/runner/providers/openai.ts +191 -0
- package/src/runner/reasoner.ts +73 -0
- package/src/runner/reasoning/index.ts +53 -0
- package/src/runner/reasoning/prompt.ts +200 -0
- package/src/runner/reasoning/react.ts +149 -0
- package/src/runner/reasoning/shopify.ts +214 -0
- package/src/runner/reasoning/skills-reasoner.ts +117 -0
- package/src/runner/reasoning/types.ts +99 -0
- package/src/runner/runner.ts +203 -0
- package/src/sarif.ts +148 -0
- package/src/source-identity.ts +0 -0
- package/src/source-trace.ts +814 -0
- package/src/suggest.ts +328 -0
- package/src/suppression-ranges.ts +354 -0
- package/src/suppressor-map.ts +189 -0
- package/src/suppressors.ts +284 -0
- package/src/tsconfig-aliases.ts +155 -0
- package/src/unity-ast.ts +331 -0
- package/src/unity-findings.ts +91 -0
- package/src/unity-guid-registry.ts +82 -0
- package/src/unity-label-resolve.ts +249 -0
- package/src/unity-rule-color-only.ts +127 -0
- package/src/unity-rule-missing-label.ts +156 -0
- package/src/unity-rules-baseline.ts +273 -0
- package/src/wcag-map.ts +35 -0
- package/src/wcag-tags.ts +35 -0
- package/src/workspace-resolve.ts +405 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@binclusive/a11y` — the engine library surface.
|
|
3
|
+
*
|
|
4
|
+
* The curated public API (see `docs/ENGINE-API.md`): the functions a CLI
|
|
5
|
+
* (`b8e`, a lean shim) and agents call, organized into four clusters —
|
|
6
|
+
* Scan, Contract lifecycle, Agent integration, Corpus.
|
|
7
|
+
*
|
|
8
|
+
* Engine INTERNALS are deliberately NOT re-exported here: component resolution,
|
|
9
|
+
* source tracing, the registry, module-scope, workspace/tsconfig resolution,
|
|
10
|
+
* the enforce pass, and the `distill/` corpus build-tool. They are implementation,
|
|
11
|
+
* not contract — import them by path if you're working inside the engine.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ── 1 · Scan & enrich — the read path ────────────────────────────────────────
|
|
15
|
+
export { collectTsx } from "./collect";
|
|
16
|
+
export {
|
|
17
|
+
type DiffScopeInput,
|
|
18
|
+
parseChangedFiles,
|
|
19
|
+
scopeChangedTsx,
|
|
20
|
+
scopeChangedTsxFromEnv,
|
|
21
|
+
} from "./diff-scope";
|
|
22
|
+
export {
|
|
23
|
+
checkFiles,
|
|
24
|
+
dedupeRecall,
|
|
25
|
+
type Finding,
|
|
26
|
+
type FindingLayer,
|
|
27
|
+
type FindingProvenance,
|
|
28
|
+
type ScanResult,
|
|
29
|
+
scan,
|
|
30
|
+
} from "./core";
|
|
31
|
+
export { type DomScanOptions, type DomScanResult, scanUrl } from "./collect-dom";
|
|
32
|
+
export { type SwiftScanResult, scanSwift } from "./collect-swift";
|
|
33
|
+
export {
|
|
34
|
+
type DisplayContract,
|
|
35
|
+
type EnrichedFinding,
|
|
36
|
+
enrich,
|
|
37
|
+
enrichAll,
|
|
38
|
+
resolveDisplay,
|
|
39
|
+
} from "./evidence";
|
|
40
|
+
// Types that `ScanResult` and the CLI presentation reference (the functions that
|
|
41
|
+
// produce them — `resolveComponents` — stay internal):
|
|
42
|
+
export {
|
|
43
|
+
type ComponentResolution,
|
|
44
|
+
type Coverage,
|
|
45
|
+
type OpaqueKind,
|
|
46
|
+
type Provenance,
|
|
47
|
+
type ResolvedComponents,
|
|
48
|
+
type ResolvedProvenance,
|
|
49
|
+
} from "./resolve-components";
|
|
50
|
+
|
|
51
|
+
// ── 2 · Contract lifecycle — init / config ──────────────────────────────────
|
|
52
|
+
export {
|
|
53
|
+
BLOCK_TARGETS,
|
|
54
|
+
CONTRACT_FILE,
|
|
55
|
+
type DriftEntry,
|
|
56
|
+
type GenResult,
|
|
57
|
+
gen,
|
|
58
|
+
type InitOptions,
|
|
59
|
+
type InitResult,
|
|
60
|
+
init,
|
|
61
|
+
type LearnInput,
|
|
62
|
+
type LearnResult,
|
|
63
|
+
learn,
|
|
64
|
+
loadContract,
|
|
65
|
+
} from "./commands";
|
|
66
|
+
export {
|
|
67
|
+
contractForFiles,
|
|
68
|
+
type EnforcementLevel,
|
|
69
|
+
enforcementFor,
|
|
70
|
+
fileIgnoreMatcher,
|
|
71
|
+
findContractFrom,
|
|
72
|
+
ignoredRuleIds,
|
|
73
|
+
} from "./config-scan";
|
|
74
|
+
export {
|
|
75
|
+
CONTRACT_VERSION,
|
|
76
|
+
type Contract,
|
|
77
|
+
ContractParseError,
|
|
78
|
+
type Declarations,
|
|
79
|
+
defaultEnforcement,
|
|
80
|
+
type Enforcement,
|
|
81
|
+
emptyDeclarations,
|
|
82
|
+
type Language,
|
|
83
|
+
type LearnedRule,
|
|
84
|
+
parseContract,
|
|
85
|
+
type Router,
|
|
86
|
+
serializeContract,
|
|
87
|
+
type Stack,
|
|
88
|
+
} from "./contract";
|
|
89
|
+
export { detectStack } from "./detect-stack";
|
|
90
|
+
// Emit path — the metadata-only wire projection (local finding -> contract) and
|
|
91
|
+
// the LOCAL SARIF renderer that reads the rich model directly. Both narrow
|
|
92
|
+
// through the ONE `evidenceImpact` accessor + `impactToLevel` (impact -> SARIF).
|
|
93
|
+
export {
|
|
94
|
+
type LenientPayload,
|
|
95
|
+
toContractFinding,
|
|
96
|
+
toContractProvenance,
|
|
97
|
+
toFindingPayload,
|
|
98
|
+
toFindingPayloadLenient,
|
|
99
|
+
} from "./emit-contract";
|
|
100
|
+
export {
|
|
101
|
+
diskLineSource,
|
|
102
|
+
isPageFinding,
|
|
103
|
+
type LineSource,
|
|
104
|
+
lineContentHash,
|
|
105
|
+
type LocationOptions,
|
|
106
|
+
normalizeLine,
|
|
107
|
+
resolveLocations,
|
|
108
|
+
} from "./source-identity";
|
|
109
|
+
export { formatSarif, impactToLevel } from "./sarif";
|
|
110
|
+
export {
|
|
111
|
+
type ComponentSuggestion,
|
|
112
|
+
type SuggestConfidence,
|
|
113
|
+
type SuggestOptions,
|
|
114
|
+
type SuggestResult,
|
|
115
|
+
suggestComponentMap,
|
|
116
|
+
} from "./suggest";
|
|
117
|
+
|
|
118
|
+
// Provider-agnostic AI-lane runner (#2095): BYO provider + the capped,
|
|
119
|
+
// non-blocking pull loop. Reasoning content (skills/lookups/finding logic) is
|
|
120
|
+
// injected through the AgentReasoner + LookupTool seams (#2096/#2097/#2098).
|
|
121
|
+
export {
|
|
122
|
+
type AgentFinding,
|
|
123
|
+
type AgentReasoner,
|
|
124
|
+
type BudgetSnapshot,
|
|
125
|
+
type CodeGraphLookupConfig,
|
|
126
|
+
type CodeGraphLookupData,
|
|
127
|
+
type CodeGraphQueryKind,
|
|
128
|
+
createCodeGraphLookup,
|
|
129
|
+
DEFAULT_RUNNER_CONFIG,
|
|
130
|
+
LookupCounter,
|
|
131
|
+
type LookupQuery,
|
|
132
|
+
type LookupResult,
|
|
133
|
+
isMeterableUsage,
|
|
134
|
+
type LookupTool,
|
|
135
|
+
meterLookup,
|
|
136
|
+
meterProvider,
|
|
137
|
+
type PassOutcome,
|
|
138
|
+
type PassReport,
|
|
139
|
+
type Provider,
|
|
140
|
+
type ProviderMessage,
|
|
141
|
+
type ProviderRequest,
|
|
142
|
+
type ProviderResponse,
|
|
143
|
+
type ReasonContext,
|
|
144
|
+
type RunInput,
|
|
145
|
+
runAgentLane,
|
|
146
|
+
type RunnerConfig,
|
|
147
|
+
type RunOutcome,
|
|
148
|
+
TokenCeilingExceeded,
|
|
149
|
+
TokenLedger,
|
|
150
|
+
type TokenUsage,
|
|
151
|
+
usageTotal,
|
|
152
|
+
} from "./runner";
|
|
153
|
+
|
|
154
|
+
// ── 3 · Agent integration — hook + MCP ──────────────────────────────────────
|
|
155
|
+
export { type HookOutput, runHook } from "./hook";
|
|
156
|
+
export {
|
|
157
|
+
buildServer,
|
|
158
|
+
type CheckA11yResult,
|
|
159
|
+
type CheckFinding,
|
|
160
|
+
type CheckUrlResult,
|
|
161
|
+
checkA11y,
|
|
162
|
+
checkUrl,
|
|
163
|
+
type GetA11yRulesResult,
|
|
164
|
+
getA11yRules,
|
|
165
|
+
type LearnA11yRuleResult,
|
|
166
|
+
learnA11yRule,
|
|
167
|
+
startStdioServer,
|
|
168
|
+
} from "./mcp";
|
|
169
|
+
|
|
170
|
+
// ── 4 · Evidence — the coverage catalog (axe baseline), read-only ────────────
|
|
171
|
+
// The corpus left the engine (ADR 0041 §G): pure detection, baseline everywhere.
|
|
172
|
+
export {
|
|
173
|
+
type AxeImpact,
|
|
174
|
+
type BaselineRuleInfo,
|
|
175
|
+
baselineRules,
|
|
176
|
+
type Evidence,
|
|
177
|
+
evidenceBestPractice,
|
|
178
|
+
evidenceFix,
|
|
179
|
+
evidenceHelpUrl,
|
|
180
|
+
evidenceImpact,
|
|
181
|
+
} from "./evidence";
|
|
182
|
+
export { BLOCK_BEGIN, BLOCK_END, extractBlock, renderBlock, spliceBlock } from "./agents-block";
|
|
183
|
+
export { RULE_ID_TO_WCAG, wcagForRuleId } from "./wcag-map";
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Liquid AST layer — L1 of the Shopify/Liquid static producer (issue #47).
|
|
3
|
+
*
|
|
4
|
+
* This is the parser foundation the structural-absence rules (L2) and the
|
|
5
|
+
* `collect-liquid` producer + `check-shopify` command (L3) build on. It wraps
|
|
6
|
+
* `@shopify/liquid-html-parser` (Shopify's own theme-check parser) and exposes the
|
|
7
|
+
* one distinction the whole precision invariant rides on: an HTML attribute can be
|
|
8
|
+
* ABSENT, or PRESENT with a value that is static text, a render-time Liquid
|
|
9
|
+
* expression (dynamic), or empty. A dynamic value means the attribute IS present —
|
|
10
|
+
* conflating "value is dynamic" with "attribute missing" is the false positive that
|
|
11
|
+
* gets an a11y tool uninstalled, so it is isolated and tested here, not in the rules.
|
|
12
|
+
*
|
|
13
|
+
* Liquid holes (`{{ }}` / `{% %}`) stay opaque: the parser keeps them as Liquid
|
|
14
|
+
* nodes, never text/markup that could trip a rule. Parsing runs in tolerant mode so
|
|
15
|
+
* odd real-world theme markup falls back to a string node instead of throwing.
|
|
16
|
+
*
|
|
17
|
+
* Pattern docs (read before changing this): `.patterns/liquid-html-parser/`
|
|
18
|
+
* (`parsing.md`, `attributes.md`, `traversal.md`).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
getName,
|
|
23
|
+
NodeTypes,
|
|
24
|
+
toLiquidHtmlAST,
|
|
25
|
+
walk,
|
|
26
|
+
type AttrDoubleQuoted,
|
|
27
|
+
type AttrEmpty,
|
|
28
|
+
type AttrSingleQuoted,
|
|
29
|
+
type AttrUnquoted,
|
|
30
|
+
type DocumentNode,
|
|
31
|
+
type HtmlElement,
|
|
32
|
+
type HtmlRawNode,
|
|
33
|
+
type HtmlSelfClosingElement,
|
|
34
|
+
type HtmlVoidElement,
|
|
35
|
+
type LiquidHtmlNode,
|
|
36
|
+
} from "@shopify/liquid-html-parser";
|
|
37
|
+
|
|
38
|
+
/** The HTML element node kinds that carry an `attributes` list. */
|
|
39
|
+
export type HtmlElementNode =
|
|
40
|
+
| HtmlElement
|
|
41
|
+
| HtmlVoidElement
|
|
42
|
+
| HtmlSelfClosingElement
|
|
43
|
+
| HtmlRawNode;
|
|
44
|
+
|
|
45
|
+
/** The concrete attribute node kinds (`{% if %}`-wrapped Liquid nodes are descended,
|
|
46
|
+
* never returned as attributes). */
|
|
47
|
+
export type AttrNode = AttrSingleQuoted | AttrDoubleQuoted | AttrUnquoted | AttrEmpty;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse result. Tolerant-mode parsing never throws on `{{ }}`/`{% %}`, but an
|
|
51
|
+
* unclosed HTML/Liquid block still throws by default — we catch it and return
|
|
52
|
+
* `{ ok: false }` so a producer scanning a whole theme never crashes on one
|
|
53
|
+
* malformed file. The caller (L3) decides whether to skip or surface it.
|
|
54
|
+
*/
|
|
55
|
+
export type ParseResult =
|
|
56
|
+
| { readonly ok: true; readonly ast: DocumentNode }
|
|
57
|
+
| { readonly ok: false; readonly error: Error };
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* How an HTML attribute's value resolves when read statically from `.liquid`.
|
|
61
|
+
* The discriminant is what the structural-absence rules branch on: only `absent`
|
|
62
|
+
* (and, per rule, `empty`) is ever a finding — `dynamic` is present-and-unknowable
|
|
63
|
+
* and must never be flagged.
|
|
64
|
+
*/
|
|
65
|
+
export type AttrValue =
|
|
66
|
+
| { readonly kind: "absent" } // no such attribute node on the element
|
|
67
|
+
| { readonly kind: "empty" } // present, no value (AttrEmpty, or value=[])
|
|
68
|
+
| { readonly kind: "static"; readonly text: string } // value is purely literal text
|
|
69
|
+
| { readonly kind: "dynamic" }; // value contains a Liquid expression
|
|
70
|
+
|
|
71
|
+
/** Byte-offset span into the source — for L2/L3 to anchor a finding's location. */
|
|
72
|
+
export interface SourceSpan {
|
|
73
|
+
readonly start: number; // 0-indexed, inclusive
|
|
74
|
+
readonly end: number; // 0-indexed, exclusive
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const HTML_ELEMENT_TYPES: ReadonlySet<string> = new Set([
|
|
78
|
+
NodeTypes.HtmlElement,
|
|
79
|
+
NodeTypes.HtmlVoidElement,
|
|
80
|
+
NodeTypes.HtmlSelfClosingElement,
|
|
81
|
+
NodeTypes.HtmlRawNode,
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
const ATTR_TYPES: ReadonlySet<string> = new Set([
|
|
85
|
+
NodeTypes.AttrSingleQuoted,
|
|
86
|
+
NodeTypes.AttrDoubleQuoted,
|
|
87
|
+
NodeTypes.AttrUnquoted,
|
|
88
|
+
NodeTypes.AttrEmpty,
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Parse a `.liquid` source string into an AST. Tolerant mode: unrecognized Liquid
|
|
93
|
+
* markup falls back to a string node rather than throwing, so a real-world theme
|
|
94
|
+
* with odd tags still parses. An unclosed document throws (the parser default);
|
|
95
|
+
* we catch and return it as `{ ok: false }`.
|
|
96
|
+
*/
|
|
97
|
+
export function parseLiquid(source: string): ParseResult {
|
|
98
|
+
try {
|
|
99
|
+
const ast = toLiquidHtmlAST(source, {
|
|
100
|
+
mode: "tolerant",
|
|
101
|
+
allowUnclosedDocumentNode: false,
|
|
102
|
+
});
|
|
103
|
+
return { ok: true, ast };
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return { ok: false, error: error instanceof Error ? error : new Error(String(error)) };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Is this node an HTML element (i.e. does it carry an `attributes` list)? */
|
|
110
|
+
export function isHtmlElement(node: LiquidHtmlNode): node is HtmlElementNode {
|
|
111
|
+
return HTML_ELEMENT_TYPES.has(node.type);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Visit every HTML element node in the tree. Uses the library's `walk`, which
|
|
116
|
+
* descends into attributes and name arrays too — plain `children` recursion would
|
|
117
|
+
* silently skip them.
|
|
118
|
+
*/
|
|
119
|
+
export function eachHtmlElement(
|
|
120
|
+
ast: DocumentNode,
|
|
121
|
+
fn: (node: HtmlElementNode) => void,
|
|
122
|
+
): void {
|
|
123
|
+
walk(ast, (node: LiquidHtmlNode) => {
|
|
124
|
+
if (isHtmlElement(node)) fn(node);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The element's tag name, normalized to a string (`getName` handles both the
|
|
129
|
+
* plain-string void-element form and the compound array form). `null` if unnamed. */
|
|
130
|
+
export function elementName(node: HtmlElementNode): string | null {
|
|
131
|
+
return getName(node);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* All attribute nodes on an element, descending into a `{% if %}`-wrapped attribute
|
|
136
|
+
* list so an attribute hidden inside a Liquid tag still counts as present. The
|
|
137
|
+
* returned nodes are the real `Attr*` nodes, never the Liquid wrapper.
|
|
138
|
+
*/
|
|
139
|
+
export function htmlAttributes(node: HtmlElementNode): AttrNode[] {
|
|
140
|
+
const out: AttrNode[] = [];
|
|
141
|
+
const collect = (attrs: readonly LiquidHtmlNode[]): void => {
|
|
142
|
+
for (const a of attrs) {
|
|
143
|
+
if (ATTR_TYPES.has(a.type)) {
|
|
144
|
+
out.push(a as AttrNode);
|
|
145
|
+
} else if ("children" in a && Array.isArray((a as { children?: unknown }).children)) {
|
|
146
|
+
collect((a as unknown as { children: LiquidHtmlNode[] }).children);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
collect(node.attributes as readonly LiquidHtmlNode[]);
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Find the attribute node named `name` (case-insensitive) on an element, if any. */
|
|
155
|
+
export function findAttr(node: HtmlElementNode, name: string): AttrNode | undefined {
|
|
156
|
+
const target = name.toLowerCase();
|
|
157
|
+
return htmlAttributes(node).find((a) => {
|
|
158
|
+
const n = getName(a);
|
|
159
|
+
return n != null && n.toLowerCase() === target;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Classify an attribute node into absent / empty / static / dynamic. `undefined`
|
|
165
|
+
* (the result of a failed lookup) is `absent`. Presence is decided by node
|
|
166
|
+
* existence, never by whether the value is empty or dynamic — see
|
|
167
|
+
* `.patterns/liquid-html-parser/attributes.md`.
|
|
168
|
+
*/
|
|
169
|
+
export function classifyAttr(attr: AttrNode | undefined): AttrValue {
|
|
170
|
+
if (!attr) return { kind: "absent" };
|
|
171
|
+
if (attr.type === NodeTypes.AttrEmpty) return { kind: "empty" };
|
|
172
|
+
const value = (attr as { value?: Array<{ type: string; value?: string }> }).value;
|
|
173
|
+
if (!value || value.length === 0) return { kind: "empty" };
|
|
174
|
+
const allText = value.every((v) => v.type === NodeTypes.TextNode);
|
|
175
|
+
if (allText) return { kind: "static", text: value.map((v) => v.value ?? "").join("") };
|
|
176
|
+
return { kind: "dynamic" };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Convenience: classify attribute `name` on `node` in one call. */
|
|
180
|
+
export function attr(node: HtmlElementNode, name: string): AttrValue {
|
|
181
|
+
return classifyAttr(findAttr(node, name));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The source span a node covers, for anchoring a finding's location. */
|
|
185
|
+
export function spanOf(node: LiquidHtmlNode): SourceSpan {
|
|
186
|
+
const position = (node as { position: SourceSpan }).position;
|
|
187
|
+
return { start: position.start, end: position.end };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The exact source text a node covers, reconstructed from its position span. The
|
|
192
|
+
* one way to read inside an `HtmlRawNode` (`<svg>`, `<script>`, `<style>`): its
|
|
193
|
+
* body is opaque raw markup, never parsed into child nodes, so a name source like
|
|
194
|
+
* an `<svg>`'s `<title>` is only visible here. See
|
|
195
|
+
* `.patterns/liquid-html-parser/node-taxonomy.md` (HtmlRawNode) and `traversal.md`
|
|
196
|
+
* (source position). Returns `""` if the node carries no `source`/`position`.
|
|
197
|
+
*/
|
|
198
|
+
export function rawSourceOf(node: LiquidHtmlNode): string {
|
|
199
|
+
const source = (node as { source?: string }).source;
|
|
200
|
+
const position = (node as { position?: SourceSpan }).position;
|
|
201
|
+
if (typeof source !== "string" || !position) return "";
|
|
202
|
+
return source.slice(position.start, position.end);
|
|
203
|
+
}
|