ctxman 3.0.0 → 3.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/.ctxman/llm-profiles.json +1067 -39
- package/.ctxman/profiles/backend-team.json +13 -0
- package/.ctxman/profiles/default.json +12 -0
- package/.ctxman/profiles/devops-team.json +21 -0
- package/.ctxman/profiles/frontend-team.json +20 -0
- package/.ctxman/templates/bug-fix.json +17 -0
- package/.ctxman/templates/code-review.json +9 -0
- package/.ctxman/templates/documentation.json +9 -0
- package/.ctxman/templates/feature.json +9 -0
- package/.ctxman/templates/full-context.json +9 -0
- package/.ctxman/templates/refactoring.json +18 -0
- package/.ctxman/templates/testing.json +9 -0
- package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
- package/LICENSE +21 -0
- package/README.md +380 -136
- package/bin/cli.js +1880 -529
- package/bin/cm-ask.js +18 -13
- package/bin/cm-audit.js +124 -0
- package/bin/cm-count.js +151 -0
- package/bin/cm-gitingest.js +128 -115
- package/bin/cm-map.js +242 -0
- package/bin/cm-mcp.js +293 -0
- package/bin/cm-pack.js +405 -0
- package/bin/cm-policy.js +170 -0
- package/bin/cm-pr.js +248 -0
- package/bin/cm-search.js +178 -0
- package/bin/cm-update.js +128 -128
- package/bin/cm-why.js +197 -0
- package/bin/mcp-server.js +8 -24
- package/ctxman.js +237 -183
- package/examples/README.md +19 -4
- package/examples/github-actions/ctxman-check.yml +49 -0
- package/examples/wizard-profiles/security-audit/profile.json +7 -1
- package/index.d.ts +1422 -0
- package/index.js +55 -31
- package/lib/agents/pr-failure-agent.js +48 -45
- package/lib/analyzers/ai-suggester.js +451 -0
- package/lib/analyzers/dependency-scanner.js +505 -0
- package/lib/analyzers/duplicate-detector.js +264 -0
- package/lib/analyzers/go-method-analyzer.js +151 -110
- package/lib/analyzers/import-tracker.js +431 -0
- package/lib/analyzers/method-analyzer.js +1055 -434
- package/lib/analyzers/performance-dashboard.js +683 -0
- package/lib/analyzers/token-calculator.js +895 -504
- package/lib/analyzers/unused-exports-detector.js +336 -0
- package/lib/api/mcp/install.js +424 -0
- package/lib/api/mcp/prompts.js +281 -281
- package/lib/api/mcp/resources.js +300 -241
- package/lib/api/mcp/sandbox.js +89 -0
- package/lib/api/mcp/server.js +675 -312
- package/lib/api/mcp/start.js +125 -0
- package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
- package/lib/api/rest/server.js +105 -28
- package/lib/cache/CacheManager.js +9 -17
- package/lib/core/Analyzer.js +16 -10
- package/lib/core/ContextBuilder.js +19 -28
- package/lib/core/Reporter.js +15 -14
- package/lib/core/Scanner.js +81 -12
- package/lib/count/index.js +100 -0
- package/lib/formatters/format-registry.js +394 -372
- package/lib/formatters/gitingest-formatter.js +656 -621
- package/lib/formatters/toon-formatter-v1.3.js +637 -632
- package/lib/formatters/toon-formatter.js +239 -240
- package/lib/integrations/git/BlameTracker.js +4 -4
- package/lib/integrations/git/DiffAnalyzer.js +38 -31
- package/lib/integrations/git/GitClient.js +71 -53
- package/lib/lsp/server.js +311 -0
- package/lib/map/graph.js +299 -0
- package/lib/map/imports.js +233 -0
- package/lib/map/index.js +33 -0
- package/lib/map/rank.js +253 -0
- package/lib/map/render.js +346 -0
- package/lib/map/resolver.js +574 -0
- package/lib/pack/budget.js +111 -0
- package/lib/pack/code-lexer.js +360 -0
- package/lib/pack/compress.js +414 -0
- package/lib/pack/index.js +354 -0
- package/lib/pack/renderers/common.js +134 -0
- package/lib/pack/renderers/index.js +24 -0
- package/lib/pack/renderers/json.js +47 -0
- package/lib/pack/renderers/markdown.js +54 -0
- package/lib/pack/renderers/plain.js +38 -0
- package/lib/pack/renderers/xml.js +85 -0
- package/lib/pack/select.js +177 -0
- package/lib/pack/split.js +182 -0
- package/lib/pack/tree.js +35 -0
- package/lib/parsers/gitignore-parser.js +113 -107
- package/lib/parsers/method-filter-parser.js +50 -33
- package/lib/plugins/ExporterPlugin.js +3 -3
- package/lib/plugins/LanguagePlugin.js +3 -3
- package/lib/plugins/PluginManager.js +7 -13
- package/lib/policy/audit.js +294 -0
- package/lib/policy/enforce.js +125 -0
- package/lib/policy/ignore-files.js +169 -0
- package/lib/policy/index.js +52 -0
- package/lib/policy/policy.js +489 -0
- package/lib/pr/diff-parser.js +267 -0
- package/lib/pr/method-mapper.js +148 -0
- package/lib/pr/pr-context.js +829 -0
- package/lib/pr/pr-git.js +149 -0
- package/lib/pr/pr-renderer.js +644 -0
- package/lib/pr/related-tests.js +296 -0
- package/lib/rag/DataSourcePlugin.js +5 -5
- package/lib/rag/EmbeddingProvider.js +19 -8
- package/lib/rag/Indexer.js +14 -5
- package/lib/rag/VectorStoreFactory.js +3 -3
- package/lib/rag/sources/FileDataSource.js +6 -6
- package/lib/rag/stores/BaseVectorStore.js +5 -5
- package/lib/rag/stores/ChromaDBStore.js +10 -10
- package/lib/rag/stores/LanceDBStore.js +29 -19
- package/lib/rag/stores/PineconeStore.js +11 -9
- package/lib/search/bm25.js +433 -0
- package/lib/search/code-units.js +201 -0
- package/lib/search/symbols.js +189 -0
- package/lib/ui/dashboard.js +120 -89
- package/lib/ui/index.js +1 -6
- package/lib/ui/init-wizard-ui.js +550 -0
- package/lib/ui/progress-bar.js +94 -88
- package/lib/ui/select-input.js +40 -32
- package/lib/ui/template-selector.js +99 -0
- package/lib/ui/wizard.js +374 -329
- package/lib/utils/claude-token-counter.js +151 -0
- package/lib/utils/cli-output.js +17 -0
- package/lib/utils/clipboard-utils.js +49 -50
- package/lib/utils/config-utils.js +81 -66
- package/lib/utils/context-versioning.js +532 -0
- package/lib/utils/error-handler.js +112 -112
- package/lib/utils/file-utils.js +153 -69
- package/lib/utils/format-converter.js +356 -283
- package/lib/utils/git-utils.js +366 -294
- package/lib/utils/llm-cost-estimator.js +281 -0
- package/lib/utils/llm-detector.js +242 -78
- package/lib/utils/logger.js +243 -237
- package/lib/utils/messages.js +355 -0
- package/lib/utils/multi-repo-manager.js +364 -0
- package/lib/utils/profile-manager.js +380 -0
- package/lib/utils/safe-read.js +61 -0
- package/lib/utils/secret-redactor.js +61 -0
- package/lib/utils/snapshot-manager.js +679 -0
- package/lib/utils/template-manager.js +378 -0
- package/lib/utils/token-utils.js +201 -170
- package/lib/utils/tokenizer-adapter.js +421 -377
- package/lib/utils/updater.js +539 -506
- package/lib/watch/FileWatcher.js +12 -8
- package/lib/watch/IncrementalAnalyzer.js +7 -7
- package/lib/why/coupling.js +81 -0
- package/lib/why/errors.js +23 -0
- package/lib/why/git.js +169 -0
- package/lib/why/history.js +108 -0
- package/lib/why/index.js +720 -0
- package/lib/why/ownership.js +79 -0
- package/lib/why/paths.js +207 -0
- package/lib/why/references.js +259 -0
- package/lib/why/render.js +722 -0
- package/lib/why/search.js +358 -0
- package/lib/why/target.js +389 -0
- package/lib/wizards/config-generator.js +255 -0
- package/lib/wizards/project-detector.js +571 -0
- package/package.json +44 -10
- package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
- package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Lexer for `pack --compress`
|
|
3
|
+
*
|
|
4
|
+
* Marks every character of a source file as code, comment or string and pairs up the
|
|
5
|
+
* brackets that are code, so the compressor can find a function body's braces without being
|
|
6
|
+
* fooled by braces inside strings, comments, regular expressions or template text. It knows
|
|
7
|
+
* each language's comment and string syntax only; it is not a parser.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import path from 'path';
|
|
11
|
+
|
|
12
|
+
/** Character kinds stored in `kinds` */
|
|
13
|
+
export const CODE = 0;
|
|
14
|
+
export const COMMENT = 1;
|
|
15
|
+
export const STRING = 2;
|
|
16
|
+
|
|
17
|
+
const C_LIKE = { lineComments: ['//'], blockComments: true };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Comment and string syntax per language family. Options:
|
|
21
|
+
* quotes: single-line string delimiters, multilineQuotes: those strings may span lines,
|
|
22
|
+
* triple: multi-line delimiters ("""), charLiterals: `'x'` is a character literal and any
|
|
23
|
+
* other `'` is code (Rust lifetimes, Scala symbols), template: JavaScript template literals,
|
|
24
|
+
* regex: JavaScript regular expression literals.
|
|
25
|
+
*/
|
|
26
|
+
const SYNTAX = {
|
|
27
|
+
js: { ...C_LIKE, quotes: '"\'', template: true, regex: true },
|
|
28
|
+
java: { ...C_LIKE, quotes: '"', triple: ['"""'], charLiterals: true },
|
|
29
|
+
csharp: { ...C_LIKE, quotes: '"', triple: ['"""'], verbatim: true, charLiterals: true },
|
|
30
|
+
go: { ...C_LIKE, quotes: '"', rawBacktick: true, charLiterals: true },
|
|
31
|
+
rust: {
|
|
32
|
+
...C_LIKE,
|
|
33
|
+
nestedComments: true,
|
|
34
|
+
quotes: '"',
|
|
35
|
+
multilineQuotes: true,
|
|
36
|
+
charLiterals: true,
|
|
37
|
+
rustRaw: true,
|
|
38
|
+
},
|
|
39
|
+
php: {
|
|
40
|
+
lineComments: ['//', '#'],
|
|
41
|
+
blockComments: true,
|
|
42
|
+
quotes: '"\'',
|
|
43
|
+
multilineQuotes: true,
|
|
44
|
+
heredoc: true,
|
|
45
|
+
},
|
|
46
|
+
kotlin: { ...C_LIKE, nestedComments: true, quotes: '"', triple: ['"""'], charLiterals: true },
|
|
47
|
+
swift: { ...C_LIKE, nestedComments: true, quotes: '"', triple: ['"""'] },
|
|
48
|
+
c: { ...C_LIKE, quotes: '"', charLiterals: true },
|
|
49
|
+
scala: { ...C_LIKE, nestedComments: true, quotes: '"', triple: ['"""'], charLiterals: true },
|
|
50
|
+
python: { lineComments: ['#'], quotes: '"\'', triple: ['"""', "'''"] },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Extensions whose method analyzer the compressor can use, by language family */
|
|
54
|
+
const FAMILY_BY_EXTENSION = {
|
|
55
|
+
'.js': 'js',
|
|
56
|
+
'.jsx': 'js',
|
|
57
|
+
'.mjs': 'js',
|
|
58
|
+
'.cjs': 'js',
|
|
59
|
+
'.ts': 'js',
|
|
60
|
+
'.tsx': 'js',
|
|
61
|
+
'.mts': 'js',
|
|
62
|
+
'.cts': 'js',
|
|
63
|
+
'.java': 'java',
|
|
64
|
+
'.cs': 'csharp',
|
|
65
|
+
'.go': 'go',
|
|
66
|
+
'.rs': 'rust',
|
|
67
|
+
'.php': 'php',
|
|
68
|
+
'.kt': 'kotlin',
|
|
69
|
+
'.kts': 'kotlin',
|
|
70
|
+
'.swift': 'swift',
|
|
71
|
+
'.c': 'c',
|
|
72
|
+
'.cc': 'c',
|
|
73
|
+
'.cpp': 'c',
|
|
74
|
+
'.h': 'c',
|
|
75
|
+
'.hpp': 'c',
|
|
76
|
+
'.scala': 'scala',
|
|
77
|
+
'.py': 'python',
|
|
78
|
+
'.rb': 'ruby',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const OPENERS = { '(': ')', '[': ']', '{': '}' };
|
|
82
|
+
const CLOSERS = new Set([')', ']', '}']);
|
|
83
|
+
|
|
84
|
+
// After one of these, a `/` starts a regular expression rather than a division
|
|
85
|
+
const REGEX_PRECEDERS = new Set('(,=:[!&|?{};+-*%<>~^}'.split(''));
|
|
86
|
+
const REGEX_KEYWORDS = new Set([
|
|
87
|
+
'return',
|
|
88
|
+
'typeof',
|
|
89
|
+
'instanceof',
|
|
90
|
+
'case',
|
|
91
|
+
'do',
|
|
92
|
+
'else',
|
|
93
|
+
'in',
|
|
94
|
+
'of',
|
|
95
|
+
'new',
|
|
96
|
+
'delete',
|
|
97
|
+
'void',
|
|
98
|
+
'throw',
|
|
99
|
+
'yield',
|
|
100
|
+
'await',
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
const CHAR_LITERAL = /'(?:\\.[^'\n]{0,9}|[^'\\\n])'/y;
|
|
104
|
+
const RUST_RAW_STRING = /b?r(#*)"/y;
|
|
105
|
+
const PHP_HEREDOC = /<<<[ \t]*(['"]?)([A-Za-z_]\w*)\1\r?\n/y;
|
|
106
|
+
const WORD_CHAR = /[\w$]/;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Language family of a file for the compressor, or null when it has no method analyzer
|
|
110
|
+
* @param {string} filePath
|
|
111
|
+
* @returns {string|null} 'js', 'python', 'ruby', ...
|
|
112
|
+
*/
|
|
113
|
+
export function familyOf(filePath) {
|
|
114
|
+
return FAMILY_BY_EXTENSION[path.extname(filePath).toLowerCase()] ?? null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Index of the first character of every line
|
|
119
|
+
* @param {string} src
|
|
120
|
+
* @returns {number[]}
|
|
121
|
+
*/
|
|
122
|
+
export function lineStartsOf(src) {
|
|
123
|
+
const starts = [0];
|
|
124
|
+
for (let i = src.indexOf('\n'); i !== -1; i = src.indexOf('\n', i + 1)) starts.push(i + 1);
|
|
125
|
+
return starts;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Zero-based line of a character index
|
|
130
|
+
* @param {number[]} lineStarts - From lineStartsOf()
|
|
131
|
+
* @param {number} index
|
|
132
|
+
* @returns {number}
|
|
133
|
+
*/
|
|
134
|
+
export function lineAt(lineStarts, index) {
|
|
135
|
+
let lo = 0;
|
|
136
|
+
let hi = lineStarts.length - 1;
|
|
137
|
+
while (lo < hi) {
|
|
138
|
+
const mid = (lo + hi + 1) >> 1;
|
|
139
|
+
if (lineStarts[mid] <= index) lo = mid;
|
|
140
|
+
else hi = mid - 1;
|
|
141
|
+
}
|
|
142
|
+
return lo;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Lex a source file
|
|
147
|
+
* @param {string} src - File content
|
|
148
|
+
* @param {string} family - From familyOf() (not 'ruby')
|
|
149
|
+
* @returns {{kinds: Uint8Array, match: Int32Array, balanced: boolean, lineStarts: number[]}}
|
|
150
|
+
* kinds: CODE/COMMENT/STRING per character; match: index of the partner of each code
|
|
151
|
+
* bracket (-1 elsewhere); balanced: false when a bracket has no partner, in which case
|
|
152
|
+
* the pairs cannot be trusted
|
|
153
|
+
*/
|
|
154
|
+
export function lexCode(src, family) {
|
|
155
|
+
const syntax = SYNTAX[family];
|
|
156
|
+
if (!syntax) throw new Error(`No lexer for ${family}`);
|
|
157
|
+
|
|
158
|
+
const n = src.length;
|
|
159
|
+
const kinds = new Uint8Array(n);
|
|
160
|
+
const match = new Int32Array(n).fill(-1);
|
|
161
|
+
const stack = [];
|
|
162
|
+
const templateDepths = []; // JS: brace depth inside each open `${ ... }`
|
|
163
|
+
let balanced = true;
|
|
164
|
+
let prev = -1; // last significant code character, for regex detection
|
|
165
|
+
|
|
166
|
+
const mark = (from, to, kind) => kinds.fill(kind, from, Math.min(to, n));
|
|
167
|
+
|
|
168
|
+
/** End of a quoted string starting at `start` (the opening delimiter) */
|
|
169
|
+
const quotedEnd = (start, quote, multiline) => {
|
|
170
|
+
for (let j = start + 1; j < n; j++) {
|
|
171
|
+
const c = src[j];
|
|
172
|
+
if (c === '\\') j++;
|
|
173
|
+
else if (c === quote) return j + 1;
|
|
174
|
+
else if (c === '\n' && !multiline) return j; // unterminated: stop at the line end
|
|
175
|
+
}
|
|
176
|
+
return n;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/** End of template text starting at `start`, and whether it stopped at a `${` */
|
|
180
|
+
const templateEnd = (start) => {
|
|
181
|
+
for (let j = start; j < n; j++) {
|
|
182
|
+
const c = src[j];
|
|
183
|
+
if (c === '\\') j++;
|
|
184
|
+
else if (c === '`') return { end: j + 1, expression: false };
|
|
185
|
+
else if (c === '$' && src[j + 1] === '{') return { end: j + 1, expression: true };
|
|
186
|
+
}
|
|
187
|
+
return { end: n, expression: false };
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const regexAllowed = () => {
|
|
191
|
+
if (prev < 0) return true;
|
|
192
|
+
const p = src[prev];
|
|
193
|
+
if (REGEX_PRECEDERS.has(p)) return true;
|
|
194
|
+
if (!WORD_CHAR.test(p)) return false;
|
|
195
|
+
let s = prev;
|
|
196
|
+
while (s > 0 && WORD_CHAR.test(src[s - 1])) s--;
|
|
197
|
+
return REGEX_KEYWORDS.has(src.slice(s, prev + 1));
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/** End of a regex literal starting at `start`, or -1 when it does not close on its line */
|
|
201
|
+
const regexEnd = (start) => {
|
|
202
|
+
let inClass = false;
|
|
203
|
+
for (let j = start + 1; j < n; j++) {
|
|
204
|
+
const c = src[j];
|
|
205
|
+
if (c === '\n') return -1;
|
|
206
|
+
if (c === '\\') j++;
|
|
207
|
+
else if (c === '[') inClass = true;
|
|
208
|
+
else if (c === ']') inClass = false;
|
|
209
|
+
else if (c === '/' && !inClass) {
|
|
210
|
+
let end = j + 1;
|
|
211
|
+
while (end < n && /[a-z]/i.test(src[end])) end++;
|
|
212
|
+
return end;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return -1;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const openBracket = (i) => stack.push(i);
|
|
219
|
+
const closeBracket = (i) => {
|
|
220
|
+
const open = stack.pop();
|
|
221
|
+
if (open === undefined || OPENERS[src[open]] !== src[i]) {
|
|
222
|
+
balanced = false;
|
|
223
|
+
if (open !== undefined) stack.push(open);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
match[open] = i;
|
|
227
|
+
match[i] = open;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/** Mark template text from `start` and resume code after it (or inside its `${`) */
|
|
231
|
+
const continueTemplate = (start) => {
|
|
232
|
+
const { end, expression } = templateEnd(start);
|
|
233
|
+
mark(start, end, STRING);
|
|
234
|
+
if (!expression) {
|
|
235
|
+
prev = end - 1;
|
|
236
|
+
return end;
|
|
237
|
+
}
|
|
238
|
+
// `end` is the `{` of `${`: a code bracket closed by the `}` that ends the expression
|
|
239
|
+
openBracket(end);
|
|
240
|
+
templateDepths.push(0);
|
|
241
|
+
prev = end;
|
|
242
|
+
return end + 1;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
let i = 0;
|
|
246
|
+
while (i < n) {
|
|
247
|
+
const ch = src[i];
|
|
248
|
+
const next = src[i + 1];
|
|
249
|
+
|
|
250
|
+
// Comments
|
|
251
|
+
const lineComment = syntax.lineComments.find((token) => src.startsWith(token, i));
|
|
252
|
+
if (lineComment && !(family === 'php' && ch === '#' && next === '[')) {
|
|
253
|
+
const end = src.indexOf('\n', i);
|
|
254
|
+
const stop = end === -1 ? n : end;
|
|
255
|
+
mark(i, stop, COMMENT);
|
|
256
|
+
i = stop;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (syntax.blockComments && ch === '/' && next === '*') {
|
|
260
|
+
let depth = 1;
|
|
261
|
+
let j = i + 2;
|
|
262
|
+
while (j < n && depth > 0) {
|
|
263
|
+
if (src[j] === '*' && src[j + 1] === '/') {
|
|
264
|
+
depth--;
|
|
265
|
+
j += 2;
|
|
266
|
+
} else if (syntax.nestedComments && src[j] === '/' && src[j + 1] === '*') {
|
|
267
|
+
depth++;
|
|
268
|
+
j += 2;
|
|
269
|
+
} else {
|
|
270
|
+
j++;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
mark(i, j, COMMENT);
|
|
274
|
+
i = j;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Strings
|
|
279
|
+
let end = -1;
|
|
280
|
+
const triple = syntax.triple?.find((token) => src.startsWith(token, i));
|
|
281
|
+
if (triple) {
|
|
282
|
+
const close = src.indexOf(triple, i + 3);
|
|
283
|
+
end = close === -1 ? n : close + 3;
|
|
284
|
+
} else if (
|
|
285
|
+
syntax.verbatim &&
|
|
286
|
+
ch === '"' &&
|
|
287
|
+
(src[i - 1] === '@' || (src[i - 1] === '$' && src[i - 2] === '@'))
|
|
288
|
+
) {
|
|
289
|
+
// C# verbatim string: spans lines, `""` is an escaped quote
|
|
290
|
+
end = n;
|
|
291
|
+
for (let j = i + 1; j < n; j++) {
|
|
292
|
+
if (src[j] !== '"') continue;
|
|
293
|
+
if (src[j + 1] === '"') j++;
|
|
294
|
+
else {
|
|
295
|
+
end = j + 1;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
} else if (syntax.quotes.includes(ch)) {
|
|
300
|
+
end = quotedEnd(i, ch, syntax.multilineQuotes);
|
|
301
|
+
} else if (syntax.charLiterals && ch === "'") {
|
|
302
|
+
CHAR_LITERAL.lastIndex = i;
|
|
303
|
+
if (CHAR_LITERAL.test(src)) end = CHAR_LITERAL.lastIndex;
|
|
304
|
+
} else if (syntax.rawBacktick && ch === '`') {
|
|
305
|
+
const close = src.indexOf('`', i + 1);
|
|
306
|
+
end = close === -1 ? n : close + 1;
|
|
307
|
+
} else if (syntax.rustRaw && (ch === 'r' || ch === 'b') && !WORD_CHAR.test(src[i - 1] ?? ' ')) {
|
|
308
|
+
RUST_RAW_STRING.lastIndex = i;
|
|
309
|
+
const raw = RUST_RAW_STRING.exec(src);
|
|
310
|
+
if (raw) {
|
|
311
|
+
const close = src.indexOf(`"${raw[1]}`, RUST_RAW_STRING.lastIndex);
|
|
312
|
+
end = close === -1 ? n : close + 1 + raw[1].length;
|
|
313
|
+
}
|
|
314
|
+
} else if (syntax.heredoc && ch === '<' && src.startsWith('<<<', i)) {
|
|
315
|
+
PHP_HEREDOC.lastIndex = i;
|
|
316
|
+
const heredoc = PHP_HEREDOC.exec(src);
|
|
317
|
+
if (heredoc) {
|
|
318
|
+
const closing = new RegExp(`^[ \\t]*${heredoc[2]}\\b`, 'm');
|
|
319
|
+
const rest = src.slice(PHP_HEREDOC.lastIndex);
|
|
320
|
+
const found = closing.exec(rest);
|
|
321
|
+
end = found ? PHP_HEREDOC.lastIndex + found.index + found[0].length : n;
|
|
322
|
+
}
|
|
323
|
+
} else if (syntax.template && ch === '`') {
|
|
324
|
+
mark(i, i + 1, STRING);
|
|
325
|
+
i = continueTemplate(i + 1);
|
|
326
|
+
continue;
|
|
327
|
+
} else if (syntax.regex && ch === '/' && regexAllowed()) {
|
|
328
|
+
end = regexEnd(i);
|
|
329
|
+
}
|
|
330
|
+
if (end > i) {
|
|
331
|
+
mark(i, end, STRING);
|
|
332
|
+
prev = end - 1;
|
|
333
|
+
i = end;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Code
|
|
338
|
+
if (ch in OPENERS) {
|
|
339
|
+
openBracket(i);
|
|
340
|
+
if (ch === '{' && templateDepths.length) templateDepths[templateDepths.length - 1]++;
|
|
341
|
+
} else if (CLOSERS.has(ch)) {
|
|
342
|
+
if (ch === '}' && templateDepths.length && templateDepths[templateDepths.length - 1] === 0) {
|
|
343
|
+
// End of a `${ ... }` expression: back into the template text
|
|
344
|
+
templateDepths.pop();
|
|
345
|
+
closeBracket(i);
|
|
346
|
+
i = continueTemplate(i + 1);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (ch === '}' && templateDepths.length) templateDepths[templateDepths.length - 1]--;
|
|
350
|
+
closeBracket(i);
|
|
351
|
+
}
|
|
352
|
+
if (ch !== ' ' && ch !== '\t' && ch !== '\n' && ch !== '\r') prev = i;
|
|
353
|
+
i++;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (stack.length || templateDepths.length) balanced = false;
|
|
357
|
+
return { kinds, match, balanced, lineStarts: lineStartsOf(src) };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export default { lexCode, familyOf, lineStartsOf, lineAt, CODE, COMMENT, STRING };
|