@backbencher/brain 0.0.0-stage → 0.1.1
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-Q5APTJQL.js +17 -0
- package/dist/brain-relationship-enrichment-MYR2AM4F.js +558 -0
- package/dist/brain.sql-EIM4WS4D.js +29 -0
- package/dist/chunk-22V37YTK.js +1758 -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-2WXYNLBA.js +29 -0
- package/dist/chunk-3CTFIJJB.js +17 -0
- package/dist/chunk-3ZTOHHSW.js +2104 -0
- package/dist/chunk-4H22TLOT.js +543 -0
- package/dist/chunk-5BVAH26D.js +340 -0
- package/dist/chunk-62DHLYW6.js +3427 -0
- package/dist/chunk-7AN7U5IZ.js +371 -0
- package/dist/chunk-7DRPNVIN.js +782 -0
- package/dist/chunk-7MBYT777.js +174 -0
- package/dist/chunk-AHDWGM7Q.js +236 -0
- package/dist/chunk-B5GEAAYF.js +818 -0
- package/dist/chunk-BEIZZ73B.js +1479 -0
- package/dist/chunk-BQ3OVQ3P.js +125 -0
- package/dist/chunk-D2Q4Y47A.js +53 -0
- package/dist/chunk-DIYAGQJL.js +34 -0
- package/dist/chunk-DWKXZ6IF.js +336 -0
- package/dist/chunk-DYM4FQEE.js +90 -0
- package/dist/chunk-ENW4BTBA.js +930 -0
- package/dist/chunk-EWMWK7WN.js +75 -0
- package/dist/chunk-EY5ZZUUA.js +626 -0
- package/dist/chunk-GBI7V7KY.js +69 -0
- package/dist/chunk-GEFFO6O2.js +152 -0
- package/dist/chunk-HJEWO5EV.js +247 -0
- package/dist/chunk-HQSPJDHI.js +613 -0
- package/dist/chunk-KAET47ZL.js +125 -0
- package/dist/chunk-KCDECRJY.js +440 -0
- package/dist/chunk-L5TDYKYK.js +76 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-NO3U3FJQ.js +201 -0
- package/dist/chunk-OWISBAOF.js +61 -0
- package/dist/chunk-QKTRECYS.js +1119 -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-RM3PY6TG.js +376 -0
- package/dist/chunk-ROMM6KMC.js +433 -0
- package/dist/chunk-SGGCSHAX.js +12050 -0
- package/dist/chunk-SK6YVHFZ.js +211 -0
- package/dist/chunk-SM7RFQRO.js +214 -0
- package/dist/chunk-UGR4QIGS.js +67 -0
- package/dist/chunk-UQNQZXHY.js +711 -0
- package/dist/chunk-UUNHHX4M.js +13 -0
- package/dist/chunk-VL52PL2T.js +71 -0
- package/dist/chunk-WIG36E67.js +196 -0
- package/dist/chunk-XHK3WWGB.js +7436 -0
- package/dist/chunk-XTQM2IP3.js +2863 -0
- package/dist/chunk-ZE2LG76D.js +1296 -0
- package/dist/chunk-ZETTGPAW.js +6950 -0
- package/dist/cli/index.js +37 -0
- package/dist/config-HZAAYDP3.js +10 -0
- package/dist/content-search-7XIP555Q.js +593 -0
- package/dist/database-76RP44JG.js +9 -0
- package/dist/doctor-ANGETYMN.js +395 -0
- package/dist/edit-engine-CG72LDZD.js +51 -0
- package/dist/engine-BKEOGN7N.js +11 -0
- package/dist/engine-VIW6NSKO.js +18 -0
- package/dist/fingerprint-OBLIMZLP.js +33 -0
- package/dist/framework-intelligence-3LORTPOX.js +860 -0
- package/dist/freshness-3K6VU5OT.js +174 -0
- package/dist/graph-engine-HBSQNPTB.js +21 -0
- package/dist/graph_request-Q6NKB6N2.js +350 -0
- package/dist/impact_request-LYP672WE.js +68 -0
- package/dist/index-space-IUUN5WYV.js +15 -0
- package/dist/index-status-7OVGYSGW.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-FK2XKZXY.js +88 -0
- package/dist/pool-worker.js +1153 -0
- package/dist/project-health-6XPENVAA.js +94 -0
- package/dist/project-index-5SDPHITE.js +57 -0
- package/dist/project-registry-ZJ3DVUGO.js +355 -0
- package/dist/real-bash-analyzer-EJAU5LW5.js +2201 -0
- package/dist/real-c-analyzer-HBD3FNNH.js +5942 -0
- package/dist/real-clean-analyzer-4EEZLPV5.js +3470 -0
- package/dist/real-clojure-analyzer-Q55EBOO2.js +2818 -0
- package/dist/real-cpp-analyzer-IVVOD25R.js +4101 -0
- package/dist/real-csharp-analyzer-ZBNLNJZJ.js +3298 -0
- package/dist/real-css-analyzer-6XRQLUN7.js +15 -0
- package/dist/real-dart-analyzer-OLR72EFW.js +3944 -0
- package/dist/real-elixir-analyzer-MQWGAH2I.js +2192 -0
- package/dist/real-erlang-analyzer-7BVPQEHO.js +1731 -0
- package/dist/real-fsharp-analyzer-C2UPFNOA.js +2390 -0
- package/dist/real-go-analyzer-UKGI43YS.js +4126 -0
- package/dist/real-graphql-analyzer-VE2EEFII.js +1442 -0
- package/dist/real-groovy-analyzer-W26VACFW.js +2285 -0
- package/dist/real-haskell-analyzer-5CVVAJRX.js +2533 -0
- package/dist/real-hcl-analyzer-5ZT4U43N.js +725 -0
- package/dist/real-html-analyzer-JWNX3C6I.js +1759 -0
- package/dist/real-java-analyzer-GU42VBGQ.js +4677 -0
- package/dist/real-javascript-analyzer-QA4QOJ6W.js +14 -0
- package/dist/real-json-analyzer-MPA6PRXL.js +1640 -0
- package/dist/real-julia-analyzer-22CWYQMN.js +2085 -0
- package/dist/real-kotlin-analyzer-QDZJLXXU.js +3956 -0
- package/dist/real-lua-analyzer-G7NHLYKS.js +1957 -0
- package/dist/real-makefile-analyzer-OCY3GT5N.js +920 -0
- package/dist/real-markdown-analyzer-TEJK2JXJ.js +1215 -0
- package/dist/real-matlab-analyzer-Q5SWLN7Z.js +480 -0
- package/dist/real-nim-analyzer-YLXSK563.js +3603 -0
- package/dist/real-objective-c-analyzer-D23ACC2R.js +1518 -0
- package/dist/real-perl-analyzer-WWTRHYDT.js +1865 -0
- package/dist/real-php-analyzer-JNKJHTCL.js +9368 -0
- package/dist/real-powershell-analyzer-XRSNQERV.js +2915 -0
- package/dist/real-protobuf-analyzer-3OKUCZ7T.js +867 -0
- package/dist/real-python-analyzer-VXAFWI72.js +4880 -0
- package/dist/real-r-analyzer-ZGAIDVI4.js +2596 -0
- package/dist/real-ruby-analyzer-X2KTLVBH.js +2429 -0
- package/dist/real-rust-analyzer-OFFHPL2A.js +3359 -0
- package/dist/real-scala-analyzer-TSTW27J6.js +3293 -0
- package/dist/real-scss-analyzer-AVWYPSOQ.js +1515 -0
- package/dist/real-solidity-analyzer-UCI2KWUB.js +2608 -0
- package/dist/real-sql-analyzer-F77ZMQXR.js +3840 -0
- package/dist/real-svelte-analyzer-SF7NRUO5.js +2532 -0
- package/dist/real-swift-analyzer-MN5QT3QN.js +2311 -0
- package/dist/real-toml-analyzer-66L7BDCQ.js +929 -0
- package/dist/real-typescript-analyzer-6KEMVDWQ.js +14 -0
- package/dist/real-vb-analyzer-VICVPOP7.js +3145 -0
- package/dist/real-vba-analyzer-HXLTTHKM.js +466 -0
- package/dist/real-vue-analyzer-QXR56CFV.js +2680 -0
- package/dist/real-xml-analyzer-YL465ZJW.js +2405 -0
- package/dist/real-yaml-analyzer-SUBCBNSI.js +3528 -0
- package/dist/real-zig-analyzer-HIQLZMGY.js +2430 -0
- package/dist/regex-search-NJKHMZNO.js +239 -0
- package/dist/rules-ACHPB7QU.js +152 -0
- package/dist/scoped-path-MJYWJZQX.js +11 -0
- package/dist/setup-show-NT5ZHYNE.js +15 -0
- package/dist/setup-wizard-G5YYR5T6.js +24 -0
- package/dist/symbol-search-GZJIM2J2.js +16 -0
- package/dist/taint-KXUHHPIE.js +202 -0
- package/dist/taint-analyzer-N7YLZU3N.js +163 -0
- package/dist/tool-handlers-OJDC35DI.js +72 -0
- package/dist/tool-registry-BPEJNSZG.js +17 -0
- package/dist/tool-validation-Q57YMMBX.js +32 -0
- package/dist/type-inference-QMT2RMLV.js +13 -0
- package/dist/vector-binary-LMY4QGAN.js +19 -0
- package/dist/version-ABP2TUL2.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,1215 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ASTEngine
|
|
3
|
+
} from "./chunk-3ZTOHHSW.js";
|
|
4
|
+
import "./chunk-BQ3OVQ3P.js";
|
|
5
|
+
import "./chunk-SK6YVHFZ.js";
|
|
6
|
+
import "./chunk-2BSKD7O3.js";
|
|
7
|
+
import "./chunk-KAET47ZL.js";
|
|
8
|
+
import "./chunk-KCDECRJY.js";
|
|
9
|
+
import "./chunk-MLKGABMK.js";
|
|
10
|
+
|
|
11
|
+
// src/ast/real-markdown-analyzer.ts
|
|
12
|
+
import { existsSync } from "fs";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
var MD_REPO_ROOT = /* @__PURE__ */ new Map();
|
|
15
|
+
function markdownRepoRootOf(filePath) {
|
|
16
|
+
const abs = path.resolve(filePath);
|
|
17
|
+
const dir = path.dirname(abs);
|
|
18
|
+
const cached = MD_REPO_ROOT.get(dir);
|
|
19
|
+
if (cached !== void 0) return cached;
|
|
20
|
+
let probe = dir;
|
|
21
|
+
for (let i = 0; i < 64; i++) {
|
|
22
|
+
if (existsSync(path.join(probe, ".git"))) {
|
|
23
|
+
MD_REPO_ROOT.set(dir, probe);
|
|
24
|
+
return probe;
|
|
25
|
+
}
|
|
26
|
+
const parent = path.dirname(probe);
|
|
27
|
+
if (parent === probe) break;
|
|
28
|
+
probe = parent;
|
|
29
|
+
}
|
|
30
|
+
MD_REPO_ROOT.set(dir, "");
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
function markdownRepoRelativeOf(filePath) {
|
|
34
|
+
const root = markdownRepoRootOf(filePath);
|
|
35
|
+
if (root === "") return "";
|
|
36
|
+
const rel = path.relative(root, path.resolve(filePath));
|
|
37
|
+
if (rel.startsWith("..") || rel === "") return "";
|
|
38
|
+
return rel.split(path.sep).join("/");
|
|
39
|
+
}
|
|
40
|
+
function markdownResolveRelativeLink(dest, filePath) {
|
|
41
|
+
const root = markdownRepoRootOf(filePath);
|
|
42
|
+
if (root === "") return null;
|
|
43
|
+
const bare = dest.split("#")[0].split("?")[0];
|
|
44
|
+
if (bare.length === 0) return null;
|
|
45
|
+
const abs = path.resolve(path.dirname(path.resolve(filePath)), bare);
|
|
46
|
+
if (!existsSync(abs)) return null;
|
|
47
|
+
const rel = path.relative(root, abs);
|
|
48
|
+
if (rel.startsWith("..") || rel === "") return null;
|
|
49
|
+
return rel.split(path.sep).join("/");
|
|
50
|
+
}
|
|
51
|
+
var RealMarkdownAnalyzer = class {
|
|
52
|
+
/** The file being analyzed, for the file model: `walkInlineTree` has no path parameter and
|
|
53
|
+
* a relative link's resolution needs one. Set once per `analyze` run. */
|
|
54
|
+
currentFilePath = "";
|
|
55
|
+
analyze(rootNode, content, filePath, context) {
|
|
56
|
+
this.currentFilePath = filePath;
|
|
57
|
+
const symbols = [];
|
|
58
|
+
const relationships = [];
|
|
59
|
+
const imports = [];
|
|
60
|
+
const errors = [];
|
|
61
|
+
const patterns = [];
|
|
62
|
+
const siteOccurrences = /* @__PURE__ */ new Map();
|
|
63
|
+
const relIds = /* @__PURE__ */ new Set();
|
|
64
|
+
const siteId = (qualifiedBase, name) => {
|
|
65
|
+
const n = (siteOccurrences.get(qualifiedBase) ?? 0) + 1;
|
|
66
|
+
siteOccurrences.set(qualifiedBase, n);
|
|
67
|
+
return {
|
|
68
|
+
id: n === 1 ? qualifiedBase : `${qualifiedBase}#${n}`,
|
|
69
|
+
target: n === 1 ? name : `${name}#${n}`,
|
|
70
|
+
repeat: n > 1
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
const anchorSlug = (text) => {
|
|
74
|
+
let out = "";
|
|
75
|
+
for (let i = 0; i < text.length; i++) {
|
|
76
|
+
const c = text.charCodeAt(i);
|
|
77
|
+
const ch = text[i];
|
|
78
|
+
if (c >= 65 && c <= 90 || c >= 97 && c <= 122 || c >= 48 && c <= 57) out += ch.toLowerCase();
|
|
79
|
+
else if (c === 32 || c === 9 || c === 45) out += "-";
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
};
|
|
83
|
+
const addRel = (source, type, target, metadata, line, distinctSites = false) => {
|
|
84
|
+
if (!source || !target) return;
|
|
85
|
+
if (source === target && !distinctSites) return;
|
|
86
|
+
const at = typeof line === "number" && line > 0 ? line : 0;
|
|
87
|
+
const id = `${source}\0${type}\0${target}\0${at}`;
|
|
88
|
+
if (relIds.has(id)) return;
|
|
89
|
+
relIds.add(id);
|
|
90
|
+
relationships.push({
|
|
91
|
+
source,
|
|
92
|
+
target,
|
|
93
|
+
type,
|
|
94
|
+
confidence: 1,
|
|
95
|
+
metadata,
|
|
96
|
+
...at > 0 ? { line_number: at } : {}
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
const mdInlineTrees = context?.mdInlineTrees ?? [];
|
|
100
|
+
const mdInlineStarts = context?.mdInlineStarts ?? [];
|
|
101
|
+
const mdInlineLines = context?.mdInlineLines ?? [];
|
|
102
|
+
const inlineByStart = /* @__PURE__ */ new Map();
|
|
103
|
+
for (let i = 0; i < mdInlineStarts.length; i++) {
|
|
104
|
+
inlineByStart.set(mdInlineStarts[i], { tree: mdInlineTrees[i], line: mdInlineLines[i] });
|
|
105
|
+
}
|
|
106
|
+
const headingStack = [];
|
|
107
|
+
let currentOwner = void 0;
|
|
108
|
+
let currentOwnerId = void 0;
|
|
109
|
+
const DOC = "document";
|
|
110
|
+
let publishedHeadings = 0;
|
|
111
|
+
const anchorMap = /* @__PURE__ */ new Map();
|
|
112
|
+
const anchorOccurrences = /* @__PURE__ */ new Map();
|
|
113
|
+
const pendingAnchors = [];
|
|
114
|
+
const inlineStarts = new Set(mdInlineStarts);
|
|
115
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
116
|
+
this.collectReferenceDefinitions(rootNode, definitions);
|
|
117
|
+
const stack = [rootNode];
|
|
118
|
+
while (stack.length > 0) {
|
|
119
|
+
const node = stack.pop();
|
|
120
|
+
const type = node.type;
|
|
121
|
+
if (type === "atx_heading" || type === "setext_heading") {
|
|
122
|
+
const level = this.headingLevel(node);
|
|
123
|
+
const name = this.headingText(node);
|
|
124
|
+
if (name) {
|
|
125
|
+
publishedHeadings++;
|
|
126
|
+
while (headingStack.length > 0 && headingStack[headingStack.length - 1].level >= level) {
|
|
127
|
+
headingStack.pop();
|
|
128
|
+
}
|
|
129
|
+
const owner = headingStack.length > 0 ? headingStack[headingStack.length - 1].name : void 0;
|
|
130
|
+
const ownerId = headingStack.length > 0 ? headingStack[headingStack.length - 1].id : void 0;
|
|
131
|
+
const base = ownerId ? `${ownerId}.${name}` : name;
|
|
132
|
+
const { id, target, repeat } = siteId(base, name);
|
|
133
|
+
const range = this.nodeToRange(node);
|
|
134
|
+
symbols.push({
|
|
135
|
+
// The site's RESOLVED spelling, which is the bare text for a first
|
|
136
|
+
// occurrence and `Text#n` for a repeat. It has to be this and not the
|
|
137
|
+
// bare text: the edge below already says `target`, the store resolves
|
|
138
|
+
// an endpoint by symbol NAME, and publishing the bare text while the
|
|
139
|
+
// edge says `Text#2` is a name nothing declares. Measured on this lane's
|
|
140
|
+
// own corpus, 120 scala3 markdown files: 529 of 1,843 relationships
|
|
141
|
+
// (28.7%) named a symbol that did not exist, every one of them a
|
|
142
|
+
// repeated site. A first occurrence is unchanged, which is why
|
|
143
|
+
// `heading-identity` and `code-block-site-identity` still read as they
|
|
144
|
+
// always did and this went unmeasured for a full review cycle.
|
|
145
|
+
name: target,
|
|
146
|
+
kind: ASTEngine.SymbolKind.Type,
|
|
147
|
+
id,
|
|
148
|
+
qualifiedName: id,
|
|
149
|
+
range,
|
|
150
|
+
visibility: "public",
|
|
151
|
+
ownerName: owner,
|
|
152
|
+
ownerKind: owner ? ASTEngine.SymbolKind.Type : void 0,
|
|
153
|
+
scope: owner,
|
|
154
|
+
metadata: {
|
|
155
|
+
nodeType: type,
|
|
156
|
+
headingLevel: level,
|
|
157
|
+
isMarkdownHeading: true,
|
|
158
|
+
// A repeated heading is a fact about the document (`## Example` in
|
|
159
|
+
// every section), not a duplicate to be swallowed.
|
|
160
|
+
isRepeatedSite: repeat || void 0
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
if (owner) addRel(owner, ASTEngine.RelationshipType.Contains, target, { role: "heading_hierarchy" }, range.startLine, ownerId !== id);
|
|
164
|
+
const slug = anchorSlug(name);
|
|
165
|
+
if (slug) {
|
|
166
|
+
const seen = (anchorOccurrences.get(slug) ?? 0) + 1;
|
|
167
|
+
anchorOccurrences.set(slug, seen);
|
|
168
|
+
const key = seen === 1 ? slug : `${slug}-${seen - 1}`;
|
|
169
|
+
if (!anchorMap.has(key)) anchorMap.set(key, { name: target, line: range.startLine });
|
|
170
|
+
}
|
|
171
|
+
headingStack.push({ name: target, id, level });
|
|
172
|
+
currentOwner = target;
|
|
173
|
+
currentOwnerId = id;
|
|
174
|
+
}
|
|
175
|
+
patterns.push("Markdown headings");
|
|
176
|
+
this.emitInlineUses(node, inlineByStart, addRel, currentOwner ?? DOC, definitions, pendingAnchors, imports);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (type === "inline") {
|
|
180
|
+
const inline = inlineByStart.get(node.startIndex);
|
|
181
|
+
if (inline?.tree?.rootNode) {
|
|
182
|
+
const source = currentOwner ?? DOC;
|
|
183
|
+
this.walkInlineTree(inline.tree.rootNode, addRel, source, inline.line, definitions, pendingAnchors);
|
|
184
|
+
this.emitIncludeImports(String(inline.tree.rootNode.text ?? ""), addRel, source, inline.line, imports);
|
|
185
|
+
}
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (type === "pipe_table") {
|
|
189
|
+
const name = this.tableName(node, content);
|
|
190
|
+
const owner = currentOwner ?? DOC;
|
|
191
|
+
const ownerId = currentOwnerId ?? DOC;
|
|
192
|
+
const base = ownerId === DOC ? name : `${ownerId}.${name}`;
|
|
193
|
+
const { id, target, repeat } = siteId(base, name);
|
|
194
|
+
const range = this.nodeToRange(node);
|
|
195
|
+
symbols.push({
|
|
196
|
+
name: target,
|
|
197
|
+
kind: ASTEngine.SymbolKind.Class,
|
|
198
|
+
id,
|
|
199
|
+
qualifiedName: id,
|
|
200
|
+
range,
|
|
201
|
+
visibility: "public",
|
|
202
|
+
ownerName: owner === DOC ? void 0 : owner,
|
|
203
|
+
ownerKind: owner === DOC ? void 0 : ASTEngine.SymbolKind.Type,
|
|
204
|
+
scope: owner === DOC ? void 0 : owner,
|
|
205
|
+
metadata: {
|
|
206
|
+
nodeType: type,
|
|
207
|
+
isMarkdownTable: true,
|
|
208
|
+
isRepeatedSite: repeat || void 0
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
addRel(owner, ASTEngine.RelationshipType.Contains, target, { role: "table_under_heading" }, range.startLine);
|
|
212
|
+
const header = this.findChildByType(node, /* @__PURE__ */ new Set(["pipe_table_header"]));
|
|
213
|
+
if (header) {
|
|
214
|
+
for (let i = 0; i < header.namedChildCount; i++) {
|
|
215
|
+
const cell = header.namedChild(i);
|
|
216
|
+
if (cell?.type === "pipe_table_cell") {
|
|
217
|
+
const col = cell.text.trim();
|
|
218
|
+
if (!col) continue;
|
|
219
|
+
const cSite = siteId(`${id}.${col}`, col);
|
|
220
|
+
symbols.push({
|
|
221
|
+
name: cSite.target,
|
|
222
|
+
kind: ASTEngine.SymbolKind.Property,
|
|
223
|
+
id: cSite.id,
|
|
224
|
+
qualifiedName: cSite.id,
|
|
225
|
+
range: this.nodeToRange(cell),
|
|
226
|
+
visibility: "public",
|
|
227
|
+
ownerName: target,
|
|
228
|
+
ownerKind: ASTEngine.SymbolKind.Class,
|
|
229
|
+
scope: target,
|
|
230
|
+
metadata: {
|
|
231
|
+
nodeType: "pipe_table_cell",
|
|
232
|
+
isMarkdownTableColumn: true,
|
|
233
|
+
isRepeatedSite: cSite.repeat || void 0
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
addRel(target, ASTEngine.RelationshipType.Contains, cSite.target, { role: "table_column" }, range.startLine, cSite.id !== id);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
patterns.push("Markdown tables");
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (type === "fenced_code_block" || type === "indented_code_block") {
|
|
244
|
+
const infoNode = this.findChildByType(node, /* @__PURE__ */ new Set(["info_string"]));
|
|
245
|
+
const langNode = infoNode ? this.findChildByType(infoNode, /* @__PURE__ */ new Set(["language"])) : null;
|
|
246
|
+
const lang = langNode?.text?.trim() ?? infoNode?.text?.trim() ?? "text";
|
|
247
|
+
const owner = currentOwner ?? DOC;
|
|
248
|
+
const ownerId = currentOwnerId ?? DOC;
|
|
249
|
+
const name = lang === "text" ? "code_block" : `code_block.${lang}`;
|
|
250
|
+
const base = ownerId === DOC ? name : `${ownerId}.${name}`;
|
|
251
|
+
const { id, target, repeat } = siteId(base, name);
|
|
252
|
+
const range = this.nodeToRange(node);
|
|
253
|
+
const declaredLang = langNode?.text?.trim() ?? infoNode?.text?.trim();
|
|
254
|
+
symbols.push({
|
|
255
|
+
name: target,
|
|
256
|
+
kind: ASTEngine.SymbolKind.Function,
|
|
257
|
+
id,
|
|
258
|
+
qualifiedName: id,
|
|
259
|
+
range,
|
|
260
|
+
visibility: "public",
|
|
261
|
+
ownerName: owner === DOC ? void 0 : owner,
|
|
262
|
+
ownerKind: owner === DOC ? void 0 : ASTEngine.SymbolKind.Type,
|
|
263
|
+
scope: owner === DOC ? void 0 : owner,
|
|
264
|
+
metadata: {
|
|
265
|
+
nodeType: type,
|
|
266
|
+
...declaredLang ? { language: declaredLang } : {},
|
|
267
|
+
isMarkdownCodeBlock: true,
|
|
268
|
+
isRepeatedSite: repeat || void 0
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
addRel(owner, ASTEngine.RelationshipType.Contains, target, { role: "code_block_under_heading" }, range.startLine);
|
|
272
|
+
patterns.push("Markdown code blocks");
|
|
273
|
+
this.emitIncludeFencePartOf(node, addRel, owner, range.startLine, patterns);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (type === "minus_metadata") {
|
|
277
|
+
const name = "front_matter";
|
|
278
|
+
const owner = DOC;
|
|
279
|
+
const { id, target, repeat } = siteId(`${owner}.${name}`, name);
|
|
280
|
+
const range = this.nodeToRange(node);
|
|
281
|
+
symbols.push({
|
|
282
|
+
// Same rule as every other site: the published name is the RESOLVED
|
|
283
|
+
// spelling, so a document with two front-matter blocks publishes
|
|
284
|
+
// `front_matter` and `front_matter#2` and any edge naming the second
|
|
285
|
+
// resolves. A first occurrence — every document until the second block
|
|
286
|
+
// exists — is byte-identical to what it always was.
|
|
287
|
+
name: target,
|
|
288
|
+
kind: ASTEngine.SymbolKind.Namespace,
|
|
289
|
+
id,
|
|
290
|
+
qualifiedName: id,
|
|
291
|
+
range,
|
|
292
|
+
visibility: "public",
|
|
293
|
+
ownerName: owner,
|
|
294
|
+
ownerKind: void 0,
|
|
295
|
+
metadata: {
|
|
296
|
+
nodeType: type,
|
|
297
|
+
isMarkdownFrontMatter: true,
|
|
298
|
+
isRepeatedSite: repeat || void 0
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
patterns.push("Markdown front matter");
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
305
|
+
const child = node.namedChild(i);
|
|
306
|
+
if (child) stack.push(child);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
let anchorsResolved = 0;
|
|
310
|
+
let anchorsUndefined = 0;
|
|
311
|
+
for (const p of pendingAnchors) {
|
|
312
|
+
const frag = this.decodeFragment(p.fragment);
|
|
313
|
+
const hit = frag ? anchorMap.get(frag) : void 0;
|
|
314
|
+
if (!hit) {
|
|
315
|
+
anchorsUndefined++;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
anchorsResolved++;
|
|
319
|
+
addRel(
|
|
320
|
+
p.source,
|
|
321
|
+
ASTEngine.RelationshipType.References,
|
|
322
|
+
hit.name,
|
|
323
|
+
{
|
|
324
|
+
role: "markdown_internal_anchor",
|
|
325
|
+
linkText: p.literal,
|
|
326
|
+
linkForm: "internal_anchor",
|
|
327
|
+
anchor: frag,
|
|
328
|
+
...p.line ? { linkLine: p.line } : {}
|
|
329
|
+
},
|
|
330
|
+
hit.line
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
if (anchorsResolved > 0) patterns.push("Markdown internal anchor references");
|
|
334
|
+
if (anchorsUndefined > 0) patterns.push(`Markdown undefined internal anchors: ${anchorsUndefined}`);
|
|
335
|
+
this.classifyEvidenceTier(relationships);
|
|
336
|
+
const selfRepoRel = markdownRepoRelativeOf(filePath);
|
|
337
|
+
if (selfRepoRel.length > 0) {
|
|
338
|
+
const lineCount = content.split("\n").length;
|
|
339
|
+
symbols.push({
|
|
340
|
+
name: selfRepoRel,
|
|
341
|
+
kind: ASTEngine.SymbolKind.Module,
|
|
342
|
+
id: selfRepoRel,
|
|
343
|
+
qualifiedName: selfRepoRel,
|
|
344
|
+
range: { startLine: 1, startColumn: 0, endLine: lineCount, endColumn: 0 },
|
|
345
|
+
visibility: "public",
|
|
346
|
+
scope: "",
|
|
347
|
+
metadata: { nodeType: "document", isMarkdownDocument: true }
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
language: "markdown",
|
|
352
|
+
symbols,
|
|
353
|
+
relationships,
|
|
354
|
+
imports,
|
|
355
|
+
exports: [],
|
|
356
|
+
errors,
|
|
357
|
+
metadata: {
|
|
358
|
+
complexity: symbols.length,
|
|
359
|
+
linesOfCode: content.split("\n").length,
|
|
360
|
+
patterns,
|
|
361
|
+
framework: "Markdown",
|
|
362
|
+
headingCount: publishedHeadings,
|
|
363
|
+
inlineBlockCount: mdInlineTrees.length
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* M14 #419-D: assign `metadata.evidenceClass` from the closed five-tier vocabulary,
|
|
369
|
+
* applied centrally so an emission site cannot skip it (the shape 22 sibling lanes
|
|
370
|
+
* use; typescript's `classifyEvidenceTier` is the reference).
|
|
371
|
+
*
|
|
372
|
+
* This lane's absence was not cosmetic. With no stored class, the read-time
|
|
373
|
+
* derivation in `graph-core.ts` decides instead, and its `STRUCTURAL_TYPES` set is
|
|
374
|
+
* `{calls, inherits, implements, overrides}` — it does NOT contain `contains`. So
|
|
375
|
+
* every heading-hierarchy, table and code-block edge, which is a direct AST
|
|
376
|
+
* parent/child relation and cannot be anything but structural, was rendered to
|
|
377
|
+
* users as `inferred`: "a guess (heuristic or confidence < 0.7) — treat as a hint,
|
|
378
|
+
* never as fact". The graph was telling the reader to distrust the one class of edge
|
|
379
|
+
* in this lane that is derived from the tree itself.
|
|
380
|
+
*
|
|
381
|
+
* The markdown mapping, and why each row is what it is:
|
|
382
|
+
* contains -> structural a parent/child relation read off the block tree.
|
|
383
|
+
* uses -> reference_qualified
|
|
384
|
+
* A link destination is not a guess about a NAME: the destination is a node's
|
|
385
|
+
* own text and, for a reference link, it is the destination the document's own
|
|
386
|
+
* `link_reference_definition` declares. That is qualifier evidence — stronger
|
|
387
|
+
* than `usage_contextual` ("the name appears in context"), weaker than structure
|
|
388
|
+
* — which is exactly what the tier is for. Every published `uses` edge in this
|
|
389
|
+
* lane resolves a destination the grammar read out of a node, so the
|
|
390
|
+
* `resolved === false` arm that other lanes need is unreachable here and is
|
|
391
|
+
* deliberately not invented.
|
|
392
|
+
* references -> reference_qualified
|
|
393
|
+
* The same tier as `uses`, for the same reason, and deliberately NOT
|
|
394
|
+
* `structural`: the edge is not a parent/child relation read off the tree. A
|
|
395
|
+
* `#fragment` names a heading, the analyzer recomputes that heading's anchor and
|
|
396
|
+
* looks it up, and the edge exists because the lookup HIT. The lookup is the
|
|
397
|
+
* qualification.
|
|
398
|
+
*/
|
|
399
|
+
classifyEvidenceTier(relationships) {
|
|
400
|
+
for (const r of relationships) {
|
|
401
|
+
const md = r.metadata ?? (r.metadata = {});
|
|
402
|
+
if (md.evidenceClass) continue;
|
|
403
|
+
const t = String(r.type ?? "");
|
|
404
|
+
if (t === "contains") {
|
|
405
|
+
md.evidenceClass = "structural";
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (t === "uses") {
|
|
409
|
+
md.evidenceClass = "reference_qualified";
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (t === "references") {
|
|
413
|
+
md.evidenceClass = "reference_qualified";
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (t === "imports") {
|
|
417
|
+
md.evidenceClass = "import_based";
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (t === "part_of") {
|
|
421
|
+
md.evidenceClass = "import_based";
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
md.evidenceClass = "usage_contextual";
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Collect `link_reference_definition` nodes into `label -> destination`.
|
|
429
|
+
*
|
|
430
|
+
* A reference link (`[text][ref]`, `[ref][]`, `[ref]`) carries only its LABEL; the
|
|
431
|
+
* destination lives in a definition elsewhere in the document, and CommonMark puts
|
|
432
|
+
* definitions at BLOCK level, so they are found here rather than in the inline tree.
|
|
433
|
+
* Before this, a document's reference links produced no `uses` edge at all —
|
|
434
|
+
* measured on this lane's pinned corpus (scala3@18df09c0, 738 tracked .md files) at
|
|
435
|
+
* 671 sites (44 uses + 627 definitions) across 66 files, while the lane doc claimed
|
|
436
|
+
* "Links (inline and reference destinations)".
|
|
437
|
+
*
|
|
438
|
+
* The walk descends named children but SKIPS `inline` subtrees: a definition cannot
|
|
439
|
+
* live inside one, and skipping keeps this pass off the inline grammar's node count,
|
|
440
|
+
* which is the expensive half of a markdown file. First definition of a label wins,
|
|
441
|
+
* which is CommonMark's own rule.
|
|
442
|
+
*/
|
|
443
|
+
collectReferenceDefinitions(root, into) {
|
|
444
|
+
const s = [root];
|
|
445
|
+
while (s.length > 0) {
|
|
446
|
+
const n = s.pop();
|
|
447
|
+
if (n.type === "inline") continue;
|
|
448
|
+
if (n.type === "link_reference_definition") {
|
|
449
|
+
const labelNode = this.findChildByType(n, /* @__PURE__ */ new Set(["link_label"]));
|
|
450
|
+
const destNode = this.findChildByType(n, /* @__PURE__ */ new Set(["link_destination"]));
|
|
451
|
+
if (labelNode && destNode) {
|
|
452
|
+
const label = this.stripBrackets(labelNode.text);
|
|
453
|
+
const destination = this.stripAngleBrackets(destNode.text.trim()).trim();
|
|
454
|
+
if (label && destination && !into.has(label)) {
|
|
455
|
+
into.set(label, { destination, line: labelNode.startPosition.row + 1 });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
461
|
+
const c = n.namedChild(i);
|
|
462
|
+
if (c) s.push(c);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* A heading's OWN `inline` node, which the two heading forms do not hold the same way.
|
|
468
|
+
*
|
|
469
|
+
* MD-DEFECT-1 (2026-10-03, the M60 pass-2 review). The ATX form puts the heading text in
|
|
470
|
+
* a DIRECT `inline` child:
|
|
471
|
+
*
|
|
472
|
+
* atx_heading > atx_h1_marker, inline
|
|
473
|
+
*
|
|
474
|
+
* and the SETEXT form does not — the text lives one level down, inside the paragraph the
|
|
475
|
+
* underline terminates:
|
|
476
|
+
*
|
|
477
|
+
* setext_heading > paragraph > inline , setext_h1_underline
|
|
478
|
+
*
|
|
479
|
+
* Both shapes are measured on the pinned wasm, not read off a spec. `emitInlineUses`
|
|
480
|
+
* resolved the heading's inline with `findChildByType`, which walks DIRECT named
|
|
481
|
+
* children only, so it returned null for every setext heading and the method returned
|
|
482
|
+
* before emitting anything: **every link, image, autolink and reference link inside a
|
|
483
|
+
* setext heading was silently dropped.** The `if (!inline) return` read as a guard
|
|
484
|
+
* against a heading with no text, and it was in fact a guard that matched half the
|
|
485
|
+
* heading forms.
|
|
486
|
+
*
|
|
487
|
+
* The population is small and it is stated rather than inflated: 76 setext headings in a
|
|
488
|
+
* 1,200-file corpus draw, of which 17 files carry one, and a whole-corpus scan of every
|
|
489
|
+
* line whose FOLLOWING line is a setext underline and which itself contains a link found
|
|
490
|
+
* **3** — `docker/vendor/github.com/hashicorp/go-immutable-radix/README.md:1`,
|
|
491
|
+
* `docker/vendor/github.com/hashicorp/go-immutable-radix/v2/README.md:1`, and
|
|
492
|
+
* `packer/README.md:4`. All three are H1 badge banners. Three instances is small; the
|
|
493
|
+
* asymmetry inside ONE construct is the defect, and the same argument closed MD-G1 for
|
|
494
|
+
* indented code blocks at a population the lane's own first estimate understated by 100x.
|
|
495
|
+
* It is also a dead branch in the sense the workstream names — reachable, and always
|
|
496
|
+
* taking the early return for one of the two heading forms.
|
|
497
|
+
*
|
|
498
|
+
* The engine already supplies the tree. `extractMarkdownInlineBlocks` walks the WHOLE
|
|
499
|
+
* block tree and collects every `inline` node, so the setext paragraph's inline is in
|
|
500
|
+
* `context.mdInlineStarts` under its own byte offset; nothing but this lookup was
|
|
501
|
+
* missing, and no shared file changes.
|
|
502
|
+
*
|
|
503
|
+
* Depth is bounded to one `paragraph` hop on purpose. A heading's text is the inline
|
|
504
|
+
* inside that paragraph, and reaching further would start picking up inline blocks that
|
|
505
|
+
* are body content rather than heading text.
|
|
506
|
+
*/
|
|
507
|
+
headingInline(node) {
|
|
508
|
+
const direct = this.findChildByType(node, /* @__PURE__ */ new Set(["inline"]));
|
|
509
|
+
if (direct) return direct;
|
|
510
|
+
const paragraph = this.findChildByType(node, /* @__PURE__ */ new Set(["paragraph"]));
|
|
511
|
+
if (!paragraph) return null;
|
|
512
|
+
return this.findChildByType(paragraph, /* @__PURE__ */ new Set(["inline"]));
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Walk a heading node's own inline child and emit `uses` edges for every
|
|
516
|
+
* link/image/autolink in the companion inline tree matched by byte offset.
|
|
517
|
+
*
|
|
518
|
+
* `headingInline` rather than a direct-child lookup, for the reason its own comment
|
|
519
|
+
* gives: an ATX heading holds its `inline` directly and a setext heading holds it inside
|
|
520
|
+
* a `paragraph`, and reading only the direct form dropped every link in every setext
|
|
521
|
+
* heading in the corpus.
|
|
522
|
+
*/
|
|
523
|
+
emitInlineUses(node, inlineByStart, addRel, source, definitions, pendingAnchors, imports) {
|
|
524
|
+
const inline = this.headingInline(node);
|
|
525
|
+
if (!inline) return;
|
|
526
|
+
const entry = inlineByStart.get(inline.startIndex);
|
|
527
|
+
if (entry?.tree?.rootNode) {
|
|
528
|
+
this.walkInlineTree(entry.tree.rootNode, addRel, source, entry.line, definitions, pendingAnchors);
|
|
529
|
+
this.emitIncludeImports(String(entry.tree.rootNode.text ?? ""), addRel, source, entry.line, imports);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* A template include directive, and the `imports` edge it carries (MD-G3).
|
|
534
|
+
*
|
|
535
|
+
* ## Why this is a text rule in a lane whose measured property is zero regexes
|
|
536
|
+
*
|
|
537
|
+
* Zero regex literals is a property this lane publishes and a probe measures
|
|
538
|
+
* (`validation/m12/results/regex-census.json`, markdown 4 → 0). Keeping it is not
|
|
539
|
+
* stubbornness: it is what makes "every fact this analyzer publishes is AST-derived"
|
|
540
|
+
* a checkable claim rather than a habit. This method is the one place where the fact
|
|
541
|
+
* cannot be AST-derived, and it is a character-code scan of ONE node's own text for a
|
|
542
|
+
* construct the grammar does not model — which is a different thing from scanning a
|
|
543
|
+
* file for a pattern. `stripAngleBrackets`, `stripAtxClosingSequence`, `anchorSlug` and
|
|
544
|
+
* `decodeFragment` are all the same technique, and they are why the property survived
|
|
545
|
+
* this round.
|
|
546
|
+
*
|
|
547
|
+
* ## What it reads, and what it refuses to read
|
|
548
|
+
*
|
|
549
|
+
* `{%` … `%}`, whose first word is `include` (optionally `include-markdown` or
|
|
550
|
+
* `include_relative`), whose argument is either a quoted string or a bare token, and
|
|
551
|
+
* whose argument **ends in a known file extension**.
|
|
552
|
+
*
|
|
553
|
+
* That last clause is the whole discipline. The corpus carries 1,030+ `{% include %}`
|
|
554
|
+
* directives and most are Jinja/pymdownx MACRO calls naming no file —
|
|
555
|
+
* `{% include type t="Number" %}` (317 occurrences), `{% include tag ver="5.0" %}`
|
|
556
|
+
* (60+), `{% include example spec=… %}` (94). Publishing `imports -> Number` or
|
|
557
|
+
* `imports -> 5.0` would be exactly the fabrication the workstream forbids, and it
|
|
558
|
+
* would be invisible to the differential only because the oracle would have to repeat
|
|
559
|
+
* the same mistake. The extension test is the discriminator, and it is measured: 68
|
|
560
|
+
* directives on the pinned corpus name a file, and those 68 are the population.
|
|
561
|
+
*
|
|
562
|
+
* ## Confidence and evidence class
|
|
563
|
+
*
|
|
564
|
+
* `1.0` and `import_based`. Nothing here is inferred: the path is the author's own
|
|
565
|
+
* text, read verbatim, and the edge says this document pulls that path in. The
|
|
566
|
+
* confidence is NOT a claim that the file exists — a per-file analyzer cannot know,
|
|
567
|
+
* and 0 of the 68 were checked for existence by this lane. It is a claim about how the
|
|
568
|
+
* edge was derived, which is what the number means everywhere else in this lane.
|
|
569
|
+
*/
|
|
570
|
+
emitIncludeImports(text, addRel, source, baseLine, imports) {
|
|
571
|
+
const n = text.length;
|
|
572
|
+
let row = 0;
|
|
573
|
+
let scannedTo = 0;
|
|
574
|
+
for (let i = 0; i + 1 < n; i++) {
|
|
575
|
+
if (text.charCodeAt(i) !== 123 || text.charCodeAt(i + 1) !== 37) continue;
|
|
576
|
+
let p = i + 2;
|
|
577
|
+
if (p < n && text.charCodeAt(p) === 45) p++;
|
|
578
|
+
while (p < n && this.isSpaceCode(text.charCodeAt(p))) p++;
|
|
579
|
+
const wordAt = p;
|
|
580
|
+
if (!this.consumeWord(text, p, "include")) continue;
|
|
581
|
+
p += 7;
|
|
582
|
+
while (p < n) {
|
|
583
|
+
const c = text.charCodeAt(p);
|
|
584
|
+
const wordChar = c >= 65 && c <= 90 || c >= 97 && c <= 122 || c >= 48 && c <= 57 || c === 45 || c === 95;
|
|
585
|
+
if (!wordChar) break;
|
|
586
|
+
p++;
|
|
587
|
+
}
|
|
588
|
+
const wordEnd = p;
|
|
589
|
+
while (p < n && this.isSpaceCode(text.charCodeAt(p))) p++;
|
|
590
|
+
const arg = this.readIncludeArgument(text, p, wordAt, wordEnd);
|
|
591
|
+
if (!arg.path || !this.namesAFile(arg.path)) continue;
|
|
592
|
+
for (let k = scannedTo; k < i; k++) if (text.charCodeAt(k) === 10) row++;
|
|
593
|
+
scannedTo = i;
|
|
594
|
+
const line = typeof baseLine === "number" && baseLine > 0 ? baseLine + row : void 0;
|
|
595
|
+
const offset = baseLine > 0 ? row : 0;
|
|
596
|
+
imports.push({
|
|
597
|
+
path: arg.path,
|
|
598
|
+
importType: "template-include",
|
|
599
|
+
localName: arg.path,
|
|
600
|
+
range: {
|
|
601
|
+
startLine: line ?? 0,
|
|
602
|
+
startColumn: 0,
|
|
603
|
+
endLine: (line ?? 0) + arg.lines - 1,
|
|
604
|
+
endColumn: 0,
|
|
605
|
+
startIndex: 0,
|
|
606
|
+
endIndex: 0
|
|
607
|
+
},
|
|
608
|
+
metadata: { directive: arg.directive, quoted: arg.quoted, inlineBlockLine: offset }
|
|
609
|
+
});
|
|
610
|
+
addRel(
|
|
611
|
+
source,
|
|
612
|
+
ASTEngine.RelationshipType.Imports,
|
|
613
|
+
arg.path,
|
|
614
|
+
{ role: "markdown_template_include", directive: arg.directive, ...arg.quoted ? { quoted: true } : {} },
|
|
615
|
+
line
|
|
616
|
+
);
|
|
617
|
+
i = i + 2;
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
isSpaceCode(c) {
|
|
622
|
+
return c === 32 || c === 9 || c === 10 || c === 13;
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* `word` at `at`, by character code.
|
|
626
|
+
*
|
|
627
|
+
* The boundary rule is asymmetric on purpose and the first version got it wrong in the
|
|
628
|
+
* direction that loses real edges: it treated `-` and `_` as word characters, so
|
|
629
|
+
* `include-markdown` and `include_relative` — 53 of the corpus's 68 file-naming
|
|
630
|
+
* directives between them — did not match, and only the bare `{% include x.md %}` form
|
|
631
|
+
* published. A letter or a digit after the word still ends the match, so `{% includes
|
|
632
|
+
* x.md %}` is not a directive; that is the case the fixture's near-miss section pins.
|
|
633
|
+
*/
|
|
634
|
+
consumeWord(text, at, word) {
|
|
635
|
+
for (let k = 0; k < word.length; k++) if (text.charCodeAt(at + k) !== word.charCodeAt(k)) return false;
|
|
636
|
+
const after = text.charCodeAt(at + word.length);
|
|
637
|
+
if (after >= 65 && after <= 90 || after >= 97 && after <= 122 || after >= 48 && after <= 57) return false;
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* The directive's argument: a quoted string if there is one, else the bare token up to
|
|
642
|
+
* whitespace or `%}`. Returns `lines` so the emitted range can cover a multi-line
|
|
643
|
+
* directive, which `{% include` with a long argument list does produce.
|
|
644
|
+
*/
|
|
645
|
+
readIncludeArgument(text, at, wordAt, wordEnd) {
|
|
646
|
+
const n = text.length;
|
|
647
|
+
const directive = wordEnd > wordAt ? text.slice(wordAt, wordEnd) : "include";
|
|
648
|
+
let p = at;
|
|
649
|
+
let quoted = false;
|
|
650
|
+
if (p < n && (text.charCodeAt(p) === 34 || text.charCodeAt(p) === 39)) {
|
|
651
|
+
const q = text.charCodeAt(p);
|
|
652
|
+
quoted = true;
|
|
653
|
+
p++;
|
|
654
|
+
let out2 = "";
|
|
655
|
+
while (p < n && text.charCodeAt(p) !== q) {
|
|
656
|
+
out2 += text[p];
|
|
657
|
+
p++;
|
|
658
|
+
}
|
|
659
|
+
return { path: out2.trim() || void 0, directive, quoted, lines: 1 };
|
|
660
|
+
}
|
|
661
|
+
let out = "";
|
|
662
|
+
while (p < n && !this.isSpaceCode(text.charCodeAt(p)) && text.charCodeAt(p) !== 37) {
|
|
663
|
+
out += text[p];
|
|
664
|
+
p++;
|
|
665
|
+
}
|
|
666
|
+
let lines = 1;
|
|
667
|
+
for (let k = at; k < p && k < n; k++) if (text.charCodeAt(k) === 10) lines++;
|
|
668
|
+
return { path: out || void 0, directive, quoted: false, lines };
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Does this argument name a FILE rather than a macro keyword?
|
|
672
|
+
*
|
|
673
|
+
* The extension list is by character code and closed, and every entry is an extension
|
|
674
|
+
* that appears in the corpus's own include directives. It is a ROLE test rather than a
|
|
675
|
+
* shape test: the role is "a path to another file this build will read", and the
|
|
676
|
+
* discriminator the corpus supports is the extension, because that is the only part of
|
|
677
|
+
* the argument that differs between `properties.md` and `t="Number"`.
|
|
678
|
+
*/
|
|
679
|
+
namesAFile(path2) {
|
|
680
|
+
const dot = path2.lastIndexOf(".");
|
|
681
|
+
if (dot <= 0 || dot === path2.length - 1) return false;
|
|
682
|
+
const ext = path2.slice(dot + 1).toLowerCase();
|
|
683
|
+
const known = ["md", "markdown", "html", "htm", "txt", "yml", "yaml", "json", "toml", "svg", "png", "jpg", "jpeg", "gif", "csv", "ini", "conf", "pdf"];
|
|
684
|
+
for (const k of known) if (ext === k) return true;
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Walk a parsed inline tree and emit `uses` edges for links, images, autolinks and
|
|
689
|
+
* reference links.
|
|
690
|
+
*
|
|
691
|
+
* Two structural facts about the companion inline grammar make this correct rather
|
|
692
|
+
* than merely working, and both were measured against the pinned wasm rather than
|
|
693
|
+
* read off a spec:
|
|
694
|
+
*
|
|
695
|
+
* 1. A link inside a `code_span` is UNREACHABLE here, structurally. The grammar
|
|
696
|
+
* gives `code_span` only ANONYMOUS children (`[`, `]`, `(`, `:`, `/`, `.`,
|
|
697
|
+
* `code_span_delimiter`) — no `link_destination` — and this walk descends NAMED
|
|
698
|
+
* children only. A `[nope](https://bad.example)` inside backticks therefore
|
|
699
|
+
* cannot be emitted, which is correct, and it is correct for a reason a reader
|
|
700
|
+
* can check rather than for the absence of a filter.
|
|
701
|
+
*
|
|
702
|
+
* 2. A link inside `strong_emphasis`/`emphasis` IS reachable, because those are
|
|
703
|
+
* named nodes with named children. That is also correct: `**a [x](u)**` is a link.
|
|
704
|
+
*
|
|
705
|
+
* `baseLine` is the 1-based FILE line of this inline block. A node row inside this
|
|
706
|
+
* sub-tree is relative to the SUBSTRING the engine parsed, so `baseLine + row` is
|
|
707
|
+
* the file line, and it is the only correct conversion available — which is why the
|
|
708
|
+
* engine hands `mdInlineLines` over at all.
|
|
709
|
+
*/
|
|
710
|
+
walkInlineTree(root, addRel, source, baseLine, definitions, pendingAnchors) {
|
|
711
|
+
const fileLine = (node) => {
|
|
712
|
+
if (typeof baseLine !== "number" || baseLine <= 0) return void 0;
|
|
713
|
+
return baseLine + (typeof node.startPosition?.row === "number" ? node.startPosition.row : 0);
|
|
714
|
+
};
|
|
715
|
+
const s = [root];
|
|
716
|
+
while (s.length > 0) {
|
|
717
|
+
const inn = s.pop();
|
|
718
|
+
switch (inn.type) {
|
|
719
|
+
case "link_destination": {
|
|
720
|
+
const target = this.stripAngleBrackets(inn.text.trim()).trim();
|
|
721
|
+
if (target) {
|
|
722
|
+
if (pendingAnchors && target.charCodeAt(0) === 35) {
|
|
723
|
+
pendingAnchors.push({ source, fragment: target.slice(1), literal: target, line: fileLine(inn) });
|
|
724
|
+
} else {
|
|
725
|
+
addRel(
|
|
726
|
+
source,
|
|
727
|
+
ASTEngine.RelationshipType.Uses,
|
|
728
|
+
// THE FILE MODEL'S TARGET. A relative destination is resolved against the
|
|
729
|
+
// containing document — CommonMark's own rule — and when it names a file on
|
|
730
|
+
// disk the edge targets that file's published document symbol instead of a
|
|
731
|
+
// string that names nothing. The destination AS WRITTEN stays on the edge as
|
|
732
|
+
// `linkDestination`, which is the half the lane's earlier ruling was protecting;
|
|
733
|
+
// nothing is fabricated and nothing is lost. A destination that resolves to no
|
|
734
|
+
// file, or to one outside the repository, is left exactly as written.
|
|
735
|
+
this.linkKindOf(target).targetKind === "repoRelativePath" ? markdownResolveRelativeLink(target, this.currentFilePath) ?? target : target,
|
|
736
|
+
{ role: "markdown_link", linkText: target, linkDestination: target, ...this.linkKindOf(target) },
|
|
737
|
+
fileLine(inn)
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
743
|
+
case "uri_autolink":
|
|
744
|
+
case "email_autolink": {
|
|
745
|
+
const target = this.stripAngleBrackets(inn.text);
|
|
746
|
+
if (target) {
|
|
747
|
+
const kind = this.linkTargetMetadata(target);
|
|
748
|
+
addRel(
|
|
749
|
+
source,
|
|
750
|
+
ASTEngine.RelationshipType.Uses,
|
|
751
|
+
target,
|
|
752
|
+
{ role: "markdown_autolink", isExternal: true, targetKind: this.uriSchemeOf(target) === null ? "emailAutolink" : kind.targetKind, ...kind.externalScheme !== void 0 ? { externalScheme: kind.externalScheme } : {} },
|
|
753
|
+
fileLine(inn)
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
case "full_reference_link":
|
|
759
|
+
case "collapsed_reference_link":
|
|
760
|
+
case "shortcut_link": {
|
|
761
|
+
this.emitReferenceLink(inn, addRel, source, definitions, fileLine(inn), void 0, pendingAnchors);
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
case "image": {
|
|
765
|
+
const labelNode = this.findChildByTypeAny(inn, /* @__PURE__ */ new Set(["link_label"]));
|
|
766
|
+
if (labelNode) {
|
|
767
|
+
this.emitReferenceLink(inn, addRel, source, definitions, fileLine(inn), this.stripBrackets(labelNode.text), pendingAnchors);
|
|
768
|
+
}
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
for (let i = inn.namedChildCount - 1; i >= 0; i--) {
|
|
773
|
+
const c = inn.namedChild(i);
|
|
774
|
+
if (c) s.push(c);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* A reference link's edge points at the DESTINATION the document declares.
|
|
780
|
+
*
|
|
781
|
+
* A reference link with NO matching `link_reference_definition` is NOT published,
|
|
782
|
+
* and that is the specification rather than a noise heuristic. CommonMark: "If a
|
|
783
|
+
* link reference definition has not been defined for the link label, then it is not
|
|
784
|
+
* a link" — the construct renders as literal text. So `[1]` in a legal notice, or
|
|
785
|
+
* `[missing closing paren](url` with the paren never closed, is not a link and
|
|
786
|
+
* emitting an edge to a target named `1` would be a fabricated endpoint.
|
|
787
|
+
*
|
|
788
|
+
* Measured, not assumed: publishing the undefined case produced 171 edges across 34
|
|
789
|
+
* of the lane's 738 corpus files, and sampling them showed them dominated by bare
|
|
790
|
+
* bracketed citation markers — `NOTICE.md: document -> 1` six times over. Those are
|
|
791
|
+
* also an R10 target-spelling problem: `1` is not a name this lane spells its
|
|
792
|
+
* symbols with. Suppressing the undefined case removed 171 fabricated endpoints and
|
|
793
|
+
* left the 213 genuinely-resolved reference links, which are the whole point.
|
|
794
|
+
*
|
|
795
|
+
* The consequence worth stating: an undefined reference is invisible rather than
|
|
796
|
+
* flagged. That is a real limitation and it is a DECISION, taken because the
|
|
797
|
+
* alternative measures as fabrication.
|
|
798
|
+
*/
|
|
799
|
+
emitReferenceLink(node, addRel, source, definitions, line, knownLabel, pendingAnchors) {
|
|
800
|
+
const label = knownLabel ?? this.referenceLabelOf(node);
|
|
801
|
+
if (!label) return;
|
|
802
|
+
const defined = definitions.get(label);
|
|
803
|
+
if (!defined) return;
|
|
804
|
+
const destination = defined.destination;
|
|
805
|
+
if (pendingAnchors && destination.charCodeAt(0) === 35) {
|
|
806
|
+
pendingAnchors.push({ source, fragment: destination.slice(1), literal: destination, line });
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
const target = this.linkKindOf(destination).targetKind === "repoRelativePath" ? markdownResolveRelativeLink(destination, this.currentFilePath) ?? destination : destination;
|
|
810
|
+
addRel(
|
|
811
|
+
source,
|
|
812
|
+
ASTEngine.RelationshipType.Uses,
|
|
813
|
+
target,
|
|
814
|
+
{
|
|
815
|
+
role: "markdown_link",
|
|
816
|
+
linkText: label,
|
|
817
|
+
linkForm: node.type,
|
|
818
|
+
resolvedVia: "link_reference_definition",
|
|
819
|
+
linkDestination: destination,
|
|
820
|
+
// The destination, not the label. A reference link resolves its LABEL
|
|
821
|
+
// through the definition and its DESTINATION is what the edge points at, so
|
|
822
|
+
// classifying the label would answer a question nobody asked — `[home]` is
|
|
823
|
+
// not a path and `http://example.com` is not a heading.
|
|
824
|
+
...this.linkTargetMetadata(destination)
|
|
825
|
+
},
|
|
826
|
+
line
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
/** The reference label of a reference link: its `link_label`, else its link text. */
|
|
830
|
+
referenceLabelOf(node) {
|
|
831
|
+
const labelNode = this.findChildByTypeAny(node, /* @__PURE__ */ new Set(["link_label"]));
|
|
832
|
+
if (labelNode) {
|
|
833
|
+
const t = this.stripBrackets(labelNode.text);
|
|
834
|
+
if (t) return t;
|
|
835
|
+
}
|
|
836
|
+
const textNode = this.findChildByTypeAny(node, /* @__PURE__ */ new Set(["link_text"]));
|
|
837
|
+
if (textNode) {
|
|
838
|
+
const t = textNode.text.trim();
|
|
839
|
+
if (t) return t;
|
|
840
|
+
}
|
|
841
|
+
return void 0;
|
|
842
|
+
}
|
|
843
|
+
/** Strip surrounding square brackets by position, no regex. */
|
|
844
|
+
stripBrackets(text) {
|
|
845
|
+
const t = text.trim();
|
|
846
|
+
if (t.length >= 2 && t.charCodeAt(0) === 91 && t.charCodeAt(t.length - 1) === 93) {
|
|
847
|
+
return t.slice(1, -1).trim();
|
|
848
|
+
}
|
|
849
|
+
return t;
|
|
850
|
+
}
|
|
851
|
+
/** `findChildByType` over a sub-parse node, which is typed `any` not `Node`. */
|
|
852
|
+
findChildByTypeAny(node, types) {
|
|
853
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
854
|
+
const child = node.namedChild(i);
|
|
855
|
+
if (child && types.has(child.type)) return child;
|
|
856
|
+
}
|
|
857
|
+
return null;
|
|
858
|
+
}
|
|
859
|
+
/** Strip surrounding angle brackets by position, no regex. */
|
|
860
|
+
stripAngleBrackets(text) {
|
|
861
|
+
const t = text.trim();
|
|
862
|
+
if (t.length >= 2 && t.charCodeAt(0) === 60 && t.charCodeAt(t.length - 1) === 62) {
|
|
863
|
+
return t.slice(1, -1);
|
|
864
|
+
}
|
|
865
|
+
return t;
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* A link destination's RFC 3986 scheme, or `null` when it has none.
|
|
869
|
+
*
|
|
870
|
+
* By the RFC's own rule rather than by pattern: `ALPHA *( ALPHA / DIGIT / "+" /
|
|
871
|
+
* "-" / "." ) ":"`, with `/`, `?` or `#` ending the scan because none of them may
|
|
872
|
+
* appear in a scheme. `https://x` has one; `docs/a.md` does not — the segment
|
|
873
|
+
* before its colon contains a `/`.
|
|
874
|
+
*
|
|
875
|
+
* TWO constraints, both measured rather than assumed:
|
|
876
|
+
*
|
|
877
|
+
* - **Two characters minimum.** RFC 3986 admits `X:`, so a Windows drive path
|
|
878
|
+
* `X:/src` classified as a URI scheme. It is a path in this repository, and
|
|
879
|
+
* calling it external would be a false statement about where the target lives.
|
|
880
|
+
* - **One balanced PARENTHESIS pair stripped first.** `[x]((https://…))` is
|
|
881
|
+
* legal CommonMark and its destination really is `(https://…)` — the inner
|
|
882
|
+
* pair is balanced, so the parentheses are part of the destination rather than
|
|
883
|
+
* delimiters — so the scan has to see through them to find the scheme.
|
|
884
|
+
*
|
|
885
|
+
* An ANGLE pair is NOT stripped here, and that is deliberate: the `<...>`
|
|
886
|
+
* destination form is unwrapped at its source instead (`walkInlineTree`'s
|
|
887
|
+
* `link_destination` case and `collectReferenceDefinitions`), because CommonMark
|
|
888
|
+
* says those brackets are not part of the destination at all. Stripping them a
|
|
889
|
+
* second time here would be a branch that never fires, and a rule the
|
|
890
|
+
* implementation cannot reach is not a rule.
|
|
891
|
+
*/
|
|
892
|
+
uriSchemeOf(dest) {
|
|
893
|
+
let body = dest.trim();
|
|
894
|
+
for (let guard = 0; guard < 2; guard++) {
|
|
895
|
+
if (body.length < 2) break;
|
|
896
|
+
if (body.charCodeAt(0) !== 40 || body.charCodeAt(body.length - 1) !== 41) break;
|
|
897
|
+
body = body.slice(1, -1).trim();
|
|
898
|
+
}
|
|
899
|
+
if (body.length >= 2 && body.charCodeAt(0) === 47 && body.charCodeAt(1) === 47) return "network-path";
|
|
900
|
+
let colon = -1;
|
|
901
|
+
for (let i = 0; i < body.length; i++) {
|
|
902
|
+
const c = body.charCodeAt(i);
|
|
903
|
+
if (c === 58) {
|
|
904
|
+
colon = i;
|
|
905
|
+
break;
|
|
906
|
+
}
|
|
907
|
+
if (c === 47 || c === 63 || c === 35) return null;
|
|
908
|
+
}
|
|
909
|
+
if (colon < 2) return null;
|
|
910
|
+
const first = body.charCodeAt(0);
|
|
911
|
+
const alpha = first >= 65 && first <= 90 || first >= 97 && first <= 122;
|
|
912
|
+
if (!alpha) return null;
|
|
913
|
+
for (let k = 1; k < colon; k++) {
|
|
914
|
+
const d = body.charCodeAt(k);
|
|
915
|
+
const ok = d >= 65 && d <= 90 || d >= 97 && d <= 122 || d >= 48 && d <= 57 || d === 43 || d === 45 || d === 46;
|
|
916
|
+
if (!ok) return null;
|
|
917
|
+
}
|
|
918
|
+
return body.slice(0, colon);
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* What KIND of place a link destination names, as metadata on the `uses` edge.
|
|
922
|
+
*
|
|
923
|
+
* Why this exists, measured on the 600-file prefix of this lane's 17,456-file
|
|
924
|
+
* admitted population (production `parseFile`, `validation/m60/markdown/
|
|
925
|
+
* abc-before-600.json`): 4,107 of the 4,951 edge targets that bind to no local
|
|
926
|
+
* declaration carry a URI scheme — 3,996 `https`, 110 `http`, 1 `some` — and a
|
|
927
|
+
* consumer reading a resolution rate cannot tell them apart from a reference to a
|
|
928
|
+
* heading that failed to bind. The column therefore overstated this lane's
|
|
929
|
+
* defects by 82.9% before the split existed.
|
|
930
|
+
*
|
|
931
|
+
* The three-way split is the substance, not a detail:
|
|
932
|
+
*
|
|
933
|
+
* - `externalUri` — a remote resource. Outside the corpus by construction, so
|
|
934
|
+
* it can never bind to a local declaration and must not be scored as a miss.
|
|
935
|
+
* - `repoRelativePath` — 572 targets, a link to a sibling file in the SAME
|
|
936
|
+
* repository (`CHANGELOG.md`, `docs/src/contributing.md`). Marking these
|
|
937
|
+
* external would be a false statement: the file is in the corpus, the analyzer
|
|
938
|
+
* simply has no document-target model. They are named so they are countable as
|
|
939
|
+
* what they are — a modelling gap, not an external reference.
|
|
940
|
+
* - `bareToken` — 272 targets, dominated by Documenter.jl's `@ref`
|
|
941
|
+
* cross-reference placeholder (262 of them). A template directive, not a path.
|
|
942
|
+
*
|
|
943
|
+
* What this deliberately does NOT do, because each would be a fabrication or a
|
|
944
|
+
* contract break:
|
|
945
|
+
*
|
|
946
|
+
* - It does not change the TARGET STRING. The endpoint is the link destination
|
|
947
|
+
* as written; a consumer that wants a file target needs a file model, not a
|
|
948
|
+
* renamed endpoint.
|
|
949
|
+
* - It does not change the TYPE LABEL. `uses` over a URL is the lane's pinned
|
|
950
|
+
* contract (`markdown-mutants.ts` mutant MD-L1 relabels these in place, and
|
|
951
|
+
* `internal-anchor-links` forbids `uses` over an in-document anchor), so the
|
|
952
|
+
* label is untouched and only metadata is added.
|
|
953
|
+
* - It does not SUPPRESS the edge. A markdown link to a website is a real use of
|
|
954
|
+
* that resource and belongs in the graph; dropping 4,107 edges to make a
|
|
955
|
+
* column look better is the opposite of the point.
|
|
956
|
+
*
|
|
957
|
+
* Character scanning throughout, no regex, for the reason `anchorSlug` documents:
|
|
958
|
+
* 0 regex literals is a pinned property of this analyzer and
|
|
959
|
+
* `script/m12/verify-regex-census.ts` is the gate that decides it.
|
|
960
|
+
*/
|
|
961
|
+
/** `linkTargetMetadata` for an edge site that needs the classification before the metadata
|
|
962
|
+
* literal is built. One call, one rule: this delegates rather than duplicating it. */
|
|
963
|
+
linkKindOf(dest) {
|
|
964
|
+
return this.linkTargetMetadata(dest);
|
|
965
|
+
}
|
|
966
|
+
linkTargetMetadata(dest) {
|
|
967
|
+
const scheme = this.uriSchemeOf(dest);
|
|
968
|
+
if (scheme !== null) return { isExternal: true, externalScheme: scheme, targetKind: "externalUri" };
|
|
969
|
+
const body = dest.trim();
|
|
970
|
+
if (body.indexOf("/") >= 0 || body.indexOf(".") >= 0) return { isExternal: false, targetKind: "repoRelativePath" };
|
|
971
|
+
return { isExternal: false, targetKind: "bareToken" };
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* A link fragment, percent-decoded and lowercased, for comparison against a heading
|
|
975
|
+
* anchor. No regex, and no `decodeURIComponent` either: that throws on a malformed
|
|
976
|
+
* escape (`#100%`), and a document with a typo in a link would then fail to parse
|
|
977
|
+
* rather than fail to resolve. The decode is skipped for any escape that is not a
|
|
978
|
+
* valid pair of hex digits and the raw text is used instead — a fragment that
|
|
979
|
+
* cannot be decoded cannot match a slug that contains only letters, digits and
|
|
980
|
+
* hyphens anyway, so the fallback loses nothing that could have resolved.
|
|
981
|
+
*/
|
|
982
|
+
decodeFragment(frag) {
|
|
983
|
+
let out = "";
|
|
984
|
+
for (let i = 0; i < frag.length; i++) {
|
|
985
|
+
const c = frag.charCodeAt(i);
|
|
986
|
+
if (c === 37 && i + 2 < frag.length) {
|
|
987
|
+
const hi = this.hexVal(frag.charCodeAt(i + 1));
|
|
988
|
+
const lo = this.hexVal(frag.charCodeAt(i + 2));
|
|
989
|
+
if (hi >= 0 && lo >= 0) {
|
|
990
|
+
out += String.fromCharCode(hi * 16 + lo);
|
|
991
|
+
i += 2;
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
out += frag[i];
|
|
996
|
+
}
|
|
997
|
+
return out.toLowerCase();
|
|
998
|
+
}
|
|
999
|
+
/** -1 for anything that is not a hex digit. */
|
|
1000
|
+
hexVal(c) {
|
|
1001
|
+
if (c >= 48 && c <= 57) return c - 48;
|
|
1002
|
+
if (c >= 97 && c <= 102) return c - 87;
|
|
1003
|
+
if (c >= 65 && c <= 70) return c - 55;
|
|
1004
|
+
return -1;
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* `part_of` from a brace-directive inclusion fence (MD-G5) — the emission half.
|
|
1008
|
+
*
|
|
1009
|
+
* The whole argument is at the call site; this method is the mechanism. It is
|
|
1010
|
+
* called from the fenced-code-block arm for EVERY fence, so the first thing it
|
|
1011
|
+
* does is decline everything that is not a splice inclusion, by a rule a reader
|
|
1012
|
+
* can check against the measured census
|
|
1013
|
+
* (validation/m14/fixtures/markdown/measurements/extended-construct-census.json):
|
|
1014
|
+
*
|
|
1015
|
+
* 1. the info string must exist and begin `{` — else this is an ordinary fence
|
|
1016
|
+
* with a language or no info at all (the census counted 72,786 fences and
|
|
1017
|
+
* 317 brace rows, so 99.6% of fences decline here);
|
|
1018
|
+
* 2. the directive inside the braces must be `include` or `literalinclude` —
|
|
1019
|
+
* else this is an admonition/nav/etc. brace directive (`{note}` 93 sites,
|
|
1020
|
+
* `{contents}` 84, `{eval-rst}` 79, `{code-block}` 20, ... — all counted,
|
|
1021
|
+
* all declined);
|
|
1022
|
+
* 3. the argument must be a non-empty path — `{include}` bare names nothing;
|
|
1023
|
+
* 4. the fence must carry no FRAGMENT option (`:lines:`, `:start-after:`,
|
|
1024
|
+
* `:end-before:`, ...) — a fragment-quoted include is a QUOTE of part of a
|
|
1025
|
+
* file, not a splice of the file into this document's scope, and the census
|
|
1026
|
+
* counts that split (1 splice / 1 fragment-quoted over the whole population).
|
|
1027
|
+
*
|
|
1028
|
+
* THE TARGET IS SYMBOL-SHAPED. A path target resolves 0 rows — the cpp lane
|
|
1029
|
+
* measured 0 of 4,612 — because the arbiter resolves target strings against
|
|
1030
|
+
* symbol names. The edge therefore targets the included file's MODULE STEM (the
|
|
1031
|
+
* basename without its extension, which is the name a document symbol of that
|
|
1032
|
+
* file would publish), and the path as written is kept on the row as
|
|
1033
|
+
* `metadata.partPath`.
|
|
1034
|
+
*
|
|
1035
|
+
* `metadata.partRole: "library"` is MANDATORY on every `part_of` row the
|
|
1036
|
+
* vocabulary's own contract defines (src/ast/types.ts): this file is the
|
|
1037
|
+
* including document — the library — and the directive names the part.
|
|
1038
|
+
*/
|
|
1039
|
+
emitIncludeFencePartOf(node, addRel, source, line, patterns) {
|
|
1040
|
+
const infoNode = this.findChildByType(node, /* @__PURE__ */ new Set(["info_string"]));
|
|
1041
|
+
if (!infoNode) return;
|
|
1042
|
+
const info = infoNode.text.trim();
|
|
1043
|
+
if (info.charCodeAt(0) !== 123) return;
|
|
1044
|
+
const closeBrace = info.indexOf("}");
|
|
1045
|
+
if (closeBrace < 2) return;
|
|
1046
|
+
const directive = info.slice(1, closeBrace).trim();
|
|
1047
|
+
if (directive !== "include" && directive !== "literalinclude") return;
|
|
1048
|
+
const argument = info.slice(closeBrace + 1).trim();
|
|
1049
|
+
if (!argument || !this.namesAFile(argument)) return;
|
|
1050
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1051
|
+
const child = node.namedChild(i);
|
|
1052
|
+
if (!child || child.type !== "code_fence_content") continue;
|
|
1053
|
+
const body = String(child.text ?? "");
|
|
1054
|
+
let lineStart = 0;
|
|
1055
|
+
while (lineStart < body.length) {
|
|
1056
|
+
let lineEnd = lineStart;
|
|
1057
|
+
while (lineEnd < body.length && body.charCodeAt(lineEnd) !== 10) lineEnd++;
|
|
1058
|
+
const line2 = body.slice(lineStart, lineEnd).trim();
|
|
1059
|
+
if (line2.length > 0 && line2.charCodeAt(0) === 58) {
|
|
1060
|
+
const key = line2.slice(1).split(":")[0].split(" ")[0].trim().toLowerCase();
|
|
1061
|
+
if (key === "lines" || key === "start-after" || key === "end-before" || key === "start-line" || key === "end-line" || key === "lineno-start" || key === "lineno-end" || key === "lineno-match" || key === "lineno" || key === "start" || key === "end") {
|
|
1062
|
+
patterns.push("Markdown fragment-quoted include (not part_of)");
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
lineStart = lineEnd + 1;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
const slash = argument.lastIndexOf("/");
|
|
1070
|
+
const base = slash >= 0 ? argument.slice(slash + 1) : argument;
|
|
1071
|
+
const dot = base.lastIndexOf(".");
|
|
1072
|
+
const stem = dot > 0 ? base.slice(0, dot) : base;
|
|
1073
|
+
if (!stem) return;
|
|
1074
|
+
patterns.push("Markdown include fences (part_of)");
|
|
1075
|
+
addRel(
|
|
1076
|
+
source,
|
|
1077
|
+
ASTEngine.RelationshipType.PartOf,
|
|
1078
|
+
stem,
|
|
1079
|
+
{
|
|
1080
|
+
role: "markdown_include_fence",
|
|
1081
|
+
directive,
|
|
1082
|
+
partPath: argument,
|
|
1083
|
+
partRole: "library"
|
|
1084
|
+
},
|
|
1085
|
+
line
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
headingLevel(node) {
|
|
1089
|
+
const marker = this.findChildByType(
|
|
1090
|
+
node,
|
|
1091
|
+
/* @__PURE__ */ new Set([
|
|
1092
|
+
"atx_h1_marker",
|
|
1093
|
+
"atx_h2_marker",
|
|
1094
|
+
"atx_h3_marker",
|
|
1095
|
+
"atx_h4_marker",
|
|
1096
|
+
"atx_h5_marker",
|
|
1097
|
+
"atx_h6_marker",
|
|
1098
|
+
"setext_h1_underline",
|
|
1099
|
+
"setext_h2_underline"
|
|
1100
|
+
])
|
|
1101
|
+
);
|
|
1102
|
+
if (marker) {
|
|
1103
|
+
const t = marker.type;
|
|
1104
|
+
if (t.includes("h1")) return 1;
|
|
1105
|
+
if (t.includes("h2")) return 2;
|
|
1106
|
+
if (t.includes("h3")) return 3;
|
|
1107
|
+
if (t.includes("h4")) return 4;
|
|
1108
|
+
if (t.includes("h5")) return 5;
|
|
1109
|
+
if (t.includes("h6")) return 6;
|
|
1110
|
+
}
|
|
1111
|
+
return 1;
|
|
1112
|
+
}
|
|
1113
|
+
/** Heading display text: the inline child's raw text (no marker, no regex). */
|
|
1114
|
+
/**
|
|
1115
|
+
* A heading's display text.
|
|
1116
|
+
*
|
|
1117
|
+
* Three grammar facts, each a defect before this round, all resolved by position
|
|
1118
|
+
* rather than by pattern (the lane's zero-regex property is the reason these reads
|
|
1119
|
+
* exist at all, and none of them needed a regex to fix).
|
|
1120
|
+
*
|
|
1121
|
+
* 1. ATX CLOSING SEQUENCE. `# H #` is the heading `H`. The grammar already splits
|
|
1122
|
+
* the marker out as a named `atx_hN_marker` child, so the closing run is removed
|
|
1123
|
+
* by position: everything after the last space, when that remainder is only `#`
|
|
1124
|
+
* characters. Before this, such a heading published a symbol named `H #`, so a
|
|
1125
|
+
* document that closed its headings and one that did not produced two different
|
|
1126
|
+
* symbols for the same section.
|
|
1127
|
+
*
|
|
1128
|
+
* 2. A NAME NEVER CONTAINS A NEWLINE. A multi-line setext heading's text is its
|
|
1129
|
+
* PARAGRAPH, and the paragraph holds the text plus the underline, so the
|
|
1130
|
+
* previous "node text minus the last line" fallback returned every line but the
|
|
1131
|
+
* underline. Measured: a two-line setext heading published
|
|
1132
|
+
* `"Multi Line Title\nsecond line of paragraph"` — a name carrying an embedded
|
|
1133
|
+
* newline into `ownerName`, the qualified path and every display surface. Only
|
|
1134
|
+
* the first line is the heading.
|
|
1135
|
+
*
|
|
1136
|
+
* 3. AN EMPTY HEADING IS ABSENCE, NOT A NAME. `#` with no content returns undefined
|
|
1137
|
+
* here, so nothing is published for it. The previous fallback returned the marker
|
|
1138
|
+
* itself, so a document of empty headings published a `contains` chain of
|
|
1139
|
+
* symbols named `#`, `##`, `###` — which is the M60 R7 class (a name that is
|
|
1140
|
+
* not a name), one step short of the register's own `MARKER` pattern, and two
|
|
1141
|
+
* sibling `##` headings named `##` collided and lost a level of hierarchy.
|
|
1142
|
+
*
|
|
1143
|
+
* The empty case has to be caught by TYPE, not by an empty inline child: a bare
|
|
1144
|
+
* `#` is `atx_heading > atx_h1_marker` with NO `inline` child at all (measured on
|
|
1145
|
+
* the pinned wasm), so a guard written as "if the inline text is empty, stop"
|
|
1146
|
+
* never runs for the one case it was written for, and the setext fallback
|
|
1147
|
+
* rescues the marker. That is why the first attempt at this fix appeared to do
|
|
1148
|
+
* nothing.
|
|
1149
|
+
*/
|
|
1150
|
+
headingText(node) {
|
|
1151
|
+
if (node.type === "atx_heading") {
|
|
1152
|
+
const inline = this.findChildByType(node, /* @__PURE__ */ new Set(["inline"]));
|
|
1153
|
+
if (!inline) return void 0;
|
|
1154
|
+
const text = this.stripAtxClosingSequence(inline.text.trim());
|
|
1155
|
+
return text ? text : void 0;
|
|
1156
|
+
}
|
|
1157
|
+
const raw = node.text.trim();
|
|
1158
|
+
if (!raw) return void 0;
|
|
1159
|
+
const nl = raw.indexOf("\n");
|
|
1160
|
+
const first = (nl >= 0 ? raw.slice(0, nl) : raw).trim();
|
|
1161
|
+
return first ? first : void 0;
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Remove an ATX heading's optional closing `#` run, by position and by
|
|
1165
|
+
* character code — no regex. CommonMark: "The optional closing sequence of #s
|
|
1166
|
+
* must be preceded by spaces or tabs and may be followed by spaces or tabs only."
|
|
1167
|
+
* A `#` that is preceded by no space (`a#b`) is part of the text and is kept; a run
|
|
1168
|
+
* that is followed by other text is part of the text and is kept.
|
|
1169
|
+
*/
|
|
1170
|
+
stripAtxClosingSequence(text) {
|
|
1171
|
+
if (text.length === 0 || text.charCodeAt(text.length - 1) !== 35) return text;
|
|
1172
|
+
let end = text.length;
|
|
1173
|
+
while (end > 0 && text.charCodeAt(end - 1) === 35) end--;
|
|
1174
|
+
if (end === text.length) return text;
|
|
1175
|
+
const before = text.charCodeAt(end - 1);
|
|
1176
|
+
if (before !== 32 && before !== 9) return text;
|
|
1177
|
+
return text.slice(0, end).trim();
|
|
1178
|
+
}
|
|
1179
|
+
/** Table identity: header cell text joined by `/` (no regex). */
|
|
1180
|
+
tableName(node, content) {
|
|
1181
|
+
const header = this.findChildByType(node, /* @__PURE__ */ new Set(["pipe_table_header"]));
|
|
1182
|
+
if (header) {
|
|
1183
|
+
const parts = [];
|
|
1184
|
+
for (let i = 0; i < header.namedChildCount; i++) {
|
|
1185
|
+
const cell = header.namedChild(i);
|
|
1186
|
+
if (cell?.type === "pipe_table_cell") {
|
|
1187
|
+
const t = cell.text.trim();
|
|
1188
|
+
if (t) parts.push(t);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
if (parts.length > 0) return parts.join("/");
|
|
1192
|
+
}
|
|
1193
|
+
return "table";
|
|
1194
|
+
}
|
|
1195
|
+
findChildByType(node, types) {
|
|
1196
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1197
|
+
const child = node.namedChild(i);
|
|
1198
|
+
if (child && types.has(child.type)) return child;
|
|
1199
|
+
}
|
|
1200
|
+
return null;
|
|
1201
|
+
}
|
|
1202
|
+
nodeToRange(node) {
|
|
1203
|
+
return {
|
|
1204
|
+
startLine: node.startPosition.row + 1,
|
|
1205
|
+
startColumn: node.startPosition.column,
|
|
1206
|
+
endLine: node.endPosition.row + 1,
|
|
1207
|
+
endColumn: node.endPosition.column,
|
|
1208
|
+
startIndex: node.startIndex,
|
|
1209
|
+
endIndex: node.endIndex
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
};
|
|
1213
|
+
export {
|
|
1214
|
+
RealMarkdownAnalyzer
|
|
1215
|
+
};
|