@backbencher/brain 0.0.0-stage → 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/CHANGELOG.md +298 -0
- package/EMBEDDINGS.md +355 -0
- package/LICENSE +21 -0
- package/README.md +415 -2
- package/bin/bb +13 -0
- package/dist/analysis-exclusions-7ZJDXOVZ.js +35 -0
- package/dist/brain-index-health-KY7K552E.js +17 -0
- package/dist/brain-relationship-enrichment-QH7OOHUF.js +558 -0
- package/dist/brain.sql-EIM4WS4D.js +29 -0
- package/dist/chunk-2BSKD7O3.js +40 -0
- package/dist/chunk-2DE27DVC.js +1314 -0
- package/dist/chunk-2RRBMNBQ.js +322 -0
- package/dist/chunk-2RTN2DFU.js +711 -0
- package/dist/chunk-2WXYNLBA.js +29 -0
- package/dist/chunk-3CTFIJJB.js +17 -0
- package/dist/chunk-3LFTSMIV.js +174 -0
- package/dist/chunk-47HKX4AJ.js +930 -0
- package/dist/chunk-4H22TLOT.js +543 -0
- package/dist/chunk-5SD45FLC.js +818 -0
- package/dist/chunk-66A5ZFYA.js +1758 -0
- package/dist/chunk-6AIRWAO2.js +247 -0
- package/dist/chunk-6WNGD3NN.js +196 -0
- package/dist/chunk-7AN7U5IZ.js +371 -0
- package/dist/chunk-BHB7BYYI.js +626 -0
- package/dist/chunk-BQ3OVQ3P.js +125 -0
- package/dist/chunk-CW4XXSNN.js +1479 -0
- package/dist/chunk-D2Q4Y47A.js +53 -0
- package/dist/chunk-DIYAGQJL.js +34 -0
- package/dist/chunk-DYM4FQEE.js +90 -0
- package/dist/chunk-EWMWK7WN.js +75 -0
- package/dist/chunk-FVVQTSIM.js +3427 -0
- package/dist/chunk-FWSL4VG6.js +1290 -0
- package/dist/chunk-GBI7V7KY.js +69 -0
- package/dist/chunk-GEFFO6O2.js +152 -0
- package/dist/chunk-HOWAQXJO.js +236 -0
- package/dist/chunk-HQSPJDHI.js +613 -0
- package/dist/chunk-IFZNR5UJ.js +2863 -0
- package/dist/chunk-J4PMBXQQ.js +336 -0
- package/dist/chunk-KAET47ZL.js +125 -0
- package/dist/chunk-KCDECRJY.js +440 -0
- package/dist/chunk-KLK3PBTO.js +1119 -0
- package/dist/chunk-L5TDYKYK.js +76 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-OWISBAOF.js +61 -0
- package/dist/chunk-PJTNGD3Z.js +6924 -0
- package/dist/chunk-QLK6ED4Q.js +235 -0
- package/dist/chunk-R2GW4F6R.js +19 -0
- package/dist/chunk-RCMTFSEF.js +87 -0
- package/dist/chunk-RH7KC5HU.js +12050 -0
- package/dist/chunk-RM3PY6TG.js +376 -0
- package/dist/chunk-ROMM6KMC.js +433 -0
- package/dist/chunk-SK6YVHFZ.js +211 -0
- package/dist/chunk-SM7RFQRO.js +214 -0
- package/dist/chunk-UFE77L67.js +782 -0
- package/dist/chunk-UGR4QIGS.js +67 -0
- package/dist/chunk-UUNHHX4M.js +13 -0
- package/dist/chunk-V44PGPA6.js +2104 -0
- package/dist/chunk-VL52PL2T.js +71 -0
- package/dist/chunk-VURYFIHW.js +201 -0
- package/dist/chunk-XJKJYIPP.js +7436 -0
- package/dist/chunk-YI6FF2ZR.js +340 -0
- package/dist/cli/index.js +37 -0
- package/dist/config-HZAAYDP3.js +10 -0
- package/dist/content-search-642F7E5V.js +593 -0
- package/dist/database-TCTZDLE5.js +9 -0
- package/dist/doctor-GXCKSJOX.js +395 -0
- package/dist/edit-engine-KM7MPFEC.js +51 -0
- package/dist/engine-42TZFNIZ.js +11 -0
- package/dist/engine-LO6L4JLQ.js +18 -0
- package/dist/fingerprint-XDXFRWO6.js +33 -0
- package/dist/framework-intelligence-DWFCKV44.js +860 -0
- package/dist/freshness-3K6VU5OT.js +174 -0
- package/dist/graph-engine-PL5YA7BA.js +21 -0
- package/dist/graph_request-MM4REP6H.js +350 -0
- package/dist/impact_request-WDE47IZ5.js +68 -0
- package/dist/index-space-IUUN5WYV.js +15 -0
- package/dist/index-status-6BD7UBHR.js +34 -0
- package/dist/index.js +159 -0
- package/dist/instance-FO6QZRLH.js +19 -0
- package/dist/language-detection-DFCAU5DZ.js +11 -0
- package/dist/lsp/server.js +4301 -0
- package/dist/mcp/server.js +132 -0
- package/dist/onboard-DIQZV2NW.js +161 -0
- package/dist/override-companion-pass-XR7MHTP7.js +117 -0
- package/dist/parallel-indexer-4PW3AFXP.js +88 -0
- package/dist/pool-worker.js +1153 -0
- package/dist/project-health-ZS5TS2Z6.js +94 -0
- package/dist/project-index-5SDPHITE.js +57 -0
- package/dist/project-registry-DGZBIZS7.js +355 -0
- package/dist/real-bash-analyzer-SXSV67E2.js +2201 -0
- package/dist/real-c-analyzer-SY7FTZ6W.js +5942 -0
- package/dist/real-clean-analyzer-IGDHHLZK.js +3470 -0
- package/dist/real-clojure-analyzer-4OH7IWLB.js +2818 -0
- package/dist/real-cpp-analyzer-7LTDRJRJ.js +4101 -0
- package/dist/real-csharp-analyzer-KICU3TLH.js +3298 -0
- package/dist/real-css-analyzer-HPU277Q6.js +15 -0
- package/dist/real-dart-analyzer-7LZ5LCMW.js +3944 -0
- package/dist/real-elixir-analyzer-BE3MEVZO.js +2192 -0
- package/dist/real-erlang-analyzer-BZMAKYBK.js +1731 -0
- package/dist/real-fsharp-analyzer-5QLTOV3E.js +2390 -0
- package/dist/real-go-analyzer-POD7MZQR.js +4126 -0
- package/dist/real-graphql-analyzer-MAQRZ6QW.js +1442 -0
- package/dist/real-groovy-analyzer-MEWSUPMJ.js +2285 -0
- package/dist/real-haskell-analyzer-I5L2GR3Y.js +2533 -0
- package/dist/real-hcl-analyzer-DBOPXSWJ.js +725 -0
- package/dist/real-html-analyzer-O63XZLZD.js +1759 -0
- package/dist/real-java-analyzer-CHCW3EKA.js +4677 -0
- package/dist/real-javascript-analyzer-QAAJQYZE.js +14 -0
- package/dist/real-json-analyzer-UGBR7YQN.js +1640 -0
- package/dist/real-julia-analyzer-35Q4GHOZ.js +2085 -0
- package/dist/real-kotlin-analyzer-GRAYCWW2.js +3956 -0
- package/dist/real-lua-analyzer-5BAPDZDF.js +1957 -0
- package/dist/real-makefile-analyzer-YJKAHNAS.js +920 -0
- package/dist/real-markdown-analyzer-UKUQZO4F.js +1215 -0
- package/dist/real-matlab-analyzer-FVIKQZKG.js +480 -0
- package/dist/real-nim-analyzer-M7FAIABG.js +3603 -0
- package/dist/real-objective-c-analyzer-XMYQIGQI.js +1518 -0
- package/dist/real-perl-analyzer-ZLY6M2DZ.js +1865 -0
- package/dist/real-php-analyzer-KI2ZAM7X.js +9368 -0
- package/dist/real-powershell-analyzer-B3T5ZP6U.js +2915 -0
- package/dist/real-protobuf-analyzer-23OZ75XV.js +867 -0
- package/dist/real-python-analyzer-I4TIUB6D.js +4880 -0
- package/dist/real-r-analyzer-FJZJXXS6.js +2596 -0
- package/dist/real-ruby-analyzer-M3UFIVTQ.js +2429 -0
- package/dist/real-rust-analyzer-LVSONPAN.js +3359 -0
- package/dist/real-scala-analyzer-CWPI33Q2.js +3293 -0
- package/dist/real-scss-analyzer-TGRJQ7TH.js +1515 -0
- package/dist/real-solidity-analyzer-4P2VBVWA.js +2608 -0
- package/dist/real-sql-analyzer-J7DYZOZR.js +3840 -0
- package/dist/real-svelte-analyzer-NHEN2SX5.js +2532 -0
- package/dist/real-swift-analyzer-2NSYULWI.js +2311 -0
- package/dist/real-toml-analyzer-L3C66L7Q.js +929 -0
- package/dist/real-typescript-analyzer-FTC6ETRQ.js +14 -0
- package/dist/real-vb-analyzer-ORY7LAQS.js +3145 -0
- package/dist/real-vba-analyzer-JH2TTUFI.js +466 -0
- package/dist/real-vue-analyzer-OO4FUGBV.js +2680 -0
- package/dist/real-xml-analyzer-6CQVR477.js +2405 -0
- package/dist/real-yaml-analyzer-J6MQG3YX.js +3528 -0
- package/dist/real-zig-analyzer-EFKCPJNU.js +2430 -0
- package/dist/regex-search-OPTLKUN5.js +239 -0
- package/dist/rules-I453JTRC.js +152 -0
- package/dist/scoped-path-MJYWJZQX.js +11 -0
- package/dist/setup-show-Y5GB5KXA.js +15 -0
- package/dist/setup-wizard-G5YYR5T6.js +24 -0
- package/dist/symbol-search-KB4MU4SI.js +16 -0
- package/dist/taint-YOTN5X55.js +202 -0
- package/dist/taint-analyzer-N7YLZU3N.js +163 -0
- package/dist/tool-handlers-G4NAGJIH.js +72 -0
- package/dist/tool-registry-BPEJNSZG.js +17 -0
- package/dist/tool-validation-RF675A3C.js +32 -0
- package/dist/type-inference-4Y7WTMFE.js +13 -0
- package/dist/vector-binary-LMY4QGAN.js +19 -0
- package/dist/version-LXGNFO37.js +9 -0
- package/dist/wasm-NMWO3WXY.js +16 -0
- package/docs/EMBEDDINGS.md +132 -0
- package/docs/README.md +178 -0
- package/docs/data/m10-baseline.json +602 -0
- package/docs/data/m10-benchmark-baseline.json +57 -0
- package/docs/data/m10-benchmark-last-run.json +127 -0
- package/docs/data/m10-final-audit.json +215 -0
- package/docs/data/m12-final-audit.json +138 -0
- package/docs/data/m12-surface-matrix.json +50 -0
- package/docs/data/public-tools.json +1902 -0
- package/docs/indexing/DEAD-ASSETS.md +88 -0
- package/docs/indexing/INDEXER-REVIEW.md +634 -0
- package/docs/indexing/PERFORMANCE-FINDINGS.md +106 -0
- package/docs/indexing/README.md +427 -0
- package/docs/indexing/discovery-matrix.md +41 -0
- package/docs/indexing/embeddings.md +325 -0
- package/docs/indexing/environment-variables.md +77 -0
- package/docs/indexing/performance.md +323 -0
- package/docs/indexing/resume.md +345 -0
- package/docs/indexing/troubleshooting.md +391 -0
- package/docs/man/bb.1 +190 -0
- package/docs/tools/bb_analyze.md +147 -0
- package/docs/tools/bb_annotate.md +110 -0
- package/docs/tools/bb_annotations.md +106 -0
- package/docs/tools/bb_ask.md +101 -0
- package/docs/tools/bb_cross_language.md +103 -0
- package/docs/tools/bb_doc.md +169 -0
- package/docs/tools/bb_edit.md +185 -0
- package/docs/tools/bb_graph.md +169 -0
- package/docs/tools/bb_health.md +156 -0
- package/docs/tools/bb_impact.md +117 -0
- package/docs/tools/bb_index.md +108 -0
- package/docs/tools/bb_migrate.md +135 -0
- package/docs/tools/bb_patterns.md +140 -0
- package/docs/tools/bb_search.md +150 -0
- package/docs/tools/bb_security.md +100 -0
- package/docs/tools/bb_visualize.md +184 -0
- package/grammars/README.md +57 -0
- package/grammars/THIRD-PARTY-NOTICES.md +50 -0
- package/grammars/tree-sitter-c/LICENSE +21 -0
- package/grammars/tree-sitter-clojure/LICENSE.md +21 -0
- package/grammars/tree-sitter-cpp/LICENSE +21 -0
- package/grammars/tree-sitter-csharp/LICENSE +21 -0
- package/grammars/tree-sitter-css/LICENSE +21 -0
- package/grammars/tree-sitter-css/README.md +21 -0
- package/grammars/tree-sitter-dart/LICENSE +20 -0
- package/grammars/tree-sitter-dart-upstream/LICENSE +20 -0
- package/grammars/tree-sitter-dart-upstream/README.md +18 -0
- package/grammars/tree-sitter-elixir/LICENSE +176 -0
- package/grammars/tree-sitter-elixir/README.md +11 -0
- package/grammars/tree-sitter-fsharp/LICENSE +21 -0
- package/grammars/tree-sitter-fsharp/README.md +45 -0
- package/grammars/tree-sitter-graphql/LICENSE +21 -0
- package/grammars/tree-sitter-graphql/README.md +106 -0
- package/grammars/tree-sitter-groovy/LICENSE +21 -0
- package/grammars/tree-sitter-groovy/README.md +4 -0
- package/grammars/tree-sitter-haskell/LICENSE +21 -0
- package/grammars/tree-sitter-haskell/README.md +394 -0
- package/grammars/tree-sitter-hcl/CHANGELOG.md +102 -0
- package/grammars/tree-sitter-hcl/LICENSE +201 -0
- package/grammars/tree-sitter-hcl/README.md +39 -0
- package/grammars/tree-sitter-html/LICENSE +21 -0
- package/grammars/tree-sitter-html/README.md +21 -0
- package/grammars/tree-sitter-julia/LICENSE +21 -0
- package/grammars/tree-sitter-julia/README.md +24 -0
- package/grammars/tree-sitter-kotlin/LICENSE +9 -0
- package/grammars/tree-sitter-lua/LICENSE.md +21 -0
- package/grammars/tree-sitter-lua/README.md +29 -0
- package/grammars/tree-sitter-make/LICENSE +21 -0
- package/grammars/tree-sitter-matlab/LICENSE +19 -0
- package/grammars/tree-sitter-matlab/README.md +99 -0
- package/grammars/tree-sitter-nim/LICENSE +373 -0
- package/grammars/tree-sitter-nim/README.md +46 -0
- package/grammars/tree-sitter-objc/LICENSE +21 -0
- package/grammars/tree-sitter-objc/README.md +20 -0
- package/grammars/tree-sitter-perl/LICENSE +7 -0
- package/grammars/tree-sitter-perl/README.md +100 -0
- package/grammars/tree-sitter-powershell/LICENSE +21 -0
- package/grammars/tree-sitter-powershell/README.md +7 -0
- package/grammars/tree-sitter-proto/LICENSE +21 -0
- package/grammars/tree-sitter-proto/README.md +105 -0
- package/grammars/tree-sitter-r/LICENSE +9 -0
- package/grammars/tree-sitter-r/README.md +66 -0
- package/grammars/tree-sitter-ruby/LICENSE +22 -0
- package/grammars/tree-sitter-scss/LICENSE +21 -0
- package/grammars/tree-sitter-scss/README.md +17 -0
- package/grammars/tree-sitter-solidity/LICENSE +7 -0
- package/grammars/tree-sitter-sql/LICENSE +21 -0
- package/grammars/tree-sitter-typescript/LICENSE +21 -0
- package/grammars/tree-sitter-vba/LICENSE +21 -0
- package/grammars/tree-sitter-vba/README.md +738 -0
- package/grammars/tree-sitter-vbnet/LICENSE +21 -0
- package/grammars/tree-sitter-vbnet/README.md +322 -0
- package/grammars/tree-sitter-xml/LICENSE +19 -0
- package/grammars/tree-sitter-yaml/LICENSE +20 -0
- package/grammars/tree-sitter-yaml/README.md +21 -0
- package/package.json +184 -4
- package/wasm/dart.wasm +0 -0
- package/wasm/tree-sitter-bash.wasm +0 -0
- package/wasm/tree-sitter-c.wasm +0 -0
- package/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/wasm/tree-sitter-clean.wasm +0 -0
- package/wasm/tree-sitter-clojure.wasm +0 -0
- package/wasm/tree-sitter-core_schema.wasm +0 -0
- package/wasm/tree-sitter-cpp.wasm +0 -0
- package/wasm/tree-sitter-css.wasm +0 -0
- package/wasm/tree-sitter-dart.wasm +0 -0
- package/wasm/tree-sitter-elisp.wasm +0 -0
- package/wasm/tree-sitter-elixir.wasm +0 -0
- package/wasm/tree-sitter-elm.wasm +0 -0
- package/wasm/tree-sitter-embedded_template.wasm +0 -0
- package/wasm/tree-sitter-erlang.wasm +0 -0
- package/wasm/tree-sitter-fsharp.wasm +0 -0
- package/wasm/tree-sitter-fsharp_signature.wasm +0 -0
- package/wasm/tree-sitter-go.wasm +0 -0
- package/wasm/tree-sitter-graphql.wasm +0 -0
- package/wasm/tree-sitter-groovy.wasm +0 -0
- package/wasm/tree-sitter-haskell.wasm +0 -0
- package/wasm/tree-sitter-hcl.wasm +0 -0
- package/wasm/tree-sitter-html.wasm +0 -0
- package/wasm/tree-sitter-java.wasm +0 -0
- package/wasm/tree-sitter-javascript.wasm +0 -0
- package/wasm/tree-sitter-json.wasm +0 -0
- package/wasm/tree-sitter-json_schema.wasm +0 -0
- package/wasm/tree-sitter-julia.wasm +0 -0
- package/wasm/tree-sitter-kotlin.wasm +0 -0
- package/wasm/tree-sitter-lua.wasm +0 -0
- package/wasm/tree-sitter-make.wasm +0 -0
- package/wasm/tree-sitter-markdown.wasm +0 -0
- package/wasm/tree-sitter-markdown_inline.wasm +0 -0
- package/wasm/tree-sitter-matlab.wasm +0 -0
- package/wasm/tree-sitter-nim.wasm +0 -0
- package/wasm/tree-sitter-objc.wasm +0 -0
- package/wasm/tree-sitter-ocaml.wasm +0 -0
- package/wasm/tree-sitter-perl.wasm +0 -0
- package/wasm/tree-sitter-php.wasm +0 -0
- package/wasm/tree-sitter-php_only.wasm +0 -0
- package/wasm/tree-sitter-powershell.wasm +0 -0
- package/wasm/tree-sitter-protobuf.wasm +0 -0
- package/wasm/tree-sitter-python.wasm +0 -0
- package/wasm/tree-sitter-ql.wasm +0 -0
- package/wasm/tree-sitter-r.wasm +0 -0
- package/wasm/tree-sitter-rescript.wasm +0 -0
- package/wasm/tree-sitter-ruby.wasm +0 -0
- package/wasm/tree-sitter-rust.wasm +0 -0
- package/wasm/tree-sitter-scala.wasm +0 -0
- package/wasm/tree-sitter-scss.wasm +0 -0
- package/wasm/tree-sitter-solidity.wasm +0 -0
- package/wasm/tree-sitter-sql.wasm +0 -0
- package/wasm/tree-sitter-svelte.wasm +0 -0
- package/wasm/tree-sitter-swift.wasm +0 -0
- package/wasm/tree-sitter-systemrdl.wasm +0 -0
- package/wasm/tree-sitter-terraform.wasm +0 -0
- package/wasm/tree-sitter-tlaplus.wasm +0 -0
- package/wasm/tree-sitter-toml.wasm +0 -0
- package/wasm/tree-sitter-tsx.wasm +0 -0
- package/wasm/tree-sitter-typescript.wasm +0 -0
- package/wasm/tree-sitter-vba.wasm +0 -0
- package/wasm/tree-sitter-vbnet.wasm +0 -0
- package/wasm/tree-sitter-vue.wasm +0 -0
- package/wasm/tree-sitter-xml.wasm +0 -0
- package/wasm/tree-sitter-yaml.wasm +0 -0
- package/wasm/tree-sitter-zig.wasm +0 -0
- package/wasm/tree-sitter.wasm +0 -0
- package/wasm/tsx.wasm +0 -0
- package/wasm/typescript.wasm +0 -0
|
@@ -0,0 +1,1759 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ASTEngine
|
|
3
|
+
} from "./chunk-V44PGPA6.js";
|
|
4
|
+
import "./chunk-BQ3OVQ3P.js";
|
|
5
|
+
import "./chunk-SK6YVHFZ.js";
|
|
6
|
+
import "./chunk-2BSKD7O3.js";
|
|
7
|
+
import "./chunk-KAET47ZL.js";
|
|
8
|
+
import {
|
|
9
|
+
EXTENSION_LANGUAGE
|
|
10
|
+
} from "./chunk-KCDECRJY.js";
|
|
11
|
+
import "./chunk-MLKGABMK.js";
|
|
12
|
+
|
|
13
|
+
// src/ast/real-html-analyzer.ts
|
|
14
|
+
import * as path from "path";
|
|
15
|
+
import { existsSync, statSync } from "fs";
|
|
16
|
+
import { createRequire } from "module";
|
|
17
|
+
var require2 = createRequire(import.meta.url);
|
|
18
|
+
function isHtmlSpace(code) {
|
|
19
|
+
return code === 32 || code === 9 || code === 10 || code === 13 || code === 12;
|
|
20
|
+
}
|
|
21
|
+
function asciiLower(text) {
|
|
22
|
+
for (let i = 0; i < text.length; i++) {
|
|
23
|
+
const c = text.charCodeAt(i);
|
|
24
|
+
if (c >= 65 && c <= 90) return text.slice(0, i) + text.slice(i).toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
return text;
|
|
27
|
+
}
|
|
28
|
+
var NAMED_CHARACTER_REFERENCES = /* @__PURE__ */ new Map([
|
|
29
|
+
["amp", "&"],
|
|
30
|
+
["AMP", "&"],
|
|
31
|
+
["lt", "<"],
|
|
32
|
+
["LT", "<"],
|
|
33
|
+
["gt", ">"],
|
|
34
|
+
["GT", ">"],
|
|
35
|
+
["quot", '"'],
|
|
36
|
+
["QUOT", '"'],
|
|
37
|
+
["apos", "'"],
|
|
38
|
+
["nbsp", "\xA0"],
|
|
39
|
+
["copy", "\xA9"],
|
|
40
|
+
["reg", "\xAE"],
|
|
41
|
+
["trade", "\u2122"],
|
|
42
|
+
["hellip", "\u2026"],
|
|
43
|
+
["mdash", "\u2014"],
|
|
44
|
+
["ndash", "\u2013"],
|
|
45
|
+
["lsquo", "\u2018"],
|
|
46
|
+
["rsquo", "\u2019"],
|
|
47
|
+
["ldquo", "\u201C"],
|
|
48
|
+
["rdquo", "\u201D"],
|
|
49
|
+
["bull", "\u2022"],
|
|
50
|
+
["middot", "\xB7"],
|
|
51
|
+
["deg", "\xB0"],
|
|
52
|
+
["plusmn", "\xB1"],
|
|
53
|
+
["times", "\xD7"],
|
|
54
|
+
["divide", "\xF7"],
|
|
55
|
+
["laquo", "\xAB"],
|
|
56
|
+
["raquo", "\xBB"],
|
|
57
|
+
["sect", "\xA7"],
|
|
58
|
+
["para", "\xB6"],
|
|
59
|
+
["euro", "\u20AC"],
|
|
60
|
+
["pound", "\xA3"],
|
|
61
|
+
["yen", "\xA5"],
|
|
62
|
+
["cent", "\xA2"],
|
|
63
|
+
["dagger", "\u2020"],
|
|
64
|
+
["Dagger", "\u2021"],
|
|
65
|
+
["permil", "\u2030"],
|
|
66
|
+
["larr", "\u2190"],
|
|
67
|
+
["rarr", "\u2192"],
|
|
68
|
+
["harr", "\u2194"],
|
|
69
|
+
["darr", "\u2193"],
|
|
70
|
+
["uarr", "\u2191"]
|
|
71
|
+
]);
|
|
72
|
+
function isReferenceDigits(text, hex) {
|
|
73
|
+
if (text.length === 0) return false;
|
|
74
|
+
for (let i = 0; i < text.length; i++) {
|
|
75
|
+
const c = text.charCodeAt(i);
|
|
76
|
+
if (c >= 48 && c <= 57) continue;
|
|
77
|
+
if (hex && (c >= 97 && c <= 102 || c >= 65 && c <= 70)) continue;
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
function decodeCharacterReferences(text) {
|
|
83
|
+
if (text.indexOf("&") === -1) return text;
|
|
84
|
+
let out = "";
|
|
85
|
+
let i = 0;
|
|
86
|
+
while (i < text.length) {
|
|
87
|
+
const amp = text.indexOf("&", i);
|
|
88
|
+
if (amp === -1) {
|
|
89
|
+
out += text.slice(i);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
out += text.slice(i, amp);
|
|
93
|
+
const semi = text.indexOf(";", amp + 1);
|
|
94
|
+
if (semi === -1 || semi - amp > 33) {
|
|
95
|
+
out += "&";
|
|
96
|
+
i = amp + 1;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const body = text.slice(amp + 1, semi);
|
|
100
|
+
if (body.length === 0) {
|
|
101
|
+
out += "&";
|
|
102
|
+
i = amp + 1;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (body.charCodeAt(0) === 35) {
|
|
106
|
+
const hex = body.charCodeAt(1) === 120 || body.charCodeAt(1) === 88 || body.charCodeAt(1) === 35 && (body.charCodeAt(2) === 120 || body.charCodeAt(2) === 88);
|
|
107
|
+
const digits = hex ? body.charCodeAt(1) === 35 ? body.slice(3) : body.slice(2) : body.slice(1);
|
|
108
|
+
if (digits.length > 0 && digits.length <= 8 && isReferenceDigits(digits, hex)) {
|
|
109
|
+
const code = hex ? Number.parseInt(digits, 16) : Number.parseInt(digits, 10);
|
|
110
|
+
if (code >= 0 && code <= 1114111 && !(code >= 55296 && code <= 57343)) {
|
|
111
|
+
out += String.fromCodePoint(code);
|
|
112
|
+
i = semi + 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
out += "&";
|
|
117
|
+
i = amp + 1;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const named = NAMED_CHARACTER_REFERENCES.get(body);
|
|
121
|
+
if (named !== void 0) {
|
|
122
|
+
out += named;
|
|
123
|
+
i = semi + 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
out += "&";
|
|
127
|
+
i = amp + 1;
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
function unquote(text) {
|
|
132
|
+
const n = text.length;
|
|
133
|
+
if (n >= 2) {
|
|
134
|
+
const first = text.charCodeAt(0);
|
|
135
|
+
const last = text.charCodeAt(n - 1);
|
|
136
|
+
if ((first === 34 || first === 39) && first === last) return text.slice(1, -1);
|
|
137
|
+
}
|
|
138
|
+
return text;
|
|
139
|
+
}
|
|
140
|
+
function urlScheme(value) {
|
|
141
|
+
const n = value.length;
|
|
142
|
+
for (let i = 0; i < n; i++) {
|
|
143
|
+
const c = value.charCodeAt(i);
|
|
144
|
+
if (c === 58) {
|
|
145
|
+
if (i === 0) return "";
|
|
146
|
+
const f = value.charCodeAt(0);
|
|
147
|
+
const alpha2 = f >= 65 && f <= 90 || f >= 97 && f <= 122;
|
|
148
|
+
return alpha2 ? asciiLower(value.slice(0, i)) : "";
|
|
149
|
+
}
|
|
150
|
+
const alpha = c >= 65 && c <= 90 || c >= 97 && c <= 122;
|
|
151
|
+
const digit = c >= 48 && c <= 57;
|
|
152
|
+
if (!alpha && !digit && c !== 43 && c !== 45 && c !== 46) return "";
|
|
153
|
+
}
|
|
154
|
+
return "";
|
|
155
|
+
}
|
|
156
|
+
function isNetworkScheme(value) {
|
|
157
|
+
const s = urlScheme(value);
|
|
158
|
+
return s === "http" || s === "https" || s === "ftp" || s === "ftps";
|
|
159
|
+
}
|
|
160
|
+
function isNonResourceScheme(value) {
|
|
161
|
+
const s = urlScheme(value);
|
|
162
|
+
if (s === "") return false;
|
|
163
|
+
return !isNetworkScheme(value);
|
|
164
|
+
}
|
|
165
|
+
function isProtocolRelative(value) {
|
|
166
|
+
return value.length >= 2 && value.charCodeAt(0) === 47 && value.charCodeAt(1) === 47;
|
|
167
|
+
}
|
|
168
|
+
function fragmentTarget(value) {
|
|
169
|
+
return value.length > 1 && value.charCodeAt(0) === 35 ? value.slice(1) : "";
|
|
170
|
+
}
|
|
171
|
+
function srcsetCandidates(value) {
|
|
172
|
+
const out = [];
|
|
173
|
+
const n = value.length;
|
|
174
|
+
let i = 0;
|
|
175
|
+
while (i < n) {
|
|
176
|
+
while (i < n && (isHtmlSpace(value.charCodeAt(i)) || value.charCodeAt(i) === 44)) i++;
|
|
177
|
+
if (i >= n) break;
|
|
178
|
+
const urlStart = i;
|
|
179
|
+
while (i < n && !isHtmlSpace(value.charCodeAt(i))) i++;
|
|
180
|
+
const url = value.slice(urlStart, i);
|
|
181
|
+
let trimmed = url;
|
|
182
|
+
while (trimmed.length > 0 && trimmed.charCodeAt(trimmed.length - 1) === 44) {
|
|
183
|
+
trimmed = trimmed.slice(0, -1);
|
|
184
|
+
i++;
|
|
185
|
+
}
|
|
186
|
+
if (trimmed.length > 0) out.push(trimmed);
|
|
187
|
+
let depth = 0;
|
|
188
|
+
while (i < n) {
|
|
189
|
+
const c = value.charCodeAt(i);
|
|
190
|
+
if (c === 40) depth++;
|
|
191
|
+
else if (c === 41 && depth > 0) depth--;
|
|
192
|
+
else if (c === 44 && depth === 0) {
|
|
193
|
+
i++;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
i++;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
function firstToken(text) {
|
|
202
|
+
const n = text.length;
|
|
203
|
+
let start = 0;
|
|
204
|
+
while (start < n && isHtmlSpace(text.charCodeAt(start))) start++;
|
|
205
|
+
if (start >= n) return "";
|
|
206
|
+
let end = start;
|
|
207
|
+
while (end < n && !isHtmlSpace(text.charCodeAt(end))) end++;
|
|
208
|
+
return text.slice(start, end);
|
|
209
|
+
}
|
|
210
|
+
function table(entries) {
|
|
211
|
+
return Object.assign(/* @__PURE__ */ Object.create(null), entries);
|
|
212
|
+
}
|
|
213
|
+
function map() {
|
|
214
|
+
return /* @__PURE__ */ Object.create(null);
|
|
215
|
+
}
|
|
216
|
+
var SEMANTIC_ELEMENTS = table({
|
|
217
|
+
"header": true,
|
|
218
|
+
"nav": true,
|
|
219
|
+
"main": true,
|
|
220
|
+
"section": true,
|
|
221
|
+
"article": true,
|
|
222
|
+
"aside": true,
|
|
223
|
+
"footer": true,
|
|
224
|
+
"h1": true,
|
|
225
|
+
"h2": true,
|
|
226
|
+
"h3": true,
|
|
227
|
+
"h4": true,
|
|
228
|
+
"h5": true,
|
|
229
|
+
"h6": true,
|
|
230
|
+
"figure": true,
|
|
231
|
+
"figcaption": true,
|
|
232
|
+
"time": true,
|
|
233
|
+
"data": true,
|
|
234
|
+
"mark": true,
|
|
235
|
+
"details": true,
|
|
236
|
+
"summary": true,
|
|
237
|
+
"dialog": true,
|
|
238
|
+
"meter": true,
|
|
239
|
+
"progress": true,
|
|
240
|
+
"search": true,
|
|
241
|
+
"address": true,
|
|
242
|
+
"hgroup": true
|
|
243
|
+
});
|
|
244
|
+
var INTERACTIVE_ELEMENTS = table({
|
|
245
|
+
"button": true,
|
|
246
|
+
"input": true,
|
|
247
|
+
"select": true,
|
|
248
|
+
"textarea": true,
|
|
249
|
+
"a": true,
|
|
250
|
+
"details": true,
|
|
251
|
+
"summary": true,
|
|
252
|
+
"dialog": true,
|
|
253
|
+
"menu": true,
|
|
254
|
+
"option": true,
|
|
255
|
+
"optgroup": true,
|
|
256
|
+
"label": true,
|
|
257
|
+
"fieldset": true,
|
|
258
|
+
"output": true,
|
|
259
|
+
"area": true
|
|
260
|
+
});
|
|
261
|
+
var FORM_ELEMENTS = table({
|
|
262
|
+
"form": true,
|
|
263
|
+
"input": true,
|
|
264
|
+
"select": true,
|
|
265
|
+
"textarea": true,
|
|
266
|
+
"button": true,
|
|
267
|
+
"fieldset": true,
|
|
268
|
+
"legend": true,
|
|
269
|
+
"label": true,
|
|
270
|
+
"datalist": true,
|
|
271
|
+
"optgroup": true,
|
|
272
|
+
"option": true,
|
|
273
|
+
"output": true,
|
|
274
|
+
"meter": true,
|
|
275
|
+
"progress": true
|
|
276
|
+
});
|
|
277
|
+
var TEMPLATE_BARRIERS = table({ "template": true, "slot": true });
|
|
278
|
+
var LINK_FETCHABLE_RELS = table({
|
|
279
|
+
"stylesheet": true,
|
|
280
|
+
"icon": true,
|
|
281
|
+
"shortcut icon": true,
|
|
282
|
+
"apple-touch-icon": true,
|
|
283
|
+
"apple-touch-icon-precomposed": true,
|
|
284
|
+
"mask-icon": true,
|
|
285
|
+
"manifest": true,
|
|
286
|
+
"preload": true,
|
|
287
|
+
"prefetch": true,
|
|
288
|
+
"preload-stylesheet": true,
|
|
289
|
+
"modulepreload": true,
|
|
290
|
+
"import": true,
|
|
291
|
+
"subresource": true,
|
|
292
|
+
"alternate": true,
|
|
293
|
+
"canonical": true
|
|
294
|
+
});
|
|
295
|
+
var LINK_RESOURCE_TYPES = table({
|
|
296
|
+
"stylesheet": "stylesheet",
|
|
297
|
+
"icon": "icon",
|
|
298
|
+
"shortcut icon": "icon",
|
|
299
|
+
"apple-touch-icon": "icon",
|
|
300
|
+
"apple-touch-icon-precomposed": "icon",
|
|
301
|
+
"mask-icon": "icon",
|
|
302
|
+
"manifest": "manifest",
|
|
303
|
+
"preload": "preload",
|
|
304
|
+
"modulepreload": "preload",
|
|
305
|
+
"prefetch": "prefetch",
|
|
306
|
+
"import": "import",
|
|
307
|
+
"alternate": "alternate",
|
|
308
|
+
"canonical": "canonical"
|
|
309
|
+
});
|
|
310
|
+
var PARAM_RESOURCE_NAMES = table({
|
|
311
|
+
"movie": true,
|
|
312
|
+
"src": true,
|
|
313
|
+
"url": true,
|
|
314
|
+
"codebase": true,
|
|
315
|
+
"archive": true
|
|
316
|
+
});
|
|
317
|
+
var CONDITIONAL_DIRECTIVE_SUFFIXES = ["if", "else", "elseif", "when", "unless", "show", "test"];
|
|
318
|
+
var REPEAT_DIRECTIVE_SUFFIXES = ["for", "each", "repeat"];
|
|
319
|
+
function spaceSeparatedUrls(value) {
|
|
320
|
+
const out = [];
|
|
321
|
+
let start = 0;
|
|
322
|
+
for (let i = 0; i <= value.length; i++) {
|
|
323
|
+
if (i !== value.length && !isHtmlSpace(value.charCodeAt(i))) continue;
|
|
324
|
+
if (i > start) out.push(value.slice(start, i));
|
|
325
|
+
start = i + 1;
|
|
326
|
+
}
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
329
|
+
function refreshContentUrl(content) {
|
|
330
|
+
const semi = content.indexOf(";");
|
|
331
|
+
if (semi === -1) return "";
|
|
332
|
+
if (!asciiLower(content.slice(semi + 1).trim()).startsWith("url=")) return "";
|
|
333
|
+
const inner = content.slice(semi + 1).trim().slice(4).trim();
|
|
334
|
+
if (inner.length < 2) return inner;
|
|
335
|
+
const first = inner[0];
|
|
336
|
+
const last = inner[inner.length - 1];
|
|
337
|
+
if ((first === '"' || first === "'") && first === last) return inner.slice(1, -1).trim();
|
|
338
|
+
return inner;
|
|
339
|
+
}
|
|
340
|
+
var RESOURCE_RULES = table({
|
|
341
|
+
script: [
|
|
342
|
+
{ attr: "src", resourceType: "script", import: true },
|
|
343
|
+
// SVG2 spells a script's source `href`, not `src`. Inside `<svg>` that is the
|
|
344
|
+
// ONLY spelling there is, so a table with `src` alone dropped every inline SVG
|
|
345
|
+
// script's dependency. The row is keyed on the tag, not the namespace, which
|
|
346
|
+
// is safe because an HTML `<script>` has no `href` to read — the lookup finds
|
|
347
|
+
// no attribute and emits nothing, rather than needing a namespace test the
|
|
348
|
+
// table has no way to express.
|
|
349
|
+
{ attr: "href", resourceType: "svg-script", import: true }
|
|
350
|
+
],
|
|
351
|
+
link: [
|
|
352
|
+
// `rel` decides both WHETHER the href is a document the page fetches and
|
|
353
|
+
// WHICH kind it is: `rel=stylesheet` is a stylesheet, `rel=icon` an icon,
|
|
354
|
+
// and `rel=preconnect`/`dns-prefetch` name a host for a connection warm-up
|
|
355
|
+
// rather than a subresource — the pre-fix analyzer mirrored those into
|
|
356
|
+
// `imports`, so the index claimed a dependency on a URL nothing serves.
|
|
357
|
+
{ attr: "href", resourceType: "link", import: true, relFilter: LINK_FETCHABLE_RELS }
|
|
358
|
+
],
|
|
359
|
+
img: [
|
|
360
|
+
{ attr: "src", resourceType: "img", import: true },
|
|
361
|
+
{ attr: "srcset", resourceType: "img-srcset", candidates: "srcset", import: true },
|
|
362
|
+
{ attr: "longdesc", resourceType: "img-longdesc", import: true }
|
|
363
|
+
],
|
|
364
|
+
// `<source>` carries BOTH a single `src` and a `srcset` candidate list, and
|
|
365
|
+
// the pre-fix analyzer read only `src` and only when the IMMEDIATE parent was
|
|
366
|
+
// picture/audio/video — so `<video><div><source src=…></div></video>` lost the
|
|
367
|
+
// resource. A candidate's own `media` attribute is the context test, and the
|
|
368
|
+
// ancestor test is now a bounded walk (see `isMediaContext`).
|
|
369
|
+
source: [
|
|
370
|
+
{ attr: "src", resourceType: "media-source", import: true },
|
|
371
|
+
{ attr: "srcset", resourceType: "img-srcset", candidates: "srcset", import: true }
|
|
372
|
+
],
|
|
373
|
+
video: [
|
|
374
|
+
{ attr: "src", resourceType: "video", import: true },
|
|
375
|
+
{ attr: "poster", resourceType: "video-poster", import: true }
|
|
376
|
+
],
|
|
377
|
+
audio: [{ attr: "src", resourceType: "audio", import: true }],
|
|
378
|
+
track: [{ attr: "src", resourceType: "track", import: true }],
|
|
379
|
+
embed: [{ attr: "src", resourceType: "embed", import: true }],
|
|
380
|
+
// `<object data>` names the resource itself; `<object archive>` names the
|
|
381
|
+
// ADDITIONAL resources needed to instantiate it, and the HTML specification
|
|
382
|
+
// defines it as a SPACE-separated list of URIs — the same shape as `<a ping>`
|
|
383
|
+
// and for the same reason (several per attribute, no descriptor, no comma).
|
|
384
|
+
//
|
|
385
|
+
// It was missing, and the carrier census found it rather than the ontology gate:
|
|
386
|
+
// `script/m14/verify-carrier-census.ts` reports `object@archive` as UNREVIEWED at
|
|
387
|
+
// 4 occurrences in 2 documents, and a carrier nobody has ruled is a carrier
|
|
388
|
+
// nobody has decided about. Measured on the tree over 1,699 corpus documents
|
|
389
|
+
// (aspnetcore/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/
|
|
390
|
+
// RazorWebSite.UrlResolution.Index.html and its .Encoded.html twin):
|
|
391
|
+
//
|
|
392
|
+
// object@archive 4 occurrences, 2 files — 3 real URLs, 1 Razor artifact
|
|
393
|
+
//
|
|
394
|
+
// That is a small population and the verdict does not pretend otherwise; the
|
|
395
|
+
// construct is real, it occurs, and the rule for it is one table row. It is a
|
|
396
|
+
// FETCH like `data`, so it takes the same shape and the same `import` mirror.
|
|
397
|
+
object: [
|
|
398
|
+
{ attr: "data", resourceType: "object", import: true },
|
|
399
|
+
{ attr: "archive", resourceType: "object-archive", import: true, valueList: "space" }
|
|
400
|
+
],
|
|
401
|
+
input: [{ attr: "src", resourceType: "input-image", import: true }],
|
|
402
|
+
iframe: [{ attr: "src", resourceType: "iframe", import: true }],
|
|
403
|
+
frame: [{ attr: "src", resourceType: "frame", import: true }],
|
|
404
|
+
// SVG reference carriers. `xlink:href` is the legacy spelling; both are read
|
|
405
|
+
// and the attribute map is already lowercased, so `XLINK:HREF` lands here too.
|
|
406
|
+
use: [
|
|
407
|
+
// `href="#icon"` is the ordinary SVG form and is a same-document reference;
|
|
408
|
+
// `href="sprite.svg#icon"` is an external file and a fetch. Both spellings of
|
|
409
|
+
// the attribute are read, and the attribute map is lowercased, so the legacy
|
|
410
|
+
// `XLINK:HREF` lands on the same rule.
|
|
411
|
+
{ attr: "href", resourceType: "svg-use", import: true, fragmentRef: true },
|
|
412
|
+
{ attr: "xlink:href", resourceType: "svg-use", import: true, fragmentRef: true }
|
|
413
|
+
],
|
|
414
|
+
image: [
|
|
415
|
+
{ attr: "href", resourceType: "svg-image", import: true, fragmentRef: true },
|
|
416
|
+
{ attr: "xlink:href", resourceType: "svg-image", import: true, fragmentRef: true }
|
|
417
|
+
],
|
|
418
|
+
param: [{ attr: "value", resourceType: "param", import: true, paramNameFilter: PARAM_RESOURCE_NAMES }],
|
|
419
|
+
a: [
|
|
420
|
+
{ attr: "href", resourceType: "navigation", link: true, navigation: true, fragmentRef: true },
|
|
421
|
+
// `ping` is a whitespace-separated list of URLs FETCHED when the link is
|
|
422
|
+
// activated. It is a fetch, not a navigation — the browser requests it; the
|
|
423
|
+
// user never goes there — so it takes the resource path and the `import`
|
|
424
|
+
// mirror, and the scheme contract filters an external endpoint.
|
|
425
|
+
{ attr: "ping", resourceType: "ping", import: true, valueList: "space" }
|
|
426
|
+
],
|
|
427
|
+
area: [{ attr: "href", resourceType: "navigation", link: true, navigation: true, fragmentRef: true }],
|
|
428
|
+
form: [{ attr: "action", resourceType: "form-action", link: true }],
|
|
429
|
+
button: [{ attr: "formaction", resourceType: "form-action", link: true }],
|
|
430
|
+
html: [{ attr: "manifest", resourceType: "manifest", import: true }],
|
|
431
|
+
meta: [
|
|
432
|
+
// The refresh PRAGMA. `content` is a URL only under `http-equiv=refresh`,
|
|
433
|
+
// and the gate is what keeps `charset`/`description`/viewport text from
|
|
434
|
+
// becoming an edge to a sentence. 40 occurrences across 26 files in one
|
|
435
|
+
// corpus, and entirely absent from the table until a carrier census looked
|
|
436
|
+
// for it rather than for what the table already knew.
|
|
437
|
+
{ attr: "content", resourceType: "meta-refresh", link: true, navigation: true, httpEquiv: "refresh", contentUrl: true }
|
|
438
|
+
],
|
|
439
|
+
// SVG filter reference. `feImage` pulls an image to paint INTO the filter
|
|
440
|
+
// result, which is a fetch like any other; the tag is lowercased by the
|
|
441
|
+
// walker, so the key is `feimage`.
|
|
442
|
+
feimage: [
|
|
443
|
+
{ attr: "href", resourceType: "svg-filter-image", import: true },
|
|
444
|
+
{ attr: "xlink:href", resourceType: "svg-filter-image", import: true }
|
|
445
|
+
],
|
|
446
|
+
// `cite` names where a quotation or a change came FROM. Nothing fetches it and
|
|
447
|
+
// nothing navigates to it, but it IS a written reference out of the document,
|
|
448
|
+
// so it is an edge — recorded as a usage-contextual reference rather than as
|
|
449
|
+
// an import, because grading a citation as a structural fetch overstates it.
|
|
450
|
+
// Four elements carry it, and all four are in the table rather than one, since
|
|
451
|
+
// the ruling is about the attribute's meaning and not about any single tag.
|
|
452
|
+
q: [{ attr: "cite", resourceType: "citation", fragmentRef: true, evidenceClass: "usage_contextual" }],
|
|
453
|
+
blockquote: [{ attr: "cite", resourceType: "citation", fragmentRef: true, evidenceClass: "usage_contextual" }],
|
|
454
|
+
del: [{ attr: "cite", resourceType: "citation", fragmentRef: true, evidenceClass: "usage_contextual" }],
|
|
455
|
+
ins: [{ attr: "cite", resourceType: "citation", fragmentRef: true, evidenceClass: "usage_contextual" }],
|
|
456
|
+
// Legacy presentational background images. Deprecated in HTML5 and still
|
|
457
|
+
// fetched by every shipping browser, so the dependency is real; the
|
|
458
|
+
// `legacy-` resourceType says so at the edge, which is the difference between
|
|
459
|
+
// a recorded fact and an implied one.
|
|
460
|
+
//
|
|
461
|
+
// `td`/`th` ARE THE SAME ATTRIBUTE ON THE SAME SPEC LIST, AND THIS TABLE WAS
|
|
462
|
+
// MISSING THEM (carrier census, 2026-10-28). The legacy `background`
|
|
463
|
+
// attribute is defined on `body`, `table`, `td` and `th`; the two cell
|
|
464
|
+
// elements are not a variant this analyzer invented, they are the other half
|
|
465
|
+
// of the same construct, so `body` and `table` without them described two of
|
|
466
|
+
// four owners. Measured consequence: the census found `td@background`
|
|
467
|
+
// carrying a real image path
|
|
468
|
+
// (`FSharp.Data/tests/FSharp.Data.Tests/Data/MarketDepth.htm`,
|
|
469
|
+
// `/bwx_drop_shadow_900_300.gif`) with no table claiming it, so an edge the
|
|
470
|
+
// browser really fetches was absent from the graph. `th` is added in the same
|
|
471
|
+
// commit rather than after a census reports it: the two cells differ in
|
|
472
|
+
// nothing the attribute is concerned with, and a ruling that names one and
|
|
473
|
+
// not the other is a ruling about the corpus that happened to run.
|
|
474
|
+
body: [{ attr: "background", resourceType: "legacy-background", import: true }],
|
|
475
|
+
table: [{ attr: "background", resourceType: "legacy-background", import: true }],
|
|
476
|
+
td: [{ attr: "background", resourceType: "legacy-background", import: true }],
|
|
477
|
+
th: [{ attr: "background", resourceType: "legacy-background", import: true }]
|
|
478
|
+
});
|
|
479
|
+
var FORM_ACTION_INPUT_TYPES = table({ submit: true, image: true });
|
|
480
|
+
var INPUT_FORMACTION_RULE = { attr: "formaction", resourceType: "form-action", link: true };
|
|
481
|
+
var HTML_DOCUMENT_EXTS = new Set(
|
|
482
|
+
Object.keys(EXTENSION_LANGUAGE).filter((ext) => EXTENSION_LANGUAGE[ext] === "html")
|
|
483
|
+
);
|
|
484
|
+
function resourceExtension(value) {
|
|
485
|
+
const clean = value.split("#")[0].split("?")[0];
|
|
486
|
+
const base = clean.slice(clean.lastIndexOf("/") + 1);
|
|
487
|
+
const dot = base.lastIndexOf(".");
|
|
488
|
+
return dot <= 0 ? "" : base.slice(dot + 1).toLowerCase();
|
|
489
|
+
}
|
|
490
|
+
function rebaseToProject(documentPath, projectRoot, value) {
|
|
491
|
+
const written = value.split("#")[0].split("?")[0];
|
|
492
|
+
if (written.length === 0) return null;
|
|
493
|
+
const abs = written.startsWith("/") ? path.resolve(projectRoot, written.slice(1)) : path.resolve(path.dirname(documentPath), written);
|
|
494
|
+
const rel = path.relative(projectRoot, abs);
|
|
495
|
+
if (rel.length === 0) return null;
|
|
496
|
+
const posix2 = rel.split(path.sep).join("/");
|
|
497
|
+
if (posix2.startsWith("..") || path.posix.isAbsolute(posix2)) return null;
|
|
498
|
+
return posix2;
|
|
499
|
+
}
|
|
500
|
+
var IDREF_RULES = [
|
|
501
|
+
{ tag: "", attr: "aria-labelledby", kind: "aria-labelledby", list: true },
|
|
502
|
+
{ tag: "", attr: "aria-describedby", kind: "aria-describedby", list: true },
|
|
503
|
+
{ tag: "", attr: "aria-details", kind: "aria-details", list: true },
|
|
504
|
+
{ tag: "", attr: "aria-errormessage", kind: "aria-errormessage", list: false },
|
|
505
|
+
{ tag: "", attr: "aria-controls", kind: "aria-controls", list: true },
|
|
506
|
+
{ tag: "", attr: "aria-owns", kind: "aria-owns", list: true },
|
|
507
|
+
{ tag: "", attr: "aria-flowto", kind: "aria-flowto", list: true },
|
|
508
|
+
{ tag: "", attr: "aria-activedescendant", kind: "aria-activedescendant", list: false },
|
|
509
|
+
{ tag: "td", attr: "headers", kind: "table-header", list: true },
|
|
510
|
+
{ tag: "th", attr: "headers", kind: "table-header", list: true },
|
|
511
|
+
// The form owner and the datalist an input offers. Both name an element in the
|
|
512
|
+
// same document through its `id`, which is why they are here and not in the
|
|
513
|
+
// resource table: nothing is fetched and nothing is navigated to. Both are
|
|
514
|
+
// SINGLE references, which is why they carry no `idrefList`.
|
|
515
|
+
{ tag: "", attr: "form", kind: "form-owner", list: false },
|
|
516
|
+
{ tag: "", attr: "list", kind: "datalist", list: false },
|
|
517
|
+
// `<output for>` names the controls whose value the output reports — a
|
|
518
|
+
// space-separated token list, like `headers`.
|
|
519
|
+
{ tag: "output", attr: "for", kind: "output-for", list: true }
|
|
520
|
+
];
|
|
521
|
+
var HEADING_ELEMENTS = table({ h1: true, h2: true, h3: true, h4: true, h5: true, h6: true });
|
|
522
|
+
var DELEGATE_FILE_ACTOR = "(file)";
|
|
523
|
+
var RealHtmlAnalyzer = class {
|
|
524
|
+
/**
|
|
525
|
+
* The embedded-block languages whose delegate analyzer could not be loaded in
|
|
526
|
+
* this process, populated only by `mergeEmbeddedBlocks` and published as
|
|
527
|
+
* `metadata.unavailableEmbeddedLangs`. Non-empty means: this document HAS inline
|
|
528
|
+
* blocks and they were NOT analysed. A field rather than a log line, because a
|
|
529
|
+
* consumer that asks "does this file contain code?" needs to be able to tell
|
|
530
|
+
* "no" from "not measured", and only a published field can say that.
|
|
531
|
+
*/
|
|
532
|
+
unavailableEmbeddedLangs = [];
|
|
533
|
+
/**
|
|
534
|
+
* THE DOCUMENT AND ITS PROJECT ROOT, for the two claim keys a resource edge can carry.
|
|
535
|
+
*
|
|
536
|
+
* Fields rather than two more parameters on `emitResource`, whose six call sites already
|
|
537
|
+
* thread eight arguments each: the values are properties OF THE ANALYSIS (`filePath` and
|
|
538
|
+
* `context.projectRoot`), they are identical for every emission in one document, and
|
|
539
|
+
* `analyze` is synchronous, so the pair is set once at the top of the walk and read by the
|
|
540
|
+
* helper below. Empty `claimProjectRoot` means the caller did not name a project root — the
|
|
541
|
+
* fixture runner does not — and a project-relative candidate is then NOT stated: an
|
|
542
|
+
* unrooted path would be a claim about a file list the caller never supplied.
|
|
543
|
+
*/
|
|
544
|
+
claimDocumentPath = "";
|
|
545
|
+
claimProjectRoot = "";
|
|
546
|
+
/**
|
|
547
|
+
* THE CLAIM A RESOURCE TARGET CAN CARRY — the reason the edge names something this run cannot
|
|
548
|
+
* resolve to a declaration, published on a key the ARBITER reads.
|
|
549
|
+
*
|
|
550
|
+
* THE DEFECT THIS CLOSES, in this lane's own measurement. `incoming-rule.ts` recognises a fixed
|
|
551
|
+
* list of keys (`EXTERNAL_KEYS`, `:56-80`) plus the file/source-path claim kinds, and the lane
|
|
552
|
+
* published `isExternalResource` on its network-URL fetches — a truthful marker on a spelling
|
|
553
|
+
* that is not in that list, so the arbiter read the edge as an unstated hole. `causeOK` was 0
|
|
554
|
+
* fleet-wide for exactly this reason. The fix is not to invent a new marker but to publish the
|
|
555
|
+
* claim in the vocabulary the arbiter already verifies:
|
|
556
|
+
*
|
|
557
|
+
* A NETWORK URL (`https:`, `http:`, `//cdn…`) is outside every admitted file list BY
|
|
558
|
+
* CONSTRUCTION, so the edge carries `sourcePathClaim`. Its polarity is inverted on purpose
|
|
559
|
+
* (`incoming-rule.ts:317-339`): the project REFUTES the claim the moment a candidate IS an
|
|
560
|
+
* admitted file, which is what stops this from becoming a laundering channel.
|
|
561
|
+
*
|
|
562
|
+
* AN HTML DOCUMENT that exists beside this one — `../index.html`, `page.html`, `/guide.html`
|
|
563
|
+
* — carries `importProjectFile`, the FILE claim, and it is the honest one: the project's own
|
|
564
|
+
* admitted list then CONFIRMS the file. MEASURED over the lane's bounded population: of
|
|
565
|
+
* 20,470 such targets, 20,295 name a document that is on disk AND admitted, and the other
|
|
566
|
+
* 175 are DEAD LINKS — nothing on disk — which is why existence is probed before the claim is
|
|
567
|
+
* published. Without that probe the claim would be published on 175 targets that name no file,
|
|
568
|
+
* and the arbiter would count 175 refutations against the lane: a false statement, not a
|
|
569
|
+
* better-looking table.
|
|
570
|
+
*
|
|
571
|
+
* ANYTHING ELSE PATH-SHAPED (`../../styles/main.css`, `/assets/x.js`) carries the same
|
|
572
|
+
* `sourcePathClaim`: the project admits no file answering the written string, which for an
|
|
573
|
+
* extension this lane cannot admit is true by construction.
|
|
574
|
+
*
|
|
575
|
+
* A BARE NAME GETS NOTHING (`details.html` with no file beside it, `main`, `getItem`). The
|
|
576
|
+
* source-path claim is defined only for a target containing `/` or a scheme
|
|
577
|
+
* (`incoming-rule.ts:335-337`), and a bare name is exactly the spelling that can also name a
|
|
578
|
+
* DECLARATION — so publishing a claim on it is the one move that could hide a resolution.
|
|
579
|
+
*
|
|
580
|
+
* EVERY KEY HERE IS ADDITIVE. The target is untouched, no existing metadata key is renamed or
|
|
581
|
+
* removed, and a claim is read by the arbiter only where the target names no declaration
|
|
582
|
+
* (`classifyEdge`), so no edge's resolution can move because of this function.
|
|
583
|
+
*/
|
|
584
|
+
resourceClaimMetadata(value, network) {
|
|
585
|
+
if (network) return { sourcePathClaim: [value] };
|
|
586
|
+
if (this.claimProjectRoot.length > 0 && HTML_DOCUMENT_EXTS.has(resourceExtension(value))) {
|
|
587
|
+
const rebased = rebaseToProject(this.claimDocumentPath, this.claimProjectRoot, value);
|
|
588
|
+
const absCandidate = rebased !== null ? path.join(this.claimProjectRoot, rebased) : null;
|
|
589
|
+
if (absCandidate !== null && existsSync(absCandidate)) {
|
|
590
|
+
const size = statSync(absCandidate).size;
|
|
591
|
+
if (size > 0 && size <= 1048576) return { importProjectFile: [rebased] };
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (urlScheme(value).length > 0) return { sourcePathClaim: [value] };
|
|
595
|
+
return {};
|
|
596
|
+
}
|
|
597
|
+
analyze(rootNode, content, filePath, context) {
|
|
598
|
+
const symbols = [];
|
|
599
|
+
const relationships = [];
|
|
600
|
+
const imports = [];
|
|
601
|
+
const exports = [];
|
|
602
|
+
const errors = [];
|
|
603
|
+
this.claimDocumentPath = filePath;
|
|
604
|
+
this.claimProjectRoot = context?.projectRoot ?? "";
|
|
605
|
+
const ERROR_REPORT_CAP = 50;
|
|
606
|
+
if (rootNode.hasError) {
|
|
607
|
+
const errStack = [rootNode];
|
|
608
|
+
while (errStack.length > 0) {
|
|
609
|
+
const n = errStack.pop();
|
|
610
|
+
if ((n.type === "ERROR" || n.isMissing) && errors.length < ERROR_REPORT_CAP) {
|
|
611
|
+
errors.push({
|
|
612
|
+
message: `Syntax ${n.type} detected. The Brain Engine is bypassing this broken node.`,
|
|
613
|
+
range: this.nodeToRange(n),
|
|
614
|
+
severity: "warning"
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
618
|
+
const child = n.namedChild(i);
|
|
619
|
+
if (child) errStack.push(child);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const moduleName = path.basename(filePath, path.extname(filePath)) || "document";
|
|
624
|
+
const documentRange = this.nodeToRange(rootNode);
|
|
625
|
+
const usedNames = map();
|
|
626
|
+
usedNames[moduleName] = 1;
|
|
627
|
+
symbols.push({
|
|
628
|
+
name: moduleName,
|
|
629
|
+
kind: ASTEngine.SymbolKind.Module,
|
|
630
|
+
range: documentRange,
|
|
631
|
+
scope: "",
|
|
632
|
+
visibility: "public",
|
|
633
|
+
complexity: 0,
|
|
634
|
+
// filled with the addressable-descendant count below
|
|
635
|
+
metadata: {
|
|
636
|
+
htmlSpecific: true,
|
|
637
|
+
isDocument: true,
|
|
638
|
+
doctype: this.findDoctype(rootNode),
|
|
639
|
+
lang: this.documentLanguage(rootNode)
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
const handleToSymbol = map();
|
|
643
|
+
const ambiguousHandles = map();
|
|
644
|
+
const deferred = [];
|
|
645
|
+
let totalElements = 0;
|
|
646
|
+
let semanticCount = 0;
|
|
647
|
+
let interactiveCount = 0;
|
|
648
|
+
let formElementCount = 0;
|
|
649
|
+
let ariaCount = 0;
|
|
650
|
+
let headingCount = 0;
|
|
651
|
+
let headingLevelSkips = 0;
|
|
652
|
+
let imgCount = 0;
|
|
653
|
+
let imgWithAltCount = 0;
|
|
654
|
+
let iframeWithoutLoadingCount = 0;
|
|
655
|
+
const counters = { externalLinkCount: 0, internalLinkCount: 0, labelWithoutFor: 0, inputWithoutLabel: 0 };
|
|
656
|
+
let baseHrefCount = 0;
|
|
657
|
+
let branchCount = 0;
|
|
658
|
+
let maxDepth = 0;
|
|
659
|
+
let addressableCount = 0;
|
|
660
|
+
let hasForm = false;
|
|
661
|
+
let hasExternals = false;
|
|
662
|
+
let customElementCount = 0;
|
|
663
|
+
let templateFragmentCount = 0;
|
|
664
|
+
let lastHeadingLevel = 0;
|
|
665
|
+
const frameworks = table({ HTML5: true });
|
|
666
|
+
const insights = map();
|
|
667
|
+
const stack = [{ node: rootNode, nearestNamed: moduleName, depth: 0 }];
|
|
668
|
+
while (stack.length > 0) {
|
|
669
|
+
const frame = stack.pop();
|
|
670
|
+
const node = frame.node;
|
|
671
|
+
if (node.type === "element" || node.type === "script_element" || node.type === "style_element") {
|
|
672
|
+
totalElements++;
|
|
673
|
+
if (frame.depth > maxDepth) maxDepth = frame.depth;
|
|
674
|
+
const tagNameNode = this.getElementTagName(node);
|
|
675
|
+
if (tagNameNode) {
|
|
676
|
+
const tagName = asciiLower(tagNameNode.text);
|
|
677
|
+
const { attrs, lines } = this.extractElementAttributes(node);
|
|
678
|
+
const startTag = this.getStartTag(node);
|
|
679
|
+
const tagLine = startTag ? startTag.startPosition.row + 1 : node.startPosition.row + 1;
|
|
680
|
+
const lineOf = (attr) => lines[attr] ?? tagLine;
|
|
681
|
+
const isSemantic = !!SEMANTIC_ELEMENTS[tagName];
|
|
682
|
+
const isInteractive = !!INTERACTIVE_ELEMENTS[tagName];
|
|
683
|
+
const isForm = !!FORM_ELEMENTS[tagName];
|
|
684
|
+
const isBarrier = !!TEMPLATE_BARRIERS[tagName];
|
|
685
|
+
if (isSemantic) semanticCount++;
|
|
686
|
+
if (isInteractive) interactiveCount++;
|
|
687
|
+
if (isForm) {
|
|
688
|
+
hasForm = true;
|
|
689
|
+
formElementCount++;
|
|
690
|
+
}
|
|
691
|
+
if (attrs["aria-label"] || attrs["role"] || attrs["aria-labelledby"] || attrs["aria-describedby"]) ariaCount++;
|
|
692
|
+
if (HEADING_ELEMENTS[tagName]) {
|
|
693
|
+
headingCount++;
|
|
694
|
+
const level = Number.parseInt(tagName.slice(1), 10);
|
|
695
|
+
if (lastHeadingLevel !== 0 && level > lastHeadingLevel + 1) headingLevelSkips += level - lastHeadingLevel - 1;
|
|
696
|
+
lastHeadingLevel = level;
|
|
697
|
+
}
|
|
698
|
+
if (tagName.indexOf("-") !== -1 && tagName !== "annotation-xml") {
|
|
699
|
+
customElementCount++;
|
|
700
|
+
insights["Custom-element-shaped tags present (registration is a cross-file fact this analyzer cannot see)"] = true;
|
|
701
|
+
}
|
|
702
|
+
if (this.hasDirective(attrs, CONDITIONAL_DIRECTIVE_SUFFIXES) || this.hasDirective(attrs, REPEAT_DIRECTIVE_SUFFIXES)) branchCount++;
|
|
703
|
+
if (isBarrier) branchCount++;
|
|
704
|
+
if (attrs["media"]) branchCount++;
|
|
705
|
+
if (tagName === "picture") branchCount++;
|
|
706
|
+
const rawId = attrs["id"] ?? "";
|
|
707
|
+
const rawName = rawId.length > 0 ? "" : attrs["name"] ?? "";
|
|
708
|
+
const handle = rawId.length > 0 ? rawId : rawName;
|
|
709
|
+
let currentId = frame.nearestNamed;
|
|
710
|
+
if (handle.length > 0) {
|
|
711
|
+
addressableCount++;
|
|
712
|
+
let symbolName = handle;
|
|
713
|
+
const seen = usedNames[handle];
|
|
714
|
+
if (seen === void 0) {
|
|
715
|
+
usedNames[handle] = 1;
|
|
716
|
+
} else {
|
|
717
|
+
let n = seen;
|
|
718
|
+
do {
|
|
719
|
+
n++;
|
|
720
|
+
symbolName = `${handle}#${n}`;
|
|
721
|
+
} while (usedNames[symbolName] !== void 0);
|
|
722
|
+
usedNames[symbolName] = 1;
|
|
723
|
+
usedNames[handle] = n;
|
|
724
|
+
}
|
|
725
|
+
if (handleToSymbol[handle] === void 0) {
|
|
726
|
+
handleToSymbol[handle] = symbolName;
|
|
727
|
+
} else {
|
|
728
|
+
ambiguousHandles[handle] = true;
|
|
729
|
+
}
|
|
730
|
+
currentId = symbolName;
|
|
731
|
+
symbols.push({
|
|
732
|
+
name: symbolName,
|
|
733
|
+
kind: ASTEngine.SymbolKind.Class,
|
|
734
|
+
range: this.nodeToRange(node),
|
|
735
|
+
scope: "",
|
|
736
|
+
// HTML has one scope: a document. There is no private/package/
|
|
737
|
+
// protected, so `public` is the only truthful value here and it is
|
|
738
|
+
// pinned by fixture rather than left as an unexamined default.
|
|
739
|
+
visibility: "public",
|
|
740
|
+
ownerName: frame.nearestNamed,
|
|
741
|
+
ownerKind: frame.nearestNamed === moduleName ? ASTEngine.SymbolKind.Module : ASTEngine.SymbolKind.Class,
|
|
742
|
+
complexity: 1,
|
|
743
|
+
metadata: {
|
|
744
|
+
tagName,
|
|
745
|
+
attributes: attrs,
|
|
746
|
+
htmlSpecific: true,
|
|
747
|
+
isAddressable: true,
|
|
748
|
+
identityAttribute: rawId.length > 0 ? "id" : "name",
|
|
749
|
+
isSemantic,
|
|
750
|
+
isInteractive,
|
|
751
|
+
isForm,
|
|
752
|
+
...rawId.length === 0 && rawName.length > 0 ? { alsoNamed: rawName } : {},
|
|
753
|
+
...symbolName !== handle ? { duplicateId: true, duplicateOf: handle } : {},
|
|
754
|
+
...frame.inBarrier ? { inTemplateBarrier: true } : {},
|
|
755
|
+
...attrs["is"] ? { isCustomElement: attrs["is"] } : {}
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
if (frame.nearestNamed !== symbolName) {
|
|
759
|
+
relationships.push({
|
|
760
|
+
source: frame.nearestNamed,
|
|
761
|
+
target: symbolName,
|
|
762
|
+
type: ASTEngine.RelationshipType.Contains,
|
|
763
|
+
confidence: 1,
|
|
764
|
+
line_number: this.nodeToRange(node).startLine,
|
|
765
|
+
metadata: {
|
|
766
|
+
htmlSpecific: true,
|
|
767
|
+
isDOMHierarchy: true,
|
|
768
|
+
evidenceClass: "structural",
|
|
769
|
+
...frame.nearestNamed === moduleName ? { isDocumentRoot: true } : {}
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
const rules = RESOURCE_RULES[tagName];
|
|
775
|
+
if (rules) {
|
|
776
|
+
for (let r = 0; r < rules.length; r++) {
|
|
777
|
+
const rule = rules[r];
|
|
778
|
+
const raw = attrs[rule.attr];
|
|
779
|
+
if (!raw || raw.length === 0) continue;
|
|
780
|
+
let effectiveType = rule.resourceType;
|
|
781
|
+
if (rule.relFilter) {
|
|
782
|
+
const rel = this.fetchableRelToken(attrs["rel"], rule.relFilter);
|
|
783
|
+
if (rel === null) continue;
|
|
784
|
+
effectiveType = LINK_RESOURCE_TYPES[rel] ?? rule.resourceType;
|
|
785
|
+
}
|
|
786
|
+
if (rule.paramNameFilter && !rule.paramNameFilter[asciiLower(attrs["name"] ?? "")]) continue;
|
|
787
|
+
if (rule.httpEquiv !== void 0 && asciiLower(attrs["http-equiv"] ?? "") !== rule.httpEquiv) continue;
|
|
788
|
+
const line = lineOf(rule.attr);
|
|
789
|
+
const owner = currentId;
|
|
790
|
+
const attrRange = this.attributeRange(startTag, rule.attr, node, line);
|
|
791
|
+
if (rule.contentUrl) {
|
|
792
|
+
const inner = refreshContentUrl(decodeCharacterReferences(raw).trim());
|
|
793
|
+
if (inner.length === 0) continue;
|
|
794
|
+
this.countOutcome(
|
|
795
|
+
this.emitResource(inner, owner, rule, line, attrRange, relationships, imports, deferred, true),
|
|
796
|
+
counters
|
|
797
|
+
);
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (rule.valueList === "space") {
|
|
801
|
+
const urls = spaceSeparatedUrls(decodeCharacterReferences(raw));
|
|
802
|
+
for (let u = 0; u < urls.length; u++) {
|
|
803
|
+
this.countOutcome(
|
|
804
|
+
this.emitResource(urls[u], owner, rule, line, attrRange, relationships, imports, deferred),
|
|
805
|
+
counters
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
if (rule.candidates === "srcset") {
|
|
811
|
+
const candidates = srcsetCandidates(raw);
|
|
812
|
+
for (let c = 0; c < candidates.length; c++) {
|
|
813
|
+
this.countOutcome(
|
|
814
|
+
this.emitResource(candidates[c], owner, rule, line, attrRange, relationships, imports, deferred),
|
|
815
|
+
counters
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
this.countOutcome(
|
|
821
|
+
this.emitResource(raw, owner, { ...rule, resourceType: effectiveType }, line, attrRange, relationships, imports, deferred),
|
|
822
|
+
counters
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (tagName === "input" && FORM_ACTION_INPUT_TYPES[asciiLower(attrs["type"] ?? "")]) {
|
|
827
|
+
const fa = attrs["formaction"];
|
|
828
|
+
if (fa && fa.length > 0) {
|
|
829
|
+
this.countOutcome(
|
|
830
|
+
this.emitResource(
|
|
831
|
+
fa,
|
|
832
|
+
currentId,
|
|
833
|
+
INPUT_FORMACTION_RULE,
|
|
834
|
+
lineOf("formaction"),
|
|
835
|
+
this.attributeRange(startTag, "formaction", node, lineOf("formaction")),
|
|
836
|
+
relationships,
|
|
837
|
+
imports,
|
|
838
|
+
deferred
|
|
839
|
+
),
|
|
840
|
+
counters
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
if (tagName === "label") {
|
|
845
|
+
const forAttr = attrs["for"];
|
|
846
|
+
if (forAttr && forAttr.length > 0) {
|
|
847
|
+
deferred.push({
|
|
848
|
+
// THE ELEMENT THAT WRITES THE REFERENCE, not the nearest named
|
|
849
|
+
// ancestor — `currentId` is the label's OWN handle when it is
|
|
850
|
+
// addressable (an `id`/`name`) and the ancestor only when it is not.
|
|
851
|
+
//
|
|
852
|
+
// This branch used `frame.nearestNamed` unconditionally, and it was
|
|
853
|
+
// the ONLY one of the four in-document reference emitters that did:
|
|
854
|
+
// the resource carriers pass `currentId` (`emitResource`'s caller),
|
|
855
|
+
// the IDREF class passes `currentId` (`owner`), the fragment href in
|
|
856
|
+
// `emitResource` passes its caller's `source`, and the lane's own F5
|
|
857
|
+
// oracle `deriveHtmlEdges` passes `next` — which is the element's own
|
|
858
|
+
// symbol name whenever it has one. MEASURED, both documents through
|
|
859
|
+
// `deriveHtmlEdges`:
|
|
860
|
+
//
|
|
861
|
+
// scope-barriers-and-references.html (labels `lbl-later`,
|
|
862
|
+
// `lbl-above`, `lbl-twin`, each with an id):
|
|
863
|
+
// oracle `uses lbl-later -> declared-below`; the analyzer said
|
|
864
|
+
// `uses app -> declared-below` — 3 rows, 100% of that file's
|
|
865
|
+
// label associations, sourced at the wrong actor.
|
|
866
|
+
// idref-carriers.html (`<label id="lbl" for="named-target">`):
|
|
867
|
+
// oracle `uses lbl -> named-target`; analyzer `uses app -> ...`.
|
|
868
|
+
//
|
|
869
|
+
// A label WITHOUT identity is unaffected: `currentId` is then the
|
|
870
|
+
// nearest named ancestor, which is what identity-addressable's
|
|
871
|
+
// `<label for="query">` (no id, inside `#search-form`) pins and what
|
|
872
|
+
// the oracle also derives (`uses search-form -> query`). So this
|
|
873
|
+
// changes the actor only where an actor was available to name, and it
|
|
874
|
+
// is what makes the analyzer agree with its own truth rather than
|
|
875
|
+
// pinning a divergence in two fixtures.
|
|
876
|
+
source: currentId,
|
|
877
|
+
handle: forAttr,
|
|
878
|
+
kind: "label",
|
|
879
|
+
rel: "uses",
|
|
880
|
+
line: lineOf("for")
|
|
881
|
+
});
|
|
882
|
+
} else {
|
|
883
|
+
counters.labelWithoutFor++;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
for (let ir = 0; ir < IDREF_RULES.length; ir++) {
|
|
887
|
+
const rule = IDREF_RULES[ir];
|
|
888
|
+
if (rule.tag.length > 0 && rule.tag !== tagName) continue;
|
|
889
|
+
const raw = attrs[rule.attr];
|
|
890
|
+
if (raw === void 0 || raw.length === 0) continue;
|
|
891
|
+
const line = lineOf(rule.attr);
|
|
892
|
+
const owner = currentId;
|
|
893
|
+
let start = 0;
|
|
894
|
+
for (let i = 0; i <= raw.length; i++) {
|
|
895
|
+
const atEnd = i === raw.length;
|
|
896
|
+
const c = atEnd ? 32 : raw.charCodeAt(i);
|
|
897
|
+
if (!(c === 32 || c === 9 || c === 10 || c === 13 || c === 12)) continue;
|
|
898
|
+
if (i > start) {
|
|
899
|
+
deferred.push({
|
|
900
|
+
source: owner,
|
|
901
|
+
handle: raw.slice(start, i),
|
|
902
|
+
kind: rule.kind,
|
|
903
|
+
rel: "references",
|
|
904
|
+
list: rule.list,
|
|
905
|
+
line
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
start = i + 1;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
if (tagName === "input") {
|
|
912
|
+
const type = asciiLower(attrs["type"] ?? "text");
|
|
913
|
+
if (type !== "hidden" && type !== "submit" && type !== "button" && type !== "reset" && type !== "image") {
|
|
914
|
+
counters.inputWithoutLabel++;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (tagName === "base" && attrs["href"]) baseHrefCount++;
|
|
918
|
+
if (tagName === "iframe" && !attrs["loading"]) iframeWithoutLoadingCount++;
|
|
919
|
+
if (tagName === "img") {
|
|
920
|
+
imgCount++;
|
|
921
|
+
if (attrs["alt"] !== void 0) imgWithAltCount++;
|
|
922
|
+
}
|
|
923
|
+
const nextBarrier = isBarrier;
|
|
924
|
+
const nextNamed = currentId;
|
|
925
|
+
if (isBarrier) {
|
|
926
|
+
templateFragmentCount++;
|
|
927
|
+
const children3 = node.namedChildren;
|
|
928
|
+
for (let i = children3.length - 1; i >= 0; i--) {
|
|
929
|
+
const child = children3[i];
|
|
930
|
+
if (child) {
|
|
931
|
+
stack.push({
|
|
932
|
+
node: child,
|
|
933
|
+
nearestNamed: nextNamed,
|
|
934
|
+
depth: frame.depth + 1,
|
|
935
|
+
inBarrier: true
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
const children2 = node.namedChildren;
|
|
942
|
+
for (let i = children2.length - 1; i >= 0; i--) {
|
|
943
|
+
const child = children2[i];
|
|
944
|
+
if (child) {
|
|
945
|
+
stack.push({ node: child, nearestNamed: nextNamed, depth: frame.depth + 1, inBarrier: frame.inBarrier });
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
const children = node.namedChildren;
|
|
952
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
953
|
+
const child = children[i];
|
|
954
|
+
if (child) {
|
|
955
|
+
stack.push({
|
|
956
|
+
node: child,
|
|
957
|
+
nearestNamed: frame.nearestNamed,
|
|
958
|
+
depth: frame.depth,
|
|
959
|
+
inBarrier: frame.inBarrier
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
for (let i = 0; i < deferred.length; i++) {
|
|
965
|
+
const ref = deferred[i];
|
|
966
|
+
const target = handleToSymbol[ref.handle];
|
|
967
|
+
if (target === void 0) continue;
|
|
968
|
+
if (ref.rel === "references") {
|
|
969
|
+
relationships.push({
|
|
970
|
+
source: ref.source,
|
|
971
|
+
target,
|
|
972
|
+
type: ASTEngine.RelationshipType.References,
|
|
973
|
+
confidence: 1,
|
|
974
|
+
line_number: ref.line,
|
|
975
|
+
metadata: {
|
|
976
|
+
htmlSpecific: true,
|
|
977
|
+
isIdref: true,
|
|
978
|
+
idrefKind: ref.kind,
|
|
979
|
+
// Published for the LIST carriers only, never as `idrefList: false`: a
|
|
980
|
+
// single-valued reference and a one-token list must not read alike, and
|
|
981
|
+
// the absent/present test is the only one that survives a consumer's
|
|
982
|
+
// truthiness check (`false` and `undefined` are both falsy).
|
|
983
|
+
...ref.list ? { idrefList: true } : {},
|
|
984
|
+
evidenceClass: "structural",
|
|
985
|
+
...ambiguousHandles[ref.handle] ? { ambiguousTarget: true } : {}
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
if (ref.kind === "label") {
|
|
991
|
+
relationships.push({
|
|
992
|
+
source: ref.source,
|
|
993
|
+
target,
|
|
994
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
995
|
+
confidence: 1,
|
|
996
|
+
line_number: ref.line,
|
|
997
|
+
metadata: {
|
|
998
|
+
htmlSpecific: true,
|
|
999
|
+
isLabelAssociation: true,
|
|
1000
|
+
evidenceClass: "structural",
|
|
1001
|
+
...ambiguousHandles[ref.handle] ? { ambiguousTarget: true } : {}
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
} else {
|
|
1005
|
+
relationships.push({
|
|
1006
|
+
source: ref.source,
|
|
1007
|
+
target,
|
|
1008
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
1009
|
+
confidence: 1,
|
|
1010
|
+
line_number: ref.line,
|
|
1011
|
+
metadata: {
|
|
1012
|
+
htmlSpecific: true,
|
|
1013
|
+
isInternalAnchor: true,
|
|
1014
|
+
evidenceClass: "structural",
|
|
1015
|
+
...ambiguousHandles[ref.handle] ? { ambiguousTarget: true } : {}
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
this.accumulateSubtreeSizes(symbols);
|
|
1021
|
+
const structural = this.computeScores({
|
|
1022
|
+
totalElements,
|
|
1023
|
+
semanticCount,
|
|
1024
|
+
interactiveCount,
|
|
1025
|
+
formElementCount,
|
|
1026
|
+
ariaCount,
|
|
1027
|
+
headingCount,
|
|
1028
|
+
headingLevelSkips,
|
|
1029
|
+
imgCount,
|
|
1030
|
+
imgWithAltCount,
|
|
1031
|
+
iframeWithoutLoadingCount,
|
|
1032
|
+
labelWithoutForCount: counters.labelWithoutFor,
|
|
1033
|
+
inputWithoutLabelCount: counters.inputWithoutLabel,
|
|
1034
|
+
branchCount,
|
|
1035
|
+
maxDepth,
|
|
1036
|
+
addressableCount,
|
|
1037
|
+
customElementCount,
|
|
1038
|
+
templateFragmentCount
|
|
1039
|
+
});
|
|
1040
|
+
if (addressableCount > 0) insights["Addressable DOM nodes identified by id/name"] = true;
|
|
1041
|
+
if (semanticCount > 0) insights["Well-structured semantic document"] = true;
|
|
1042
|
+
if (ariaCount > 0) insights["Commitment to accessibility detected"] = true;
|
|
1043
|
+
if (headingLevelSkips > 0) insights["Heading levels skip a level"] = true;
|
|
1044
|
+
if (hasExternals) {
|
|
1045
|
+
insights["External resources linked"] = true;
|
|
1046
|
+
let hasExternalScript = false;
|
|
1047
|
+
let hasExternalStyle = false;
|
|
1048
|
+
for (let i = 0; i < imports.length; i++) {
|
|
1049
|
+
if (imports[i].metadata?.isScript) hasExternalScript = true;
|
|
1050
|
+
if (imports[i].metadata?.isStylesheet) hasExternalStyle = true;
|
|
1051
|
+
}
|
|
1052
|
+
if (hasExternalScript) insights["External interactivity linked"] = true;
|
|
1053
|
+
if (hasExternalStyle) insights["External styling linked"] = true;
|
|
1054
|
+
}
|
|
1055
|
+
if (hasForm) insights["Contains user input forms"] = true;
|
|
1056
|
+
if (customElementCount > 0) insights["Custom-element-shaped tags present; registration not observable from this document"] = true;
|
|
1057
|
+
if (templateFragmentCount > 0) insights["Deferred template fragments present"] = true;
|
|
1058
|
+
if (baseHrefCount > 0) insights["Base URL rewrites relative resource resolution"] = true;
|
|
1059
|
+
if (counters.externalLinkCount > 0) insights["Outbound cross-origin navigation (recorded, not graphed)"] = true;
|
|
1060
|
+
const patterns = [];
|
|
1061
|
+
if (hasForm) patterns.push("Form Controls Usage");
|
|
1062
|
+
if (semanticCount > 0) patterns.push("Semantic HTML5 Structure");
|
|
1063
|
+
if (ariaCount > 0) patterns.push("ARIA Accessibility Enhancements");
|
|
1064
|
+
if (addressableCount > 0) patterns.push("Addressable DOM Identity");
|
|
1065
|
+
if (customElementCount > 0) patterns.push("Custom-element-shaped tag usage");
|
|
1066
|
+
const detectedFrameworks = [];
|
|
1067
|
+
for (const f in frameworks) detectedFrameworks.push(f);
|
|
1068
|
+
const architecturalInsights = [];
|
|
1069
|
+
for (const insight in insights) architecturalInsights.push(insight);
|
|
1070
|
+
if (context && Array.isArray(context.htmlEmbeddedTrees) && context.htmlEmbeddedTrees.length > 0) {
|
|
1071
|
+
const unavailable = this.mergeEmbeddedBlocks(context, moduleName, symbols, relationships, imports, exports, errors);
|
|
1072
|
+
if (unavailable.length > 0) this.unavailableEmbeddedLangs = [...new Set(unavailable)].sort();
|
|
1073
|
+
}
|
|
1074
|
+
return {
|
|
1075
|
+
language: "html",
|
|
1076
|
+
symbols,
|
|
1077
|
+
relationships,
|
|
1078
|
+
imports,
|
|
1079
|
+
exports,
|
|
1080
|
+
errors,
|
|
1081
|
+
metadata: {
|
|
1082
|
+
// Structural depth, not a symbol count: `symbols.length` was the pre-fix
|
|
1083
|
+
// value and it is not a complexity measure of anything.
|
|
1084
|
+
complexity: maxDepth + 1,
|
|
1085
|
+
linesOfCode: content.split("\n").length,
|
|
1086
|
+
cyclomaticComplexity: 1 + branchCount,
|
|
1087
|
+
elementCount: totalElements,
|
|
1088
|
+
addressableNodeCount: addressableCount,
|
|
1089
|
+
maxElementDepth: maxDepth,
|
|
1090
|
+
framework: "HTML5",
|
|
1091
|
+
patterns,
|
|
1092
|
+
detectedFrameworks,
|
|
1093
|
+
frameworkPatterns: [],
|
|
1094
|
+
architecturalInsights,
|
|
1095
|
+
architecturalScore: structural.architecturalScore,
|
|
1096
|
+
codeQuality: structural.architecturalScore > 80 ? "Excellent" : structural.architecturalScore > 60 ? "Good" : "Fair",
|
|
1097
|
+
hasTypeInference: false,
|
|
1098
|
+
hasFrameworkIntelligence: true,
|
|
1099
|
+
// The three scores are published under engine-forwarded keys AND as
|
|
1100
|
+
// metadata.score, so the numbers survive the engine's metadata literal
|
|
1101
|
+
// instead of being computed and dropped.
|
|
1102
|
+
accessibilityScore: structural.accessibility,
|
|
1103
|
+
semanticScore: structural.semantic,
|
|
1104
|
+
performanceScore: structural.performance,
|
|
1105
|
+
score: structural,
|
|
1106
|
+
analysisVersion: "m14-html-identity-v2",
|
|
1107
|
+
...this.unavailableEmbeddedLangs.length > 0 ? { unavailableEmbeddedLangs: [...this.unavailableEmbeddedLangs] } : {},
|
|
1108
|
+
documentMetrics: {
|
|
1109
|
+
totalElements,
|
|
1110
|
+
semanticCount,
|
|
1111
|
+
interactiveCount,
|
|
1112
|
+
formElementCount,
|
|
1113
|
+
ariaCount,
|
|
1114
|
+
headingCount,
|
|
1115
|
+
headingLevelSkips,
|
|
1116
|
+
imgCount,
|
|
1117
|
+
imgWithAltCount,
|
|
1118
|
+
iframeWithoutLoadingCount,
|
|
1119
|
+
labelWithoutForCount: counters.labelWithoutFor,
|
|
1120
|
+
inputWithoutLabelCount: counters.inputWithoutLabel,
|
|
1121
|
+
customElementCount,
|
|
1122
|
+
templateFragmentCount,
|
|
1123
|
+
baseHrefCount,
|
|
1124
|
+
// A hyperlink is not a code dependency, so outbound navigation is a
|
|
1125
|
+
// DOCUMENT METRIC rather than 200-odd `uses` edges into a graph whose
|
|
1126
|
+
// every edge is scored for architectural criticality.
|
|
1127
|
+
externalLinkCount: counters.externalLinkCount,
|
|
1128
|
+
internalLinkCount: counters.internalLinkCount
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
// ==========================================================================
|
|
1134
|
+
// RESOURCE EMISSION
|
|
1135
|
+
// ==========================================================================
|
|
1136
|
+
/**
|
|
1137
|
+
* One fetch/inline-resource edge, or one hyperlink, or nothing.
|
|
1138
|
+
*
|
|
1139
|
+
* Returns what it decided so the caller's document metrics stay exact without
|
|
1140
|
+
* this method needing a reference to them:
|
|
1141
|
+
* "fetch" a `uses` edge was emitted for a resource the document loads
|
|
1142
|
+
* "fragment" a same-document reference; the caller already deferred it
|
|
1143
|
+
* "navigation" an in-project hyperlink/endpoint, emitted as `uses`
|
|
1144
|
+
* "external" a cross-origin or non-resource-scheme URL: deliberately
|
|
1145
|
+
* NOT graphed (see the header) — the caller counts it
|
|
1146
|
+
* "script-url" a `javascript:` URL: a hazard, not a dependency
|
|
1147
|
+
* "none" nothing to say
|
|
1148
|
+
*
|
|
1149
|
+
* The decision table:
|
|
1150
|
+
* - a same-document fragment is NOT a fetch: it is a reference into the
|
|
1151
|
+
* document's own address space, resolved after the walk (deferred).
|
|
1152
|
+
* - a non-resource scheme (`mailto:`, `tel:`, `javascript:`, `data:`, …) is
|
|
1153
|
+
* not a dependency. Decoded BEFORE the scheme test (see the header), so
|
|
1154
|
+
* `javascript:` and `javascript:` cannot slip past — the
|
|
1155
|
+
* pre-fix filter was a bare `startsWith("javascript:")` and missed both,
|
|
1156
|
+
* AND existed only in the `<a>` branch, so `<form action="javascript:…">`
|
|
1157
|
+
* emitted a `calls` edge and an `imports` row for one.
|
|
1158
|
+
* - a network URL is a fetch, but its target is not a project module, so the
|
|
1159
|
+
* edge is `usage_contextual` and is NOT mirrored into `imports` (mirroring
|
|
1160
|
+
* it put `https://cdn.example.com/x.js` in brain_import, where every other
|
|
1161
|
+
* lane's import is a module the index can resolve).
|
|
1162
|
+
* - anything else is a project-relative module: `import_based`, and mirrored.
|
|
1163
|
+
*/
|
|
1164
|
+
emitResource(rawValue, source, rule, line, range, relationships, imports, deferred, alreadyDecoded = false) {
|
|
1165
|
+
const value = (alreadyDecoded ? rawValue : decodeCharacterReferences(rawValue)).trim();
|
|
1166
|
+
if (value.length === 0) return "none";
|
|
1167
|
+
const fragment = fragmentTarget(value);
|
|
1168
|
+
if (value === "#") return "none";
|
|
1169
|
+
if (fragment.length > 0) {
|
|
1170
|
+
if (rule.fragmentRef) {
|
|
1171
|
+
deferred.push({ source, handle: fragment, kind: "anchor", rel: "uses", line });
|
|
1172
|
+
return "fragment";
|
|
1173
|
+
}
|
|
1174
|
+
relationships.push({
|
|
1175
|
+
source,
|
|
1176
|
+
target: value,
|
|
1177
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
1178
|
+
confidence: 1,
|
|
1179
|
+
line_number: line,
|
|
1180
|
+
metadata: {
|
|
1181
|
+
htmlSpecific: true,
|
|
1182
|
+
resourceType: rule.resourceType,
|
|
1183
|
+
isInternalAnchor: true,
|
|
1184
|
+
isUnresolvedFragment: true,
|
|
1185
|
+
evidenceClass: "structural"
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
return "fragment";
|
|
1189
|
+
}
|
|
1190
|
+
if (isNonResourceScheme(value)) {
|
|
1191
|
+
if (urlScheme(value) === "javascript") {
|
|
1192
|
+
relationships.push({
|
|
1193
|
+
source,
|
|
1194
|
+
target: "javascript:",
|
|
1195
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
1196
|
+
confidence: 1,
|
|
1197
|
+
line_number: line,
|
|
1198
|
+
metadata: {
|
|
1199
|
+
htmlSpecific: true,
|
|
1200
|
+
resourceType: "javascript-url",
|
|
1201
|
+
isScriptUrl: true,
|
|
1202
|
+
evidenceClass: "usage_contextual"
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
return "script-url";
|
|
1206
|
+
}
|
|
1207
|
+
return "external";
|
|
1208
|
+
}
|
|
1209
|
+
const network = isProtocolRelative(value) || isNetworkScheme(value);
|
|
1210
|
+
if (rule.link) {
|
|
1211
|
+
if (network) return "external";
|
|
1212
|
+
relationships.push({
|
|
1213
|
+
source,
|
|
1214
|
+
target: value,
|
|
1215
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
1216
|
+
// 0.9, not 1.0: an observed hyperlink is not a derived fact. The tier
|
|
1217
|
+
// classifier in every other gated lane reads confidence, and a 1.0 here
|
|
1218
|
+
// would claim `structural` for a link that was merely written down.
|
|
1219
|
+
confidence: 0.9,
|
|
1220
|
+
line_number: line,
|
|
1221
|
+
// Honesty contract §4.1: a relationship below 1.0 must carry a
|
|
1222
|
+
// MACHINE-READABLE reason code, and the only justification this tier had
|
|
1223
|
+
// was the comment above. Measured over this lane's corpus (1,185
|
|
1224
|
+
// git-tracked documents, `validation/m14/probes/html/ontology-census.mts`):
|
|
1225
|
+
// 60,285 html-native edges sit at this tier — 59,953 `navigation` and 332
|
|
1226
|
+
// `form-action` — and not one carried a field a consumer or an audit could
|
|
1227
|
+
// read. The code makes the tier machine-checkable without re-reading the
|
|
1228
|
+
// analyzer: a consumer filtering "observed, not derived" reads a field.
|
|
1229
|
+
metadata: {
|
|
1230
|
+
htmlSpecific: true,
|
|
1231
|
+
resourceType: rule.resourceType,
|
|
1232
|
+
isNavigation: rule.navigation === true,
|
|
1233
|
+
// A form endpoint is a dependency on a backend route, which is a
|
|
1234
|
+
// materially different fact from "the user can click away".
|
|
1235
|
+
isFormEndpoint: rule.navigation !== true,
|
|
1236
|
+
evidenceClass: "usage_contextual",
|
|
1237
|
+
confidenceReason: "observed-hyperlink-not-derived",
|
|
1238
|
+
// THE CLAIM, on the channel the arbiter reads. A hyperlink is the largest
|
|
1239
|
+
// unresolved class this lane has (22,260 of 32,656 in the bounded
|
|
1240
|
+
// population) and it was emitted as a bare relative spelling, so the
|
|
1241
|
+
// document that wrote it was the only thing that could answer it.
|
|
1242
|
+
...this.resourceClaimMetadata(value, false)
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
return "navigation";
|
|
1246
|
+
}
|
|
1247
|
+
relationships.push({
|
|
1248
|
+
source,
|
|
1249
|
+
target: value,
|
|
1250
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
1251
|
+
confidence: 1,
|
|
1252
|
+
line_number: line,
|
|
1253
|
+
metadata: {
|
|
1254
|
+
htmlSpecific: true,
|
|
1255
|
+
resourceType: rule.resourceType,
|
|
1256
|
+
isExternalResource: network,
|
|
1257
|
+
evidenceClass: rule.evidenceClass ?? (network ? "usage_contextual" : "import_based"),
|
|
1258
|
+
// Same claim, same keys: a fetch names a FILE (`../styles/main.css`) or a
|
|
1259
|
+
// network URL, and both were published verbatim with no reason attached.
|
|
1260
|
+
...this.resourceClaimMetadata(value, network)
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
if (rule.import && !network) {
|
|
1264
|
+
imports.push({
|
|
1265
|
+
module: value,
|
|
1266
|
+
items: ["*"],
|
|
1267
|
+
isDefaultImport: true,
|
|
1268
|
+
isNamespaceImport: false,
|
|
1269
|
+
range,
|
|
1270
|
+
metadata: { isHtmlResource: true, resourceType: rule.resourceType }
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
return "fetch";
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Merge the sub-analysed inline blocks into the document's own payload.
|
|
1277
|
+
*
|
|
1278
|
+
* Each block is analysed by the JS/TS/CSS analyzer for its own grammar and the
|
|
1279
|
+
* result is re-based onto the document:
|
|
1280
|
+
* - a `line_number` and every metadata key whose NAME is a line move by
|
|
1281
|
+
* `startLine - 1` (the block's first line, 1-based, to file coordinates);
|
|
1282
|
+
* - a `range`'s startLine/endLine move by the same amount and its
|
|
1283
|
+
* startIndex/endIndex by the byte offset, because an index-based consumer
|
|
1284
|
+
* (bb_edit) is misaligned by lines alone;
|
|
1285
|
+
* - a multi-line block is indented, so the FIRST line needs no column shift
|
|
1286
|
+
* and the rest need none either — the block's text is copied verbatim from
|
|
1287
|
+
* the source, so its columns are already the document's;
|
|
1288
|
+
* - the owner of a delegate symbol with no owner is the DOCUMENT, because a
|
|
1289
|
+
* top-level `function` in an inline block is a document-scope declaration,
|
|
1290
|
+
* and `contains` then carries the same line the symbol does.
|
|
1291
|
+
*
|
|
1292
|
+
* Every shifted row is tagged `htmlEmbedded: true` with the block's language
|
|
1293
|
+
* and the offset applied. That tag is what makes the seam falsifiable: a
|
|
1294
|
+
* fixture can assert that an inline script's symbol is on the line it is
|
|
1295
|
+
* written on, and a mutant that drops the shift fails that pin instead of
|
|
1296
|
+
* silently shipping off-by-N lines.
|
|
1297
|
+
*/
|
|
1298
|
+
mergeEmbeddedBlocks(context, moduleName, symbols, relationships, imports, exports, errors) {
|
|
1299
|
+
const unavailable = [];
|
|
1300
|
+
const trees = context.htmlEmbeddedTrees ?? [];
|
|
1301
|
+
const langs = context.htmlEmbeddedLangs ?? [];
|
|
1302
|
+
const startLines = context.htmlEmbeddedStartLines ?? [];
|
|
1303
|
+
const startColumns = context.htmlEmbeddedStartColumns ?? [];
|
|
1304
|
+
const startIndices = context.htmlEmbeddedStartIndices ?? [];
|
|
1305
|
+
const delegates = map();
|
|
1306
|
+
const delegateFor = (lang) => {
|
|
1307
|
+
if (delegates[lang] !== void 0) return delegates[lang];
|
|
1308
|
+
let instance = null;
|
|
1309
|
+
try {
|
|
1310
|
+
const mod = lang === "css" ? require2("./real-css-analyzer") : lang === "ts" || lang === "tsx" ? require2("./real-typescript-analyzer") : require2("./real-javascript-analyzer");
|
|
1311
|
+
const cls = lang === "css" ? mod.RealCssAnalyzer : lang === "ts" || lang === "tsx" ? mod.RealTypeScriptAnalyzer : mod.RealJavaScriptAnalyzer;
|
|
1312
|
+
instance = typeof cls === "function" ? new cls() : null;
|
|
1313
|
+
} catch {
|
|
1314
|
+
try {
|
|
1315
|
+
const nodeRequire = createRequire(import.meta.url);
|
|
1316
|
+
const mod = lang === "css" ? nodeRequire("./real-css-analyzer") : lang === "ts" || lang === "tsx" ? nodeRequire("./real-typescript-analyzer") : nodeRequire("./real-javascript-analyzer");
|
|
1317
|
+
const cls = lang === "css" ? mod.RealCssAnalyzer : lang === "ts" || lang === "tsx" ? mod.RealTypeScriptAnalyzer : mod.RealJavaScriptAnalyzer;
|
|
1318
|
+
instance = typeof cls === "function" ? new cls() : null;
|
|
1319
|
+
} catch {
|
|
1320
|
+
instance = null;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
if (instance === null) unavailable.push(lang);
|
|
1324
|
+
delegates[lang] = instance;
|
|
1325
|
+
return instance;
|
|
1326
|
+
};
|
|
1327
|
+
for (let i = 0; i < trees.length; i++) {
|
|
1328
|
+
const tree = trees[i];
|
|
1329
|
+
if (!tree) continue;
|
|
1330
|
+
const lang = langs[i] ?? "js";
|
|
1331
|
+
const lineOffset = (startLines[i] ?? 1) - 1;
|
|
1332
|
+
const byteOffset = startIndices[i] ?? 0;
|
|
1333
|
+
const delegate = delegateFor(lang);
|
|
1334
|
+
if (delegate === null) continue;
|
|
1335
|
+
let sub;
|
|
1336
|
+
try {
|
|
1337
|
+
sub = delegate.analyze(tree.rootNode, "", moduleName);
|
|
1338
|
+
} catch {
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
for (const s of sub.symbols ?? []) {
|
|
1342
|
+
if (s.range) {
|
|
1343
|
+
s.range = {
|
|
1344
|
+
...s.range,
|
|
1345
|
+
startLine: s.range.startLine + lineOffset,
|
|
1346
|
+
endLine: s.range.endLine + lineOffset,
|
|
1347
|
+
startIndex: typeof s.range.startIndex === "number" ? s.range.startIndex + byteOffset : s.range.startIndex,
|
|
1348
|
+
endIndex: typeof s.range.endIndex === "number" ? s.range.endIndex + byteOffset : s.range.endIndex
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
if (!s.ownerName) {
|
|
1352
|
+
s.ownerName = moduleName;
|
|
1353
|
+
s.ownerKind = ASTEngine.SymbolKind.Module;
|
|
1354
|
+
s.scope = moduleName;
|
|
1355
|
+
}
|
|
1356
|
+
s.metadata = {
|
|
1357
|
+
...s.metadata ?? {},
|
|
1358
|
+
htmlEmbedded: true,
|
|
1359
|
+
embeddedLang: lang,
|
|
1360
|
+
embeddedLineOffset: lineOffset
|
|
1361
|
+
};
|
|
1362
|
+
symbols.push(s);
|
|
1363
|
+
if (s.ownerName && s.ownerName !== s.name && s.range?.startLine) {
|
|
1364
|
+
relationships.push({
|
|
1365
|
+
source: s.ownerName,
|
|
1366
|
+
target: s.name,
|
|
1367
|
+
type: ASTEngine.RelationshipType.Contains,
|
|
1368
|
+
confidence: 1,
|
|
1369
|
+
line_number: s.range.startLine,
|
|
1370
|
+
metadata: {
|
|
1371
|
+
htmlSpecific: true,
|
|
1372
|
+
isEmbeddedCode: true,
|
|
1373
|
+
// ORIGIN DISCLOSURE (2026-10-05, measured). This row was the ONE
|
|
1374
|
+
// merged row that carried no `htmlEmbedded` tag and no
|
|
1375
|
+
// `embeddedLineOffset`, while the symbols at :1585, the delegate
|
|
1376
|
+
// relationships at :1633 and the imports at :1652 all carried
|
|
1377
|
+
// both. Measured on vuejs through the production path: of 921
|
|
1378
|
+
// delegate rows, 676 carried `htmlEmbedded === true` and 245 did
|
|
1379
|
+
// not — and 245 is the seam-created `contains` class.
|
|
1380
|
+
//
|
|
1381
|
+
// The consequence is not cosmetic. The header of this method
|
|
1382
|
+
// claims "Every shifted row is tagged htmlEmbedded: true"; an
|
|
1383
|
+
// auditor who takes that as the origin test concludes these rows
|
|
1384
|
+
// are html's OWN, which is exactly the delegation question this
|
|
1385
|
+
// lane exists to answer honestly, and gets it backwards for a
|
|
1386
|
+
// quarter of the merged graph. html-origin-census.ts answers the
|
|
1387
|
+
// same question WITHOUT this tag — by differencing the
|
|
1388
|
+
// production path against the seam switched off — and the two
|
|
1389
|
+
// numbers have to agree or one of them is wrong.
|
|
1390
|
+
//
|
|
1391
|
+
// `htmlSpecific: true` on a delegate-created edge is also a lie of
|
|
1392
|
+
// the same shape: the row is not a fact about HTML's grammar, it
|
|
1393
|
+
// is a fact about the javascript inside a `<script>`. It stays,
|
|
1394
|
+
// because it is what tells a consumer the edge arrived through the
|
|
1395
|
+
// html module — but `htmlEmbedded` is what tells them WHERE it was
|
|
1396
|
+
// written, and that was missing.
|
|
1397
|
+
htmlEmbedded: true,
|
|
1398
|
+
embeddedLang: lang,
|
|
1399
|
+
embeddedLineOffset: lineOffset,
|
|
1400
|
+
evidenceClass: "structural"
|
|
1401
|
+
}
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
const delegateBases = /* @__PURE__ */ new Map();
|
|
1406
|
+
for (const r of sub.relationships ?? []) {
|
|
1407
|
+
if (r?.type !== ASTEngine.RelationshipType.Extends) continue;
|
|
1408
|
+
if (typeof r.source === "string" && r.source !== "" && typeof r.target === "string" && r.target !== "") {
|
|
1409
|
+
delegateBases.set(r.source, r.target);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
for (const r of sub.relationships ?? []) {
|
|
1413
|
+
if (r.type === ASTEngine.RelationshipType.Imports) {
|
|
1414
|
+
const mod = r.metadata?.module;
|
|
1415
|
+
if (typeof mod === "string" && mod !== "" && typeof r.target === "string" && r.target !== "" && !r.target.includes(":") && !r.target.includes("/")) {
|
|
1416
|
+
r.target = `${mod}:${r.target}`;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
if (r.type === ASTEngine.RelationshipType.Overrides) {
|
|
1420
|
+
if (typeof r.source === "string" && typeof r.target === "string" && r.target !== "" && !r.target.includes(".")) {
|
|
1421
|
+
const owner = r.source.slice(0, r.source.lastIndexOf("."));
|
|
1422
|
+
const base = owner === "" ? void 0 : delegateBases.get(owner);
|
|
1423
|
+
if (base) r.target = `${base}.${r.target}`;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
if (typeof r.line_number === "number") r.line_number += lineOffset;
|
|
1427
|
+
if (r.metadata) {
|
|
1428
|
+
for (const k of Object.keys(r.metadata)) {
|
|
1429
|
+
if (!asciiLower(k).includes("line")) continue;
|
|
1430
|
+
const v = r.metadata[k];
|
|
1431
|
+
if (typeof v === "number") r.metadata[k] = v + lineOffset;
|
|
1432
|
+
else if (Array.isArray(v) && v.some((x) => typeof x === "number")) {
|
|
1433
|
+
;
|
|
1434
|
+
r.metadata[k] = v.map((x) => typeof x === "number" ? x + lineOffset : x);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
if (r.source === DELEGATE_FILE_ACTOR) r.source = moduleName;
|
|
1439
|
+
r.metadata = {
|
|
1440
|
+
...r.metadata ?? {},
|
|
1441
|
+
htmlEmbedded: true,
|
|
1442
|
+
embeddedLang: lang,
|
|
1443
|
+
embeddedLineOffset: lineOffset
|
|
1444
|
+
};
|
|
1445
|
+
relationships.push(r);
|
|
1446
|
+
}
|
|
1447
|
+
for (const imp of sub.imports ?? []) {
|
|
1448
|
+
if (imp.range) {
|
|
1449
|
+
imp.range = {
|
|
1450
|
+
...imp.range,
|
|
1451
|
+
startLine: imp.range.startLine + lineOffset,
|
|
1452
|
+
endLine: imp.range.endLine + lineOffset,
|
|
1453
|
+
startIndex: typeof imp.range.startIndex === "number" ? imp.range.startIndex + byteOffset : imp.range.startIndex,
|
|
1454
|
+
endIndex: typeof imp.range.endIndex === "number" ? imp.range.endIndex + byteOffset : imp.range.endIndex
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
imp.metadata = { ...imp.metadata ?? {}, htmlEmbedded: true, embeddedLang: lang };
|
|
1458
|
+
imports.push(imp);
|
|
1459
|
+
}
|
|
1460
|
+
for (const e of sub.exports ?? []) {
|
|
1461
|
+
if (e.range) e.range = { ...e.range, startLine: e.range.startLine + lineOffset, endLine: e.range.endLine + lineOffset };
|
|
1462
|
+
if (e.range) {
|
|
1463
|
+
e.range = {
|
|
1464
|
+
...e.range,
|
|
1465
|
+
startLine: e.range.startLine + lineOffset,
|
|
1466
|
+
endLine: e.range.endLine + lineOffset,
|
|
1467
|
+
startIndex: typeof e.range.startIndex === "number" ? e.range.startIndex + byteOffset : e.range.startIndex,
|
|
1468
|
+
endIndex: typeof e.range.endIndex === "number" ? e.range.endIndex + byteOffset : e.range.endIndex
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
e.metadata = { ...e.metadata ?? {}, htmlEmbedded: true, embeddedLang: lang, embeddedLineOffset: lineOffset };
|
|
1472
|
+
exports.push(e);
|
|
1473
|
+
}
|
|
1474
|
+
for (const er of sub.errors ?? []) {
|
|
1475
|
+
if (er.range) {
|
|
1476
|
+
er.range = {
|
|
1477
|
+
...er.range,
|
|
1478
|
+
startLine: er.range.startLine + lineOffset,
|
|
1479
|
+
endLine: er.range.endLine + lineOffset,
|
|
1480
|
+
startIndex: typeof er.range.startIndex === "number" ? er.range.startIndex + byteOffset : er.range.startIndex,
|
|
1481
|
+
endIndex: typeof er.range.endIndex === "number" ? er.range.endIndex + byteOffset : er.range.endIndex
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
er.metadata = { ...er.metadata ?? {}, htmlEmbedded: true, embeddedLang: lang, embeddedLineOffset: lineOffset };
|
|
1485
|
+
errors.push(er);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
return unavailable;
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Fold one emission decision into the document metrics. O(1).
|
|
1492
|
+
*
|
|
1493
|
+
* A hyperlink that left the project is COUNTED and not graphed: the pre-fix
|
|
1494
|
+
* analyzer emitted a `uses` edge for every one, so a page linking to fifty
|
|
1495
|
+
* external sites published fifty graph edges that the enrichment pass then
|
|
1496
|
+
* scored for architectural criticality, and the differential could not tell a
|
|
1497
|
+
* CDN dependency from a hyperlink. The count survives in
|
|
1498
|
+
* `documentMetrics.externalLinkCount`, so the information is not lost — it is
|
|
1499
|
+
* where it belongs.
|
|
1500
|
+
*/
|
|
1501
|
+
countOutcome(outcome, counters) {
|
|
1502
|
+
if (outcome === "external") counters.externalLinkCount++;
|
|
1503
|
+
else if (outcome === "navigation") counters.internalLinkCount++;
|
|
1504
|
+
}
|
|
1505
|
+
// ==========================================================================
|
|
1506
|
+
// NODE / ATTRIBUTE ACCESS — index cursors, no array mapping, no regex
|
|
1507
|
+
// ==========================================================================
|
|
1508
|
+
/** The element's start_tag or self_closing_tag, or null. */
|
|
1509
|
+
getStartTag(node) {
|
|
1510
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1511
|
+
const child = node.child(i);
|
|
1512
|
+
if (child?.type === "start_tag" || child?.type === "self_closing_tag") return child;
|
|
1513
|
+
}
|
|
1514
|
+
return null;
|
|
1515
|
+
}
|
|
1516
|
+
getElementTagName(node) {
|
|
1517
|
+
const startTag = this.getStartTag(node);
|
|
1518
|
+
if (!startTag) return null;
|
|
1519
|
+
for (let j = 0; j < startTag.childCount; j++) {
|
|
1520
|
+
const grandChild = startTag.child(j);
|
|
1521
|
+
if (grandChild?.type === "tag_name") return grandChild;
|
|
1522
|
+
}
|
|
1523
|
+
return null;
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Attribute map, LOWERCASED KEYS, plus the line each attribute is written on.
|
|
1527
|
+
*
|
|
1528
|
+
* The lowercasing is the whole point and it is the one-line defect that cost
|
|
1529
|
+
* this analyzer its entire graph on any document written in mixed case: the tag
|
|
1530
|
+
* name was lowercased and the attribute name was not, so `<IMG SRC=…>` matched
|
|
1531
|
+
* no rule and produced nothing. HTML attribute names are ASCII
|
|
1532
|
+
* case-INSENSITIVE; the grammar hands them over verbatim.
|
|
1533
|
+
*/
|
|
1534
|
+
extractElementAttributes(node) {
|
|
1535
|
+
const attrs = map();
|
|
1536
|
+
const lines = map();
|
|
1537
|
+
const startTag = this.getStartTag(node);
|
|
1538
|
+
if (!startTag) return { attrs, lines };
|
|
1539
|
+
for (let j = 0; j < startTag.childCount; j++) {
|
|
1540
|
+
const attribute = startTag.child(j);
|
|
1541
|
+
if (attribute?.type !== "attribute") continue;
|
|
1542
|
+
let attrName = "";
|
|
1543
|
+
let attrValue = "";
|
|
1544
|
+
let attrLine = attribute.startPosition.row + 1;
|
|
1545
|
+
for (let k = 0; k < attribute.childCount; k++) {
|
|
1546
|
+
const attrChild = attribute.child(k);
|
|
1547
|
+
const t = attrChild?.type;
|
|
1548
|
+
if (t === "attribute_name") {
|
|
1549
|
+
attrName = attrChild.text;
|
|
1550
|
+
attrLine = attrChild.startPosition.row + 1;
|
|
1551
|
+
} else if (t === "quoted_attribute_value" || t === "attribute_value") {
|
|
1552
|
+
const inner = this.findChildByType(attrChild, "attribute_value");
|
|
1553
|
+
attrValue = inner ? inner.text : unquote(attrChild.text);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
if (!attrName) continue;
|
|
1557
|
+
const key = asciiLower(attrName);
|
|
1558
|
+
if (attrs[key] === void 0) {
|
|
1559
|
+
attrs[key] = attrValue;
|
|
1560
|
+
lines[key] = attrLine;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
return { attrs, lines };
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* The range of ONE attribute of a start_tag, falling back to the element's own
|
|
1567
|
+
* range when the grammar gave us no attribute node for it. O(A) over the
|
|
1568
|
+
* start_tag's children, A = the tag's attribute count.
|
|
1569
|
+
*/
|
|
1570
|
+
attributeRange(startTag, attrName, element, line) {
|
|
1571
|
+
if (startTag) {
|
|
1572
|
+
for (let j = 0; j < startTag.childCount; j++) {
|
|
1573
|
+
const attribute = startTag.child(j);
|
|
1574
|
+
if (attribute?.type !== "attribute") continue;
|
|
1575
|
+
for (let k = 0; k < attribute.childCount; k++) {
|
|
1576
|
+
const an = attribute.child(k);
|
|
1577
|
+
if (an?.type === "attribute_name" && asciiLower(an.text) === attrName) {
|
|
1578
|
+
return {
|
|
1579
|
+
startLine: line,
|
|
1580
|
+
startColumn: an.startPosition.column + 1,
|
|
1581
|
+
endLine: an.endPosition.row + 1,
|
|
1582
|
+
endColumn: an.endPosition.column + 1,
|
|
1583
|
+
startIndex: attribute.startIndex,
|
|
1584
|
+
endIndex: attribute.endIndex
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
const r = this.nodeToRange(element);
|
|
1591
|
+
r.startLine = line;
|
|
1592
|
+
return r;
|
|
1593
|
+
}
|
|
1594
|
+
findChildByType(node, type) {
|
|
1595
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1596
|
+
const c = node.namedChild(i);
|
|
1597
|
+
if (c && c.type === type) return c;
|
|
1598
|
+
}
|
|
1599
|
+
return null;
|
|
1600
|
+
}
|
|
1601
|
+
/** A directive attribute whose NAME ends with one of `suffixes` — the shape
|
|
1602
|
+
* `v-if` / `ng-repeat` / `:show` / `x-if` all take. Pure-String, O(1) per attr. */
|
|
1603
|
+
hasDirective(attrs, suffixes) {
|
|
1604
|
+
for (const key in attrs) {
|
|
1605
|
+
for (let s = 0; s < suffixes.length; s++) {
|
|
1606
|
+
const suffix = suffixes[s];
|
|
1607
|
+
if (key === suffix) return true;
|
|
1608
|
+
if (key.length > suffix.length + 1 && key.endsWith(suffix) && key.charCodeAt(key.length - suffix.length - 1) === 45) {
|
|
1609
|
+
return true;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
return false;
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* The first FETCHABLE token of a `<link rel>` list, or null.
|
|
1617
|
+
*
|
|
1618
|
+
* `rel` is a space-separated token list, not one value: the ubiquitous
|
|
1619
|
+
* `rel="shortcut icon"` is two tokens, and `rel="preconnect stylesheet"` is a
|
|
1620
|
+
* hint plus a real document. Returning the token (rather than a boolean) is
|
|
1621
|
+
* what lets the edge say WHICH kind of link it is.
|
|
1622
|
+
*/
|
|
1623
|
+
fetchableRelToken(rel, filter) {
|
|
1624
|
+
if (rel === void 0 || rel.length === 0) return null;
|
|
1625
|
+
const lowered = asciiLower(rel);
|
|
1626
|
+
const n = lowered.length;
|
|
1627
|
+
let start = 0;
|
|
1628
|
+
while (start < n) {
|
|
1629
|
+
while (start < n && isHtmlSpace(lowered.charCodeAt(start))) start++;
|
|
1630
|
+
let end = start;
|
|
1631
|
+
while (end < n && !isHtmlSpace(lowered.charCodeAt(end))) end++;
|
|
1632
|
+
if (end > start) {
|
|
1633
|
+
const token = lowered.slice(start, end);
|
|
1634
|
+
if (filter[token]) return token;
|
|
1635
|
+
}
|
|
1636
|
+
start = end;
|
|
1637
|
+
}
|
|
1638
|
+
return null;
|
|
1639
|
+
}
|
|
1640
|
+
findDoctype(node) {
|
|
1641
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1642
|
+
const c = node.namedChild(i);
|
|
1643
|
+
if (c && c.type === "doctype") {
|
|
1644
|
+
for (let j = 0; j < c.childCount; j++) {
|
|
1645
|
+
const d = c.child(j);
|
|
1646
|
+
if (d && d.type !== "<!" && d.type !== ">" && d.text.trim().length > 0) {
|
|
1647
|
+
return firstToken(d.text);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
return "html";
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
return null;
|
|
1654
|
+
}
|
|
1655
|
+
documentLanguage(node) {
|
|
1656
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1657
|
+
const c = node.namedChild(i);
|
|
1658
|
+
if (c && c.type === "element") {
|
|
1659
|
+
for (let j = 0; j < c.childCount; j++) {
|
|
1660
|
+
const startTag = c.child(j);
|
|
1661
|
+
if (startTag && (startTag.type === "start_tag" || startTag.type === "self_closing_tag")) {
|
|
1662
|
+
for (let k = 0; k < startTag.childCount; k++) {
|
|
1663
|
+
const a = startTag.child(k);
|
|
1664
|
+
if (a?.type === "attribute") {
|
|
1665
|
+
for (let m = 0; m < a.childCount; m++) {
|
|
1666
|
+
const an = a.child(m);
|
|
1667
|
+
if (an?.type === "attribute_name" && asciiLower(an.text) === "lang") {
|
|
1668
|
+
const inner = this.findChildByType(a, "attribute_value");
|
|
1669
|
+
return inner ? inner.text : unquote(a.text);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
return null;
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Accumulate each addressable element's addressable-subtree size into its
|
|
1682
|
+
* `complexity`, bottom-up, without an O(depth) walk per node.
|
|
1683
|
+
*
|
|
1684
|
+
* One reverse pass over the emitted symbols: a child's count is added to its
|
|
1685
|
+
* owner's accumulator. O(s) time, O(1) extra space beyond the accumulator map
|
|
1686
|
+
* that the symbol list itself already implies. Symbols whose owner is not in
|
|
1687
|
+
* this file (never, for html — every owner is either the module or another
|
|
1688
|
+
* emitted symbol) simply do not accumulate further.
|
|
1689
|
+
*/
|
|
1690
|
+
accumulateSubtreeSizes(symbols) {
|
|
1691
|
+
const totals = map();
|
|
1692
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
1693
|
+
const s = symbols[i];
|
|
1694
|
+
totals[s.name] = typeof s.complexity === "number" ? s.complexity : 1;
|
|
1695
|
+
}
|
|
1696
|
+
for (let i = symbols.length - 1; i >= 1; i--) {
|
|
1697
|
+
const s = symbols[i];
|
|
1698
|
+
const owner = s.ownerName;
|
|
1699
|
+
if (owner === void 0 || totals[owner] === void 0) continue;
|
|
1700
|
+
totals[owner] += totals[s.name];
|
|
1701
|
+
}
|
|
1702
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
1703
|
+
const s = symbols[i];
|
|
1704
|
+
s.complexity = totals[s.name] ?? 1;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
// ==========================================================================
|
|
1708
|
+
// SCORING — bounded, monotone, and every input is a MEASURED count
|
|
1709
|
+
// ==========================================================================
|
|
1710
|
+
computeScores(m) {
|
|
1711
|
+
const countable = m.totalElements > 0 ? Math.max(1, m.totalElements - 4) : 1;
|
|
1712
|
+
const semanticRatio = m.semanticCount / countable;
|
|
1713
|
+
let semantic = 50;
|
|
1714
|
+
if (semanticRatio > 0.05) semantic += 20;
|
|
1715
|
+
if (semanticRatio > 0.15) semantic += 20;
|
|
1716
|
+
if (m.headingCount === 0 && m.totalElements > 20) semantic -= 20;
|
|
1717
|
+
let accessibility = 50;
|
|
1718
|
+
if (m.imgCount > 0) {
|
|
1719
|
+
accessibility += Math.round(m.imgWithAltCount / m.imgCount * 30);
|
|
1720
|
+
} else {
|
|
1721
|
+
accessibility += 20;
|
|
1722
|
+
}
|
|
1723
|
+
if (m.ariaCount > 0) accessibility += 10;
|
|
1724
|
+
if (m.headingCount > 0) accessibility += 10;
|
|
1725
|
+
accessibility -= m.headingLevelSkips * 5;
|
|
1726
|
+
accessibility -= Math.min(20, m.labelWithoutForCount * 5);
|
|
1727
|
+
accessibility -= Math.min(20, m.inputWithoutLabelCount);
|
|
1728
|
+
accessibility -= Math.min(15, m.iframeWithoutLoadingCount * 5);
|
|
1729
|
+
let performance = 80;
|
|
1730
|
+
performance -= Math.min(40, m.iframeWithoutLoadingCount * 10);
|
|
1731
|
+
if (m.maxDepth > 30) performance -= 10;
|
|
1732
|
+
performance -= Math.min(10, (m.templateFragmentCount ?? 0) * 2);
|
|
1733
|
+
const architecturalScore = Math.round(
|
|
1734
|
+
(clamp(semantic, 0, 100) + clamp(accessibility, 0, 100) + clamp(performance, 0, 100)) / 3
|
|
1735
|
+
);
|
|
1736
|
+
return {
|
|
1737
|
+
accessibility: clamp(accessibility, 0, 100),
|
|
1738
|
+
semantic: clamp(semantic, 0, 100),
|
|
1739
|
+
performance: clamp(performance, 0, 100),
|
|
1740
|
+
architecturalScore
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
nodeToRange(node) {
|
|
1744
|
+
return {
|
|
1745
|
+
startLine: node.startPosition.row + 1,
|
|
1746
|
+
startColumn: node.startPosition.column + 1,
|
|
1747
|
+
endLine: node.endPosition.row + 1,
|
|
1748
|
+
endColumn: node.endPosition.column + 1,
|
|
1749
|
+
startIndex: node.startIndex,
|
|
1750
|
+
endIndex: node.endIndex
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
function clamp(value, lo, hi) {
|
|
1755
|
+
return value < lo ? lo : value > hi ? hi : value;
|
|
1756
|
+
}
|
|
1757
|
+
export {
|
|
1758
|
+
RealHtmlAnalyzer
|
|
1759
|
+
};
|