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
|
@@ -16,716 +16,721 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
class ToonFormatterV13 {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
this.indent = options.indent || 2;
|
|
21
|
+
this.delimiter = options.delimiter || ',';
|
|
22
|
+
this.lengthMarker = options.lengthMarker || false; // false or '#'
|
|
23
|
+
this.indentChar = ' '.repeat(this.indent);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Encode data to TOON format (Spec v1.3)
|
|
28
|
+
*/
|
|
29
|
+
encode(data, options = {}) {
|
|
30
|
+
// Merge options
|
|
31
|
+
const opts = { ...this, ...options };
|
|
32
|
+
this.indent = opts.indent;
|
|
33
|
+
this.delimiter = opts.delimiter || ',';
|
|
34
|
+
this.lengthMarker = opts.lengthMarker || false;
|
|
35
|
+
this.indentChar = ' '.repeat(this.indent);
|
|
36
|
+
|
|
37
|
+
return this.encodeValue(data, 0).trimEnd();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Encode any value
|
|
42
|
+
*/
|
|
43
|
+
encodeValue(value, depth = 0) {
|
|
44
|
+
const normalized = this.normalizeValue(value);
|
|
45
|
+
|
|
46
|
+
if (normalized === null) return 'null';
|
|
47
|
+
if (typeof normalized === 'boolean') return String(normalized);
|
|
48
|
+
if (typeof normalized === 'number') return this.encodeNumber(normalized);
|
|
49
|
+
if (typeof normalized === 'string') return this.encodeString(normalized);
|
|
50
|
+
if (Array.isArray(normalized)) return this.encodeArray(normalized, depth);
|
|
51
|
+
if (typeof normalized === 'object') return this.encodeObject(normalized, depth);
|
|
52
|
+
|
|
53
|
+
return 'null';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Normalize non-JSON values (Spec: Type Conversions)
|
|
58
|
+
*/
|
|
59
|
+
normalizeValue(value) {
|
|
60
|
+
// undefined → null
|
|
61
|
+
if (value === undefined) return null;
|
|
62
|
+
|
|
63
|
+
// function → null
|
|
64
|
+
if (typeof value === 'function') return null;
|
|
65
|
+
|
|
66
|
+
// symbol → null
|
|
67
|
+
if (typeof value === 'symbol') return null;
|
|
68
|
+
|
|
69
|
+
// NaN, Infinity → null
|
|
70
|
+
if (typeof value === 'number' && !isFinite(value)) return null;
|
|
71
|
+
|
|
72
|
+
// BigInt → number or string
|
|
73
|
+
if (typeof value === 'bigint') {
|
|
74
|
+
if (value >= Number.MIN_SAFE_INTEGER && value <= Number.MAX_SAFE_INTEGER) {
|
|
75
|
+
return Number(value);
|
|
76
|
+
}
|
|
77
|
+
return String(value); // Will be quoted
|
|
24
78
|
}
|
|
25
79
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
encode(data, options = {}) {
|
|
30
|
-
// Merge options
|
|
31
|
-
const opts = { ...this, ...options };
|
|
32
|
-
this.indent = opts.indent;
|
|
33
|
-
this.delimiter = opts.delimiter || ',';
|
|
34
|
-
this.lengthMarker = opts.lengthMarker || false;
|
|
35
|
-
this.indentChar = ' '.repeat(this.indent);
|
|
36
|
-
|
|
37
|
-
return this.encodeValue(data, 0).trimEnd();
|
|
80
|
+
// Date → ISO string
|
|
81
|
+
if (value instanceof Date) {
|
|
82
|
+
return value.toISOString();
|
|
38
83
|
}
|
|
39
84
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Encode number (no scientific notation)
|
|
90
|
+
*/
|
|
91
|
+
encodeNumber(num) {
|
|
92
|
+
// -0 → 0
|
|
93
|
+
if (Object.is(num, -0)) return '0';
|
|
94
|
+
|
|
95
|
+
// No scientific notation
|
|
96
|
+
return num.toLocaleString('en-US', {
|
|
97
|
+
useGrouping: false,
|
|
98
|
+
maximumFractionDigits: 20,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Encode string with smart quoting (Spec: Quoting Rules)
|
|
104
|
+
*/
|
|
105
|
+
encodeString(str) {
|
|
106
|
+
const escaped = str
|
|
107
|
+
.replace(/\\/g, '\\\\')
|
|
108
|
+
.replace(/"/g, '\\"')
|
|
109
|
+
.replace(/\n/g, '\\n')
|
|
110
|
+
.replace(/\r/g, '\\r')
|
|
111
|
+
.replace(/\t/g, '\\t');
|
|
112
|
+
|
|
113
|
+
// Always quote if any of these conditions:
|
|
114
|
+
if (
|
|
115
|
+
str === '' || // Empty string
|
|
116
|
+
str !== str.trim() || // Leading/trailing spaces
|
|
117
|
+
str.startsWith('- ') || // Looks like list item
|
|
118
|
+
/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(str) || // Looks like number
|
|
119
|
+
/^(true|false|null)$/.test(str) || // Looks like boolean/null
|
|
120
|
+
/[\[\]{}:]/.test(str) || // Contains structural chars
|
|
121
|
+
/^(\[.*\]|\{.*\})$/.test(str) || // Looks structural
|
|
122
|
+
str.includes(this.delimiter) || // Contains active delimiter
|
|
123
|
+
str.includes(':') || // Contains colon
|
|
124
|
+
str.includes('"') || // Contains quote
|
|
125
|
+
str.includes('\\') // Contains backslash
|
|
126
|
+
) {
|
|
127
|
+
return `"${escaped}"`;
|
|
54
128
|
}
|
|
55
129
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
*/
|
|
59
|
-
normalizeValue(value) {
|
|
60
|
-
// undefined → null
|
|
61
|
-
if (value === undefined) return null;
|
|
130
|
+
return escaped;
|
|
131
|
+
}
|
|
62
132
|
|
|
63
|
-
|
|
64
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Encode object
|
|
135
|
+
*/
|
|
136
|
+
encodeObject(obj, depth = 0) {
|
|
137
|
+
const keys = Object.keys(obj);
|
|
65
138
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// NaN, Infinity → null
|
|
70
|
-
if (typeof value === 'number' && !isFinite(value)) return null;
|
|
71
|
-
|
|
72
|
-
// BigInt → number or string
|
|
73
|
-
if (typeof value === 'bigint') {
|
|
74
|
-
if (value >= Number.MIN_SAFE_INTEGER && value <= Number.MAX_SAFE_INTEGER) {
|
|
75
|
-
return Number(value);
|
|
76
|
-
}
|
|
77
|
-
return String(value); // Will be quoted
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Date → ISO string
|
|
81
|
-
if (value instanceof Date) {
|
|
82
|
-
return value.toISOString();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return value;
|
|
139
|
+
if (keys.length === 0) {
|
|
140
|
+
return ''; // Empty object
|
|
86
141
|
}
|
|
87
142
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
*/
|
|
91
|
-
encodeNumber(num) {
|
|
92
|
-
// -0 → 0
|
|
93
|
-
if (Object.is(num, -0)) return '0';
|
|
94
|
-
|
|
95
|
-
// No scientific notation
|
|
96
|
-
return num.toLocaleString('en-US', {
|
|
97
|
-
useGrouping: false,
|
|
98
|
-
maximumFractionDigits: 20
|
|
99
|
-
});
|
|
100
|
-
}
|
|
143
|
+
const prefix = this.indentChar.repeat(depth);
|
|
144
|
+
const lines = [];
|
|
101
145
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
encodeString(str) {
|
|
106
|
-
const escaped = str
|
|
107
|
-
.replace(/\\/g, '\\\\')
|
|
108
|
-
.replace(/"/g, '\\"')
|
|
109
|
-
.replace(/\n/g, '\\n')
|
|
110
|
-
.replace(/\r/g, '\\r')
|
|
111
|
-
.replace(/\t/g, '\\t');
|
|
112
|
-
|
|
113
|
-
// Always quote if any of these conditions:
|
|
114
|
-
if (
|
|
115
|
-
str === '' || // Empty string
|
|
116
|
-
str !== str.trim() || // Leading/trailing spaces
|
|
117
|
-
str.startsWith('- ') || // Looks like list item
|
|
118
|
-
/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(str) || // Looks like number
|
|
119
|
-
/^(true|false|null)$/.test(str) || // Looks like boolean/null
|
|
120
|
-
/[\[\]{}:]/.test(str) || // Contains structural chars
|
|
121
|
-
/^(\[.*\]|\{.*\})$/.test(str) || // Looks structural
|
|
122
|
-
str.includes(this.delimiter) || // Contains active delimiter
|
|
123
|
-
str.includes(':') || // Contains colon
|
|
124
|
-
str.includes('"') || // Contains quote
|
|
125
|
-
str.includes('\\') // Contains backslash
|
|
126
|
-
) {
|
|
127
|
-
return `"${escaped}"`;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return escaped;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Encode object
|
|
135
|
-
*/
|
|
136
|
-
encodeObject(obj, depth = 0) {
|
|
137
|
-
const keys = Object.keys(obj);
|
|
138
|
-
|
|
139
|
-
if (keys.length === 0) {
|
|
140
|
-
return ''; // Empty object
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const prefix = this.indentChar.repeat(depth);
|
|
144
|
-
const lines = [];
|
|
145
|
-
|
|
146
|
-
for (const key of keys) {
|
|
147
|
-
const quotedKey = this.needsQuotingAsKey(key) ? `"${key}"` : key;
|
|
148
|
-
const value = obj[key];
|
|
146
|
+
for (const key of keys) {
|
|
147
|
+
const quotedKey = this.needsQuotingAsKey(key) ? `"${key}"` : key;
|
|
148
|
+
const value = obj[key];
|
|
149
149
|
|
|
150
|
-
|
|
150
|
+
const encoded = this.encodeValue(value, depth + 1);
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
} else {
|
|
159
|
-
lines.push(`${prefix}${quotedKey}: ${encoded}`);
|
|
160
|
-
}
|
|
152
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
153
|
+
// Nested object
|
|
154
|
+
lines.push(`${prefix}${quotedKey}:`);
|
|
155
|
+
if (encoded) {
|
|
156
|
+
lines.push(encoded);
|
|
161
157
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Check if key needs quoting
|
|
168
|
-
*/
|
|
169
|
-
needsQuotingAsKey(key) {
|
|
170
|
-
// Keys must match: /^[a-zA-Z_][a-zA-Z0-9_.]*$/
|
|
171
|
-
return !/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(key);
|
|
158
|
+
} else {
|
|
159
|
+
lines.push(`${prefix}${quotedKey}: ${encoded}`);
|
|
160
|
+
}
|
|
172
161
|
}
|
|
173
162
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
// Non-uniform or complex: use list format
|
|
197
|
-
return this.encodeListArray(arr, depth);
|
|
163
|
+
return lines.join('\n');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Check if key needs quoting
|
|
168
|
+
*/
|
|
169
|
+
needsQuotingAsKey(key) {
|
|
170
|
+
// Keys must match: /^[a-zA-Z_][a-zA-Z0-9_.]*$/
|
|
171
|
+
return !/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(key);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Encode array with length markers (Spec v1.3)
|
|
176
|
+
*/
|
|
177
|
+
encodeArray(arr, depth = 0) {
|
|
178
|
+
const _prefix = this.indentChar.repeat(depth);
|
|
179
|
+
const length = arr.length;
|
|
180
|
+
|
|
181
|
+
if (length === 0) {
|
|
182
|
+
const lengthPrefix = this.lengthMarker ? `#0` : `0`;
|
|
183
|
+
return `[${lengthPrefix}]:`;
|
|
198
184
|
}
|
|
199
185
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
isTabularArray(arr) {
|
|
204
|
-
if (arr.length === 0) return false;
|
|
205
|
-
|
|
206
|
-
// All must be plain objects
|
|
207
|
-
if (!arr.every(item => this.isPlainObject(item))) {
|
|
208
|
-
return false;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Get keys from first object
|
|
212
|
-
const firstKeys = Object.keys(arr[0]).sort();
|
|
213
|
-
|
|
214
|
-
// All objects must have same keys
|
|
215
|
-
if (!arr.every(item => {
|
|
216
|
-
const keys = Object.keys(item).sort();
|
|
217
|
-
return keys.length === firstKeys.length &&
|
|
218
|
-
keys.every((key, i) => key === firstKeys[i]);
|
|
219
|
-
})) {
|
|
220
|
-
return false;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// All values must be primitives (not nested objects/arrays)
|
|
224
|
-
return arr.every(item =>
|
|
225
|
-
Object.values(item).every(val =>
|
|
226
|
-
val === null ||
|
|
227
|
-
typeof val === 'string' ||
|
|
228
|
-
typeof val === 'number' ||
|
|
229
|
-
typeof val === 'boolean'
|
|
230
|
-
)
|
|
231
|
-
);
|
|
186
|
+
// Check if tabular format (uniform objects with primitive values)
|
|
187
|
+
if (this.isTabularArray(arr)) {
|
|
188
|
+
return this.encodeTabularArray(arr, depth);
|
|
232
189
|
}
|
|
233
190
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
isPrimitiveArray(arr) {
|
|
238
|
-
return arr.every(item =>
|
|
239
|
-
item === null ||
|
|
240
|
-
typeof item === 'string' ||
|
|
241
|
-
typeof item === 'number' ||
|
|
242
|
-
typeof item === 'boolean'
|
|
243
|
-
);
|
|
191
|
+
// Check if primitive array (inline)
|
|
192
|
+
if (this.isPrimitiveArray(arr)) {
|
|
193
|
+
return this.encodePrimitiveArray(arr);
|
|
244
194
|
}
|
|
245
195
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
isPlainObject(val) {
|
|
250
|
-
return val !== null &&
|
|
251
|
-
typeof val === 'object' &&
|
|
252
|
-
!Array.isArray(val) &&
|
|
253
|
-
Object.getPrototypeOf(val) === Object.prototype;
|
|
254
|
-
}
|
|
196
|
+
// Non-uniform or complex: use list format
|
|
197
|
+
return this.encodeListArray(arr, depth);
|
|
198
|
+
}
|
|
255
199
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const keys = Object.keys(arr[0]).sort();
|
|
262
|
-
const length = arr.length;
|
|
263
|
-
|
|
264
|
-
// Build length marker
|
|
265
|
-
const delimiterSuffix = this.delimiter === ',' ? '' : this.delimiter;
|
|
266
|
-
const lengthPrefix = this.lengthMarker ? `#${length}` : `${length}`;
|
|
267
|
-
|
|
268
|
-
// Header: items[N]{field1,field2}:
|
|
269
|
-
const fieldList = keys.join(this.delimiter);
|
|
270
|
-
const header = `[${lengthPrefix}${delimiterSuffix}]{${fieldList}}:`;
|
|
271
|
-
|
|
272
|
-
const lines = [prefix + header];
|
|
273
|
-
|
|
274
|
-
// Data rows
|
|
275
|
-
for (const item of arr) {
|
|
276
|
-
const values = keys.map(key => {
|
|
277
|
-
const val = this.normalizeValue(item[key]);
|
|
278
|
-
if (val === null) return 'null';
|
|
279
|
-
if (typeof val === 'boolean') return String(val);
|
|
280
|
-
if (typeof val === 'number') return this.encodeNumber(val);
|
|
281
|
-
if (typeof val === 'string') return this.encodeString(val);
|
|
282
|
-
return 'null';
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
lines.push(prefix + this.indentChar + values.join(this.delimiter));
|
|
286
|
-
}
|
|
200
|
+
/**
|
|
201
|
+
* Check if array is suitable for tabular format
|
|
202
|
+
*/
|
|
203
|
+
isTabularArray(arr) {
|
|
204
|
+
if (arr.length === 0) return false;
|
|
287
205
|
|
|
288
|
-
|
|
206
|
+
// All must be plain objects
|
|
207
|
+
if (!arr.every((item) => this.isPlainObject(item))) {
|
|
208
|
+
return false;
|
|
289
209
|
}
|
|
290
210
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if (typeof val === 'boolean') return String(val);
|
|
303
|
-
if (typeof val === 'number') return this.encodeNumber(val);
|
|
304
|
-
if (typeof val === 'string') return this.encodeString(val);
|
|
305
|
-
return 'null';
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
return `[${lengthPrefix}${delimiterSuffix}]: ${values.join(this.delimiter)}`;
|
|
211
|
+
// Get keys from first object
|
|
212
|
+
const firstKeys = Object.keys(arr[0]).sort();
|
|
213
|
+
|
|
214
|
+
// All objects must have same keys
|
|
215
|
+
if (
|
|
216
|
+
!arr.every((item) => {
|
|
217
|
+
const keys = Object.keys(item).sort();
|
|
218
|
+
return keys.length === firstKeys.length && keys.every((key, i) => key === firstKeys[i]);
|
|
219
|
+
})
|
|
220
|
+
) {
|
|
221
|
+
return false;
|
|
309
222
|
}
|
|
310
223
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
224
|
+
// All values must be primitives (not nested objects/arrays)
|
|
225
|
+
return arr.every((item) =>
|
|
226
|
+
Object.values(item).every(
|
|
227
|
+
(val) =>
|
|
228
|
+
val === null ||
|
|
229
|
+
typeof val === 'string' ||
|
|
230
|
+
typeof val === 'number' ||
|
|
231
|
+
typeof val === 'boolean'
|
|
232
|
+
)
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Check if all array elements are primitives
|
|
238
|
+
*/
|
|
239
|
+
isPrimitiveArray(arr) {
|
|
240
|
+
return arr.every(
|
|
241
|
+
(item) =>
|
|
242
|
+
item === null ||
|
|
243
|
+
typeof item === 'string' ||
|
|
244
|
+
typeof item === 'number' ||
|
|
245
|
+
typeof item === 'boolean'
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Check if value is plain object
|
|
251
|
+
*/
|
|
252
|
+
isPlainObject(val) {
|
|
253
|
+
return (
|
|
254
|
+
val !== null &&
|
|
255
|
+
typeof val === 'object' &&
|
|
256
|
+
!Array.isArray(val) &&
|
|
257
|
+
Object.getPrototypeOf(val) === Object.prototype
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Encode tabular array (Spec: Arrays of Objects)
|
|
263
|
+
*/
|
|
264
|
+
encodeTabularArray(arr, depth = 0) {
|
|
265
|
+
const prefix = this.indentChar.repeat(depth);
|
|
266
|
+
const keys = Object.keys(arr[0]).sort();
|
|
267
|
+
const length = arr.length;
|
|
268
|
+
|
|
269
|
+
// Build length marker
|
|
270
|
+
const delimiterSuffix = this.delimiter === ',' ? '' : this.delimiter;
|
|
271
|
+
const lengthPrefix = this.lengthMarker ? `#${length}` : `${length}`;
|
|
272
|
+
|
|
273
|
+
// Header: items[N]{field1,field2}:
|
|
274
|
+
const fieldList = keys.join(this.delimiter);
|
|
275
|
+
const header = `[${lengthPrefix}${delimiterSuffix}]{${fieldList}}:`;
|
|
276
|
+
|
|
277
|
+
const lines = [prefix + header];
|
|
278
|
+
|
|
279
|
+
// Data rows
|
|
280
|
+
for (const item of arr) {
|
|
281
|
+
const values = keys.map((key) => {
|
|
282
|
+
const val = this.normalizeValue(item[key]);
|
|
283
|
+
if (val === null) return 'null';
|
|
284
|
+
if (typeof val === 'boolean') return String(val);
|
|
285
|
+
if (typeof val === 'number') return this.encodeNumber(val);
|
|
286
|
+
if (typeof val === 'string') return this.encodeString(val);
|
|
287
|
+
return 'null';
|
|
288
|
+
});
|
|
363
289
|
|
|
364
|
-
|
|
290
|
+
lines.push(prefix + this.indentChar + values.join(this.delimiter));
|
|
365
291
|
}
|
|
366
292
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
293
|
+
return lines.join('\n');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Encode primitive array (Spec: Primitive Arrays)
|
|
298
|
+
*/
|
|
299
|
+
encodePrimitiveArray(arr) {
|
|
300
|
+
const length = arr.length;
|
|
301
|
+
const delimiterSuffix = this.delimiter === ',' ? '' : this.delimiter;
|
|
302
|
+
const lengthPrefix = this.lengthMarker ? `#${length}` : `${length}`;
|
|
303
|
+
|
|
304
|
+
const values = arr.map((item) => {
|
|
305
|
+
const val = this.normalizeValue(item);
|
|
306
|
+
if (val === null) return 'null';
|
|
307
|
+
if (typeof val === 'boolean') return String(val);
|
|
308
|
+
if (typeof val === 'number') return this.encodeNumber(val);
|
|
309
|
+
if (typeof val === 'string') return this.encodeString(val);
|
|
310
|
+
return 'null';
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
return `[${lengthPrefix}${delimiterSuffix}]: ${values.join(this.delimiter)}`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Encode list array (Spec: Mixed and Non-Uniform Arrays)
|
|
318
|
+
*/
|
|
319
|
+
encodeListArray(arr, depth = 0) {
|
|
320
|
+
const prefix = this.indentChar.repeat(depth);
|
|
321
|
+
const length = arr.length;
|
|
322
|
+
const delimiterSuffix = this.delimiter === ',' ? '' : this.delimiter;
|
|
323
|
+
const lengthPrefix = this.lengthMarker ? `#${length}` : `${length}`;
|
|
324
|
+
|
|
325
|
+
const lines = [`${prefix}[${lengthPrefix}${delimiterSuffix}]:`];
|
|
326
|
+
|
|
327
|
+
for (const item of arr) {
|
|
328
|
+
const normalized = this.normalizeValue(item);
|
|
329
|
+
|
|
330
|
+
if (this.isPlainObject(normalized)) {
|
|
331
|
+
// Object in list: first field on hyphen line
|
|
332
|
+
const keys = Object.keys(normalized);
|
|
333
|
+
if (keys.length > 0) {
|
|
334
|
+
const firstKey = keys[0];
|
|
335
|
+
const quotedKey = this.needsQuotingAsKey(firstKey) ? `"${firstKey}"` : firstKey;
|
|
336
|
+
const firstValue = this.encodeValue(normalized[firstKey], depth + 1);
|
|
337
|
+
|
|
338
|
+
lines.push(`${prefix}${this.indentChar}- ${quotedKey}: ${firstValue}`);
|
|
339
|
+
|
|
340
|
+
// Remaining fields at same indentation
|
|
341
|
+
for (let i = 1; i < keys.length; i++) {
|
|
342
|
+
const key = keys[i];
|
|
343
|
+
const quotedK = this.needsQuotingAsKey(key) ? `"${key}"` : key;
|
|
344
|
+
const val = this.encodeValue(normalized[key], depth + 1);
|
|
345
|
+
lines.push(`${prefix}${this.indentChar} ${quotedK}: ${val}`);
|
|
346
|
+
}
|
|
375
347
|
}
|
|
348
|
+
} else if (Array.isArray(normalized)) {
|
|
349
|
+
// Array in list
|
|
350
|
+
const encoded = this.encodeArray(normalized, depth + 1);
|
|
351
|
+
lines.push(`${prefix}${this.indentChar}- ${encoded}`);
|
|
352
|
+
} else {
|
|
353
|
+
// Primitive in list
|
|
354
|
+
const encoded = this.encodeValue(normalized, depth + 1);
|
|
355
|
+
lines.push(`${prefix}${this.indentChar}- ${encoded}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
376
358
|
|
|
377
|
-
|
|
359
|
+
return lines.join('\n');
|
|
360
|
+
}
|
|
378
361
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
362
|
+
/**
|
|
363
|
+
* Decode TOON string to JavaScript value (Spec v1.3)
|
|
364
|
+
*/
|
|
365
|
+
decode(toonString, options = {}) {
|
|
366
|
+
const _strict = options.strict !== false;
|
|
367
|
+
const lines = toonString.split('\n');
|
|
383
368
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const colonIndex = trimmed.indexOf(':');
|
|
387
|
-
const key = trimmed.substring(0, colonIndex).trim();
|
|
388
|
-
const valueStr = trimmed.substring(colonIndex + 1).trim();
|
|
389
|
-
|
|
390
|
-
// Check if it's an array header
|
|
391
|
-
if (key.includes('[') && key.includes(']')) {
|
|
392
|
-
return this.parseArray(lines, startIndex, expectedDepth);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Check if value is on same line
|
|
396
|
-
if (valueStr) {
|
|
397
|
-
return {
|
|
398
|
-
value: { [this.unquoteKey(key)]: this.parsePrimitive(valueStr) },
|
|
399
|
-
nextIndex: startIndex + 1
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Value is on next lines (nested object)
|
|
404
|
-
const result = this.parseObject(lines, startIndex, expectedDepth);
|
|
405
|
-
return result;
|
|
406
|
-
}
|
|
369
|
+
return this.parseValue(lines, 0, 0).value;
|
|
370
|
+
}
|
|
407
371
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
372
|
+
/**
|
|
373
|
+
* Parse value from lines starting at index
|
|
374
|
+
*/
|
|
375
|
+
parseValue(lines, startIndex, expectedDepth) {
|
|
376
|
+
const line = lines[startIndex];
|
|
412
377
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
value: this.parsePrimitive(trimmed),
|
|
416
|
-
nextIndex: startIndex + 1
|
|
417
|
-
};
|
|
378
|
+
if (!line) {
|
|
379
|
+
return { value: null, nextIndex: startIndex + 1 };
|
|
418
380
|
}
|
|
419
381
|
|
|
420
|
-
|
|
421
|
-
* Parse array from TOON format
|
|
422
|
-
*/
|
|
423
|
-
parseArray(lines, startIndex, depth) {
|
|
424
|
-
const line = lines[startIndex];
|
|
425
|
-
const match = line.match(/\[([#]?\d+)([,\t|])?\]\{([^}]+)\}:/);
|
|
426
|
-
|
|
427
|
-
if (match) {
|
|
428
|
-
// Tabular array
|
|
429
|
-
const lengthStr = match[1].replace('#', '');
|
|
430
|
-
const expectedLength = parseInt(lengthStr);
|
|
431
|
-
const delimiter = match[2] || ',';
|
|
432
|
-
const fields = match[3].split(delimiter);
|
|
433
|
-
|
|
434
|
-
const items = [];
|
|
435
|
-
let index = startIndex + 1;
|
|
436
|
-
|
|
437
|
-
for (let i = 0; i < expectedLength && index < lines.length; i++) {
|
|
438
|
-
const dataLine = lines[index];
|
|
439
|
-
const values = this.splitByDelimiter(dataLine.trim(), delimiter);
|
|
440
|
-
|
|
441
|
-
const obj = {};
|
|
442
|
-
fields.forEach((field, idx) => {
|
|
443
|
-
obj[field] = this.parsePrimitive(values[idx] || 'null');
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
items.push(obj);
|
|
447
|
-
index++;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
return { value: items, nextIndex: index };
|
|
451
|
-
}
|
|
382
|
+
const trimmed = line.trim();
|
|
452
383
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
// Primitive array
|
|
457
|
-
const delimiter = primitiveMatch[2] || ',';
|
|
458
|
-
const valueStr = primitiveMatch[3];
|
|
459
|
-
const values = this.splitByDelimiter(valueStr, delimiter);
|
|
460
|
-
|
|
461
|
-
return {
|
|
462
|
-
value: values.map(v => this.parsePrimitive(v)),
|
|
463
|
-
nextIndex: startIndex + 1
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// List array
|
|
468
|
-
return this.parseListArray(lines, startIndex, depth);
|
|
384
|
+
// Empty line
|
|
385
|
+
if (trimmed === '') {
|
|
386
|
+
return { value: null, nextIndex: startIndex + 1 };
|
|
469
387
|
}
|
|
470
388
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
let index = startIndex + 1;
|
|
477
|
-
|
|
478
|
-
while (index < lines.length) {
|
|
479
|
-
const line = lines[index];
|
|
480
|
-
const currentDepth = this.getIndentDepth(line);
|
|
481
|
-
|
|
482
|
-
if (currentDepth <= depth) break;
|
|
483
|
-
|
|
484
|
-
if (line.trim().startsWith('- ')) {
|
|
485
|
-
const result = this.parseListItem(lines, index, depth + 1);
|
|
486
|
-
items.push(result.value);
|
|
487
|
-
index = result.nextIndex;
|
|
488
|
-
} else {
|
|
489
|
-
break;
|
|
490
|
-
}
|
|
491
|
-
}
|
|
389
|
+
// Check for key-value pair
|
|
390
|
+
if (trimmed.includes(':')) {
|
|
391
|
+
const colonIndex = trimmed.indexOf(':');
|
|
392
|
+
const key = trimmed.substring(0, colonIndex).trim();
|
|
393
|
+
const valueStr = trimmed.substring(colonIndex + 1).trim();
|
|
492
394
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
* Parse list item
|
|
498
|
-
*/
|
|
499
|
-
parseListItem(lines, startIndex, depth) {
|
|
500
|
-
const line = lines[startIndex];
|
|
501
|
-
const content = line.trim().substring(2); // Remove "- "
|
|
502
|
-
|
|
503
|
-
if (content.includes(':')) {
|
|
504
|
-
// Object
|
|
505
|
-
const result = this.parseObject(lines, startIndex, depth);
|
|
506
|
-
return result;
|
|
507
|
-
}
|
|
395
|
+
// Check if it's an array header
|
|
396
|
+
if (key.includes('[') && key.includes(']')) {
|
|
397
|
+
return this.parseArray(lines, startIndex, expectedDepth);
|
|
398
|
+
}
|
|
508
399
|
|
|
509
|
-
|
|
400
|
+
// Check if value is on same line
|
|
401
|
+
if (valueStr) {
|
|
510
402
|
return {
|
|
511
|
-
|
|
512
|
-
|
|
403
|
+
value: { [this.unquoteKey(key)]: this.parsePrimitive(valueStr) },
|
|
404
|
+
nextIndex: startIndex + 1,
|
|
513
405
|
};
|
|
514
|
-
|
|
406
|
+
}
|
|
515
407
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
parseObject(lines, startIndex, depth) {
|
|
520
|
-
const obj = {};
|
|
521
|
-
let index = startIndex;
|
|
522
|
-
|
|
523
|
-
while (index < lines.length) {
|
|
524
|
-
const line = lines[index];
|
|
525
|
-
const trimmed = line.trim();
|
|
526
|
-
const currentDepth = this.getIndentDepth(line);
|
|
527
|
-
|
|
528
|
-
if (currentDepth < depth && index !== startIndex) break;
|
|
529
|
-
if (!trimmed || !trimmed.includes(':')) {
|
|
530
|
-
index++;
|
|
531
|
-
continue;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
const colonIndex = trimmed.indexOf(':');
|
|
535
|
-
const key = trimmed.substring(0, colonIndex).trim();
|
|
536
|
-
const valueStr = trimmed.substring(colonIndex + 1).trim();
|
|
537
|
-
|
|
538
|
-
const unquotedKey = this.unquoteKey(key);
|
|
539
|
-
|
|
540
|
-
if (valueStr) {
|
|
541
|
-
// Value on same line
|
|
542
|
-
obj[unquotedKey] = this.parsePrimitive(valueStr);
|
|
543
|
-
index++;
|
|
544
|
-
} else {
|
|
545
|
-
// Value on next lines
|
|
546
|
-
const result = this.parseValue(lines, index + 1, depth + 1);
|
|
547
|
-
obj[unquotedKey] = result.value;
|
|
548
|
-
index = result.nextIndex;
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
return { value: obj, nextIndex: index };
|
|
408
|
+
// Value is on next lines (nested object)
|
|
409
|
+
const result = this.parseObject(lines, startIndex, expectedDepth);
|
|
410
|
+
return result;
|
|
553
411
|
}
|
|
554
412
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
const trimmed = str.trim();
|
|
560
|
-
|
|
561
|
-
if (trimmed === 'null') return null;
|
|
562
|
-
if (trimmed === 'true') return true;
|
|
563
|
-
if (trimmed === 'false') return false;
|
|
413
|
+
// List item
|
|
414
|
+
if (trimmed.startsWith('- ')) {
|
|
415
|
+
return this.parseListItem(lines, startIndex, expectedDepth);
|
|
416
|
+
}
|
|
564
417
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
418
|
+
// Primitive value
|
|
419
|
+
return {
|
|
420
|
+
value: this.parsePrimitive(trimmed),
|
|
421
|
+
nextIndex: startIndex + 1,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Parse array from TOON format
|
|
427
|
+
*/
|
|
428
|
+
parseArray(lines, startIndex, depth) {
|
|
429
|
+
const line = lines[startIndex];
|
|
430
|
+
const match = line.match(/\[([#]?\d+)([,\t|])?\]\{([^}]+)\}:/);
|
|
431
|
+
|
|
432
|
+
if (match) {
|
|
433
|
+
// Tabular array
|
|
434
|
+
const lengthStr = match[1].replace('#', '');
|
|
435
|
+
const expectedLength = parseInt(lengthStr);
|
|
436
|
+
const delimiter = match[2] || ',';
|
|
437
|
+
const fields = match[3].split(delimiter);
|
|
438
|
+
|
|
439
|
+
const items = [];
|
|
440
|
+
let index = startIndex + 1;
|
|
441
|
+
|
|
442
|
+
for (let i = 0; i < expectedLength && index < lines.length; i++) {
|
|
443
|
+
const dataLine = lines[index];
|
|
444
|
+
const values = this.splitByDelimiter(dataLine.trim(), delimiter);
|
|
569
445
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
446
|
+
const obj = {};
|
|
447
|
+
fields.forEach((field, idx) => {
|
|
448
|
+
obj[field] = this.parsePrimitive(values[idx] || 'null');
|
|
449
|
+
});
|
|
574
450
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
451
|
+
items.push(obj);
|
|
452
|
+
index++;
|
|
453
|
+
}
|
|
578
454
|
|
|
579
|
-
|
|
580
|
-
* Unescape string
|
|
581
|
-
*/
|
|
582
|
-
unescapeString(str) {
|
|
583
|
-
return str
|
|
584
|
-
.replace(/\\n/g, '\n')
|
|
585
|
-
.replace(/\\r/g, '\r')
|
|
586
|
-
.replace(/\\t/g, '\t')
|
|
587
|
-
.replace(/\\"/g, '"')
|
|
588
|
-
.replace(/\\\\/g, '\\');
|
|
455
|
+
return { value: items, nextIndex: index };
|
|
589
456
|
}
|
|
590
457
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
458
|
+
// Primitive or list array
|
|
459
|
+
const primitiveMatch = line.match(/\[([#]?\d+)([,\t|])?\]: (.+)/);
|
|
460
|
+
if (primitiveMatch) {
|
|
461
|
+
// Primitive array
|
|
462
|
+
const delimiter = primitiveMatch[2] || ',';
|
|
463
|
+
const valueStr = primitiveMatch[3];
|
|
464
|
+
const values = this.splitByDelimiter(valueStr, delimiter);
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
value: values.map((v) => this.parsePrimitive(v)),
|
|
468
|
+
nextIndex: startIndex + 1,
|
|
469
|
+
};
|
|
599
470
|
}
|
|
600
471
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
472
|
+
// List array
|
|
473
|
+
return this.parseListArray(lines, startIndex, depth);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Parse list-format array
|
|
478
|
+
*/
|
|
479
|
+
parseListArray(lines, startIndex, depth) {
|
|
480
|
+
const items = [];
|
|
481
|
+
let index = startIndex + 1;
|
|
482
|
+
|
|
483
|
+
while (index < lines.length) {
|
|
484
|
+
const line = lines[index];
|
|
485
|
+
const currentDepth = this.getIndentDepth(line);
|
|
486
|
+
|
|
487
|
+
if (currentDepth <= depth) break;
|
|
488
|
+
|
|
489
|
+
if (line.trim().startsWith('- ')) {
|
|
490
|
+
const result = this.parseListItem(lines, index, depth + 1);
|
|
491
|
+
items.push(result.value);
|
|
492
|
+
index = result.nextIndex;
|
|
493
|
+
} else {
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
607
496
|
}
|
|
608
497
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
*/
|
|
612
|
-
splitByDelimiter(str, delimiter) {
|
|
613
|
-
const parts = [];
|
|
614
|
-
let current = '';
|
|
615
|
-
let inQuotes = false;
|
|
616
|
-
|
|
617
|
-
for (let i = 0; i < str.length; i++) {
|
|
618
|
-
const char = str[i];
|
|
619
|
-
const nextChar = str[i + 1];
|
|
620
|
-
|
|
621
|
-
if (char === '\\' && inQuotes) {
|
|
622
|
-
current += char + (nextChar || '');
|
|
623
|
-
i++; // Skip next char
|
|
624
|
-
continue;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
if (char === '"') {
|
|
628
|
-
inQuotes = !inQuotes;
|
|
629
|
-
current += char;
|
|
630
|
-
continue;
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
if (char === delimiter && !inQuotes) {
|
|
634
|
-
parts.push(current.trim());
|
|
635
|
-
current = '';
|
|
636
|
-
continue;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
current += char;
|
|
640
|
-
}
|
|
498
|
+
return { value: items, nextIndex: index };
|
|
499
|
+
}
|
|
641
500
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
501
|
+
/**
|
|
502
|
+
* Parse list item
|
|
503
|
+
*/
|
|
504
|
+
parseListItem(lines, startIndex, depth) {
|
|
505
|
+
const line = lines[startIndex];
|
|
506
|
+
const content = line.trim().substring(2); // Remove "- "
|
|
645
507
|
|
|
646
|
-
|
|
508
|
+
if (content.includes(':')) {
|
|
509
|
+
// Object
|
|
510
|
+
const result = this.parseObject(lines, startIndex, depth);
|
|
511
|
+
return result;
|
|
647
512
|
}
|
|
648
513
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
514
|
+
// Primitive
|
|
515
|
+
return {
|
|
516
|
+
value: this.parsePrimitive(content),
|
|
517
|
+
nextIndex: startIndex + 1,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Parse object from lines
|
|
523
|
+
*/
|
|
524
|
+
parseObject(lines, startIndex, depth) {
|
|
525
|
+
const obj = {};
|
|
526
|
+
let index = startIndex;
|
|
527
|
+
|
|
528
|
+
while (index < lines.length) {
|
|
529
|
+
const line = lines[index];
|
|
530
|
+
const trimmed = line.trim();
|
|
531
|
+
const currentDepth = this.getIndentDepth(line);
|
|
532
|
+
|
|
533
|
+
if (currentDepth < depth && index !== startIndex) break;
|
|
534
|
+
if (!trimmed || !trimmed.includes(':')) {
|
|
535
|
+
index++;
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const colonIndex = trimmed.indexOf(':');
|
|
540
|
+
const key = trimmed.substring(0, colonIndex).trim();
|
|
541
|
+
const valueStr = trimmed.substring(colonIndex + 1).trim();
|
|
542
|
+
|
|
543
|
+
const unquotedKey = this.unquoteKey(key);
|
|
544
|
+
|
|
545
|
+
if (valueStr) {
|
|
546
|
+
// Value on same line
|
|
547
|
+
obj[unquotedKey] = this.parsePrimitive(valueStr);
|
|
548
|
+
index++;
|
|
549
|
+
} else {
|
|
550
|
+
// Value on next lines
|
|
551
|
+
const result = this.parseValue(lines, index + 1, depth + 1);
|
|
552
|
+
obj[unquotedKey] = result.value;
|
|
553
|
+
index = result.nextIndex;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
658
556
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
if (char === '}') braceDepth--;
|
|
662
|
-
if (char === '[') bracketDepth++;
|
|
663
|
-
if (char === ']') bracketDepth--;
|
|
557
|
+
return { value: obj, nextIndex: index };
|
|
558
|
+
}
|
|
664
559
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
560
|
+
/**
|
|
561
|
+
* Parse primitive value
|
|
562
|
+
*/
|
|
563
|
+
parsePrimitive(str) {
|
|
564
|
+
const trimmed = str.trim();
|
|
668
565
|
|
|
669
|
-
|
|
670
|
-
|
|
566
|
+
if (trimmed === 'null') return null;
|
|
567
|
+
if (trimmed === 'true') return true;
|
|
568
|
+
if (trimmed === 'false') return false;
|
|
671
569
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
};
|
|
570
|
+
// Number
|
|
571
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
|
|
572
|
+
return parseFloat(trimmed);
|
|
676
573
|
}
|
|
677
574
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
estimateTokens(toonString) {
|
|
682
|
-
// Simple estimation: ~4 chars per token (GPT-4 average)
|
|
683
|
-
return Math.ceil(toonString.length / 4);
|
|
575
|
+
// Quoted string
|
|
576
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
577
|
+
return this.unescapeString(trimmed.slice(1, -1));
|
|
684
578
|
}
|
|
685
579
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
580
|
+
// Unquoted string
|
|
581
|
+
return trimmed;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Unescape string
|
|
586
|
+
*/
|
|
587
|
+
unescapeString(str) {
|
|
588
|
+
return str
|
|
589
|
+
.replace(/\\n/g, '\n')
|
|
590
|
+
.replace(/\\r/g, '\r')
|
|
591
|
+
.replace(/\\t/g, '\t')
|
|
592
|
+
.replace(/\\"/g, '"')
|
|
593
|
+
.replace(/\\\\/g, '\\');
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Unquote key
|
|
598
|
+
*/
|
|
599
|
+
unquoteKey(key) {
|
|
600
|
+
if (key.startsWith('"') && key.endsWith('"')) {
|
|
601
|
+
return this.unescapeString(key.slice(1, -1));
|
|
602
|
+
}
|
|
603
|
+
return key;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Get indentation depth
|
|
608
|
+
*/
|
|
609
|
+
getIndentDepth(line) {
|
|
610
|
+
const match = line.match(/^( *)/);
|
|
611
|
+
return match ? Math.floor(match[1].length / this.indent) : 0;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Split by delimiter (respecting quotes)
|
|
616
|
+
*/
|
|
617
|
+
splitByDelimiter(str, delimiter) {
|
|
618
|
+
const parts = [];
|
|
619
|
+
let current = '';
|
|
620
|
+
let inQuotes = false;
|
|
621
|
+
|
|
622
|
+
for (let i = 0; i < str.length; i++) {
|
|
623
|
+
const char = str[i];
|
|
624
|
+
const nextChar = str[i + 1];
|
|
625
|
+
|
|
626
|
+
if (char === '\\' && inQuotes) {
|
|
627
|
+
current += char + (nextChar || '');
|
|
628
|
+
i++; // Skip next char
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (char === '"') {
|
|
633
|
+
inQuotes = !inQuotes;
|
|
634
|
+
current += char;
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (char === delimiter && !inQuotes) {
|
|
639
|
+
parts.push(current.trim());
|
|
640
|
+
current = '';
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
current += char;
|
|
693
645
|
}
|
|
694
646
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
*/
|
|
698
|
-
minify(toonString) {
|
|
699
|
-
return toonString
|
|
700
|
-
.split('\n')
|
|
701
|
-
.map(line => line.trim())
|
|
702
|
-
.filter(line => line.length > 0)
|
|
703
|
-
.join('\n');
|
|
647
|
+
if (current) {
|
|
648
|
+
parts.push(current.trim());
|
|
704
649
|
}
|
|
705
650
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
*/
|
|
709
|
-
compareWithJSON(data) {
|
|
710
|
-
const toon = this.encode(data);
|
|
711
|
-
const json = JSON.stringify(data, null, 2);
|
|
651
|
+
return parts;
|
|
652
|
+
}
|
|
712
653
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
654
|
+
/**
|
|
655
|
+
* Validate TOON string (enhanced)
|
|
656
|
+
*/
|
|
657
|
+
validate(toonString) {
|
|
658
|
+
const errors = [];
|
|
717
659
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
660
|
+
// Check balanced braces/brackets
|
|
661
|
+
let braceDepth = 0;
|
|
662
|
+
let bracketDepth = 0;
|
|
663
|
+
|
|
664
|
+
for (const char of toonString) {
|
|
665
|
+
if (char === '{') braceDepth++;
|
|
666
|
+
if (char === '}') braceDepth--;
|
|
667
|
+
if (char === '[') bracketDepth++;
|
|
668
|
+
if (char === ']') bracketDepth--;
|
|
669
|
+
|
|
670
|
+
if (braceDepth < 0) errors.push('Unbalanced braces: extra closing brace');
|
|
671
|
+
if (bracketDepth < 0) errors.push('Unbalanced brackets: extra closing bracket');
|
|
728
672
|
}
|
|
673
|
+
|
|
674
|
+
if (braceDepth > 0) errors.push('Unbalanced braces: missing closing braces');
|
|
675
|
+
if (bracketDepth > 0) errors.push('Unbalanced brackets: missing closing brackets');
|
|
676
|
+
|
|
677
|
+
return {
|
|
678
|
+
valid: errors.length === 0,
|
|
679
|
+
errors,
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Estimate tokens (GPT-4 compatible)
|
|
685
|
+
*/
|
|
686
|
+
estimateTokens(toonString) {
|
|
687
|
+
// Simple estimation: ~4 chars per token (GPT-4 average)
|
|
688
|
+
return Math.ceil(toonString.length / 4);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Optimize TOON output
|
|
693
|
+
*/
|
|
694
|
+
optimize(toonString) {
|
|
695
|
+
const lines = toonString.split('\n');
|
|
696
|
+
const optimized = lines.map((line) => line.trimEnd()).join('\n');
|
|
697
|
+
return optimized.replace(/\n{3,}/g, '\n\n');
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Minify TOON output
|
|
702
|
+
*/
|
|
703
|
+
minify(toonString) {
|
|
704
|
+
return toonString
|
|
705
|
+
.split('\n')
|
|
706
|
+
.map((line) => line.trim())
|
|
707
|
+
.filter((line) => line.length > 0)
|
|
708
|
+
.join('\n');
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Compare with JSON (token savings)
|
|
713
|
+
*/
|
|
714
|
+
compareWithJSON(data) {
|
|
715
|
+
const toon = this.encode(data);
|
|
716
|
+
const json = JSON.stringify(data, null, 2);
|
|
717
|
+
|
|
718
|
+
const toonSize = toon.length;
|
|
719
|
+
const jsonSize = json.length;
|
|
720
|
+
const savings = jsonSize - toonSize;
|
|
721
|
+
const savingsPercentage = ((savings / jsonSize) * 100).toFixed(1);
|
|
722
|
+
|
|
723
|
+
return {
|
|
724
|
+
toon,
|
|
725
|
+
json,
|
|
726
|
+
toonSize,
|
|
727
|
+
jsonSize,
|
|
728
|
+
savings,
|
|
729
|
+
savingsPercentage: parseFloat(savingsPercentage),
|
|
730
|
+
toonTokens: this.estimateTokens(toon),
|
|
731
|
+
jsonTokens: this.estimateTokens(json),
|
|
732
|
+
};
|
|
733
|
+
}
|
|
729
734
|
}
|
|
730
735
|
|
|
731
736
|
export default ToonFormatterV13;
|