ctxman 3.0.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/custom-profiles.example.json +20 -0
- package/.ctxman/llm-profiles.json +131 -0
- package/.ctxman/logs/ctxman-2026-07-02.log +156 -0
- 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
- package/.ctxman/wizard-profiles/code-review/.contextignore +41 -0
- package/.ctxman/wizard-profiles/code-review/.contextinclude +30 -0
- package/.ctxman/wizard-profiles/code-review/.methodignore +26 -0
- package/.ctxman/wizard-profiles/code-review/.methodinclude +47 -0
- package/.ctxman/wizard-profiles/code-review/profile.json +26 -0
- package/.ctxman/wizard-profiles/documentation/.contextignore +21 -0
- package/.ctxman/wizard-profiles/documentation/.contextinclude +24 -0
- package/.ctxman/wizard-profiles/documentation/.methodignore +19 -0
- package/.ctxman/wizard-profiles/documentation/.methodinclude +44 -0
- package/.ctxman/wizard-profiles/documentation/profile.json +27 -0
- package/.ctxman/wizard-profiles/full/.contextignore +23 -0
- package/.ctxman/wizard-profiles/full/.contextinclude +38 -0
- package/.ctxman/wizard-profiles/full/.methodignore +5 -0
- package/.ctxman/wizard-profiles/full/.methodinclude +5 -0
- package/.ctxman/wizard-profiles/full/profile.json +28 -0
- package/.ctxman/wizard-profiles/llm-explain/.contextignore +25 -0
- package/.ctxman/wizard-profiles/llm-explain/.contextinclude +33 -0
- package/.ctxman/wizard-profiles/llm-explain/.methodignore +19 -0
- package/.ctxman/wizard-profiles/llm-explain/.methodinclude +48 -0
- package/.ctxman/wizard-profiles/llm-explain/profile.json +27 -0
- package/.ctxman/wizard-profiles/minimal/.contextignore +27 -0
- package/.ctxman/wizard-profiles/minimal/.contextinclude +14 -0
- package/.ctxman/wizard-profiles/minimal/.methodignore +22 -0
- package/.ctxman/wizard-profiles/minimal/.methodinclude +18 -0
- package/.ctxman/wizard-profiles/minimal/profile.json +26 -0
- package/.ctxman/wizard-profiles/security-audit/.contextignore +36 -0
- package/.ctxman/wizard-profiles/security-audit/.contextinclude +37 -0
- package/.ctxman/wizard-profiles/security-audit/.methodignore +29 -0
- package/.ctxman/wizard-profiles/security-audit/.methodinclude +60 -0
- package/.ctxman/wizard-profiles/security-audit/profile.json +34 -0
- package/README.md +1167 -0
- package/bin/cli.js +650 -0
- package/bin/cm-ask.js +95 -0
- package/bin/cm-gitingest.js +134 -0
- package/bin/cm-update.js +175 -0
- package/bin/mcp-server.js +31 -0
- package/ctxman.js +236 -0
- package/examples/README.md +298 -0
- package/examples/custom-llm-profiles.example.json +19 -0
- package/examples/wizard-profiles/code-review/.contextignore +41 -0
- package/examples/wizard-profiles/code-review/.contextinclude +30 -0
- package/examples/wizard-profiles/code-review/.methodignore +26 -0
- package/examples/wizard-profiles/code-review/.methodinclude +47 -0
- package/examples/wizard-profiles/code-review/profile.json +26 -0
- package/examples/wizard-profiles/documentation/.contextignore +21 -0
- package/examples/wizard-profiles/documentation/.contextinclude +24 -0
- package/examples/wizard-profiles/documentation/.methodignore +19 -0
- package/examples/wizard-profiles/documentation/.methodinclude +44 -0
- package/examples/wizard-profiles/documentation/profile.json +27 -0
- package/examples/wizard-profiles/full/.contextignore +23 -0
- package/examples/wizard-profiles/full/.contextinclude +38 -0
- package/examples/wizard-profiles/full/.methodignore +5 -0
- package/examples/wizard-profiles/full/.methodinclude +5 -0
- package/examples/wizard-profiles/full/profile.json +28 -0
- package/examples/wizard-profiles/llm-explain/.contextignore +25 -0
- package/examples/wizard-profiles/llm-explain/.contextinclude +33 -0
- package/examples/wizard-profiles/llm-explain/.methodignore +19 -0
- package/examples/wizard-profiles/llm-explain/.methodinclude +48 -0
- package/examples/wizard-profiles/llm-explain/profile.json +27 -0
- package/examples/wizard-profiles/minimal/.contextignore +27 -0
- package/examples/wizard-profiles/minimal/.contextinclude +14 -0
- package/examples/wizard-profiles/minimal/.methodignore +22 -0
- package/examples/wizard-profiles/minimal/.methodinclude +18 -0
- package/examples/wizard-profiles/minimal/profile.json +26 -0
- package/examples/wizard-profiles/security-audit/.contextignore +36 -0
- package/examples/wizard-profiles/security-audit/.contextinclude +37 -0
- package/examples/wizard-profiles/security-audit/.methodignore +29 -0
- package/examples/wizard-profiles/security-audit/.methodinclude +60 -0
- package/examples/wizard-profiles/security-audit/profile.json +34 -0
- package/index.js +68 -0
- package/lib/agents/pr-failure-agent.js +201 -0
- package/lib/analyzers/go-method-analyzer.js +126 -0
- package/lib/analyzers/method-analyzer.js +457 -0
- package/lib/analyzers/token-calculator.js +577 -0
- package/lib/api/mcp/prompts.js +319 -0
- package/lib/api/mcp/resources.js +269 -0
- package/lib/api/mcp/server.js +339 -0
- package/lib/api/mcp/transports/HttpSseTransport.js +63 -0
- package/lib/api/rest/server.js +405 -0
- package/lib/cache/CacheManager.js +388 -0
- package/lib/core/Analyzer.js +180 -0
- package/lib/core/ContextBuilder.js +325 -0
- package/lib/core/Reporter.js +270 -0
- package/lib/core/Scanner.js +129 -0
- package/lib/formatters/format-registry.js +419 -0
- package/lib/formatters/gitingest-formatter.js +746 -0
- package/lib/formatters/toon-formatter-v1.3.js +731 -0
- package/lib/formatters/toon-formatter.js +291 -0
- package/lib/integrations/git/BlameTracker.js +142 -0
- package/lib/integrations/git/DiffAnalyzer.js +260 -0
- package/lib/integrations/git/GitClient.js +336 -0
- package/lib/parsers/gitignore-parser.js +127 -0
- package/lib/parsers/method-filter-parser.js +50 -0
- package/lib/plugins/ExporterPlugin.js +102 -0
- package/lib/plugins/LanguagePlugin.js +100 -0
- package/lib/plugins/PluginManager.js +302 -0
- package/lib/rag/DataSourcePlugin.js +31 -0
- package/lib/rag/EmbeddingProvider.js +75 -0
- package/lib/rag/Indexer.js +83 -0
- package/lib/rag/VectorStoreFactory.js +24 -0
- package/lib/rag/sources/FileDataSource.js +70 -0
- package/lib/rag/stores/BaseVectorStore.js +43 -0
- package/lib/rag/stores/ChromaDBStore.js +85 -0
- package/lib/rag/stores/LanceDBStore.js +109 -0
- package/lib/rag/stores/PineconeStore.js +74 -0
- package/lib/ui/dashboard.js +109 -0
- package/lib/ui/index.js +15 -0
- package/lib/ui/progress-bar.js +110 -0
- package/lib/ui/select-input.js +51 -0
- package/lib/ui/wizard.js +359 -0
- package/lib/utils/clipboard-utils.js +66 -0
- package/lib/utils/config-utils.js +94 -0
- package/lib/utils/error-handler.js +145 -0
- package/lib/utils/file-utils.js +89 -0
- package/lib/utils/format-converter.js +325 -0
- package/lib/utils/git-utils.js +337 -0
- package/lib/utils/llm-detector.js +409 -0
- package/lib/utils/logger.js +278 -0
- package/lib/utils/token-utils.js +200 -0
- package/lib/utils/tokenizer-adapter.js +454 -0
- package/lib/utils/updater.js +565 -0
- package/lib/watch/FileWatcher.js +208 -0
- package/lib/watch/IncrementalAnalyzer.js +206 -0
- package/package.json +118 -0
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOON Format Encoder/Decoder - Spec v1.3 Compliant
|
|
3
|
+
*
|
|
4
|
+
* Full implementation of TOON (Tabular Object Oriented Notation)
|
|
5
|
+
* Following official spec: https://github.com/toon-format/spec
|
|
6
|
+
*
|
|
7
|
+
* Spec Version: 1.3 (2025-10-31)
|
|
8
|
+
* Features:
|
|
9
|
+
* - Array length markers [N]
|
|
10
|
+
* - Delimiter options: comma, tab, pipe
|
|
11
|
+
* - Length marker prefix: # (optional)
|
|
12
|
+
* - List format for non-uniform arrays
|
|
13
|
+
* - Smart quoting rules
|
|
14
|
+
* - Type normalizations
|
|
15
|
+
* - Full decoder support
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
class ToonFormatterV13 {
|
|
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
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Date → ISO string
|
|
81
|
+
if (value instanceof Date) {
|
|
82
|
+
return value.toISOString();
|
|
83
|
+
}
|
|
84
|
+
|
|
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}"`;
|
|
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];
|
|
149
|
+
|
|
150
|
+
const encoded = this.encodeValue(value, depth + 1);
|
|
151
|
+
|
|
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);
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
lines.push(`${prefix}${quotedKey}: ${encoded}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
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}]:`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check if tabular format (uniform objects with primitive values)
|
|
187
|
+
if (this.isTabularArray(arr)) {
|
|
188
|
+
return this.encodeTabularArray(arr, depth);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check if primitive array (inline)
|
|
192
|
+
if (this.isPrimitiveArray(arr)) {
|
|
193
|
+
return this.encodePrimitiveArray(arr);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Non-uniform or complex: use list format
|
|
197
|
+
return this.encodeListArray(arr, depth);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Check if array is suitable for tabular format
|
|
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
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Check if all array elements are primitives
|
|
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
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Check if value is plain object
|
|
248
|
+
*/
|
|
249
|
+
isPlainObject(val) {
|
|
250
|
+
return val !== null &&
|
|
251
|
+
typeof val === 'object' &&
|
|
252
|
+
!Array.isArray(val) &&
|
|
253
|
+
Object.getPrototypeOf(val) === Object.prototype;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Encode tabular array (Spec: Arrays of Objects)
|
|
258
|
+
*/
|
|
259
|
+
encodeTabularArray(arr, depth = 0) {
|
|
260
|
+
const prefix = this.indentChar.repeat(depth);
|
|
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
|
+
}
|
|
287
|
+
|
|
288
|
+
return lines.join('\n');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Encode primitive array (Spec: Primitive Arrays)
|
|
293
|
+
*/
|
|
294
|
+
encodePrimitiveArray(arr) {
|
|
295
|
+
const length = arr.length;
|
|
296
|
+
const delimiterSuffix = this.delimiter === ',' ? '' : this.delimiter;
|
|
297
|
+
const lengthPrefix = this.lengthMarker ? `#${length}` : `${length}`;
|
|
298
|
+
|
|
299
|
+
const values = arr.map(item => {
|
|
300
|
+
const val = this.normalizeValue(item);
|
|
301
|
+
if (val === null) return 'null';
|
|
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)}`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Encode list array (Spec: Mixed and Non-Uniform Arrays)
|
|
313
|
+
*/
|
|
314
|
+
encodeListArray(arr, depth = 0) {
|
|
315
|
+
const prefix = this.indentChar.repeat(depth);
|
|
316
|
+
const length = arr.length;
|
|
317
|
+
const delimiterSuffix = this.delimiter === ',' ? '' : this.delimiter;
|
|
318
|
+
const lengthPrefix = this.lengthMarker ? `#${length}` : `${length}`;
|
|
319
|
+
|
|
320
|
+
const lines = [`${prefix}[${lengthPrefix}${delimiterSuffix}]:`];
|
|
321
|
+
|
|
322
|
+
for (const item of arr) {
|
|
323
|
+
const normalized = this.normalizeValue(item);
|
|
324
|
+
|
|
325
|
+
if (this.isPlainObject(normalized)) {
|
|
326
|
+
// Object in list: first field on hyphen line
|
|
327
|
+
const keys = Object.keys(normalized);
|
|
328
|
+
if (keys.length > 0) {
|
|
329
|
+
const firstKey = keys[0];
|
|
330
|
+
const quotedKey = this.needsQuotingAsKey(firstKey) ? `"${firstKey}"` : firstKey;
|
|
331
|
+
const firstValue = this.encodeValue(normalized[firstKey], depth + 1);
|
|
332
|
+
|
|
333
|
+
lines.push(`${prefix}${this.indentChar}- ${quotedKey}: ${firstValue}`);
|
|
334
|
+
|
|
335
|
+
// Remaining fields at same indentation
|
|
336
|
+
for (let i = 1; i < keys.length; i++) {
|
|
337
|
+
const key = keys[i];
|
|
338
|
+
const quotedK = this.needsQuotingAsKey(key) ? `"${key}"` : key;
|
|
339
|
+
const val = this.encodeValue(normalized[key], depth + 1);
|
|
340
|
+
lines.push(`${prefix}${this.indentChar} ${quotedK}: ${val}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
} else if (Array.isArray(normalized)) {
|
|
344
|
+
// Array in list
|
|
345
|
+
const encoded = this.encodeArray(normalized, depth + 1);
|
|
346
|
+
lines.push(`${prefix}${this.indentChar}- ${encoded}`);
|
|
347
|
+
} else {
|
|
348
|
+
// Primitive in list
|
|
349
|
+
const encoded = this.encodeValue(normalized, depth + 1);
|
|
350
|
+
lines.push(`${prefix}${this.indentChar}- ${encoded}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return lines.join('\n');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Decode TOON string to JavaScript value (Spec v1.3)
|
|
359
|
+
*/
|
|
360
|
+
decode(toonString, options = {}) {
|
|
361
|
+
const strict = options.strict !== false;
|
|
362
|
+
const lines = toonString.split('\n');
|
|
363
|
+
|
|
364
|
+
return this.parseValue(lines, 0, 0).value;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Parse value from lines starting at index
|
|
369
|
+
*/
|
|
370
|
+
parseValue(lines, startIndex, expectedDepth) {
|
|
371
|
+
const line = lines[startIndex];
|
|
372
|
+
|
|
373
|
+
if (!line) {
|
|
374
|
+
return { value: null, nextIndex: startIndex + 1 };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const trimmed = line.trim();
|
|
378
|
+
|
|
379
|
+
// Empty line
|
|
380
|
+
if (trimmed === '') {
|
|
381
|
+
return { value: null, nextIndex: startIndex + 1 };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Check for key-value pair
|
|
385
|
+
if (trimmed.includes(':')) {
|
|
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
|
+
}
|
|
407
|
+
|
|
408
|
+
// List item
|
|
409
|
+
if (trimmed.startsWith('- ')) {
|
|
410
|
+
return this.parseListItem(lines, startIndex, expectedDepth);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Primitive value
|
|
414
|
+
return {
|
|
415
|
+
value: this.parsePrimitive(trimmed),
|
|
416
|
+
nextIndex: startIndex + 1
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
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
|
+
}
|
|
452
|
+
|
|
453
|
+
// Primitive or list array
|
|
454
|
+
const primitiveMatch = line.match(/\[([#]?\d+)([,\t|])?\]: (.+)/);
|
|
455
|
+
if (primitiveMatch) {
|
|
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);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Parse list-format array
|
|
473
|
+
*/
|
|
474
|
+
parseListArray(lines, startIndex, depth) {
|
|
475
|
+
const items = [];
|
|
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
|
+
}
|
|
492
|
+
|
|
493
|
+
return { value: items, nextIndex: index };
|
|
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
|
+
}
|
|
508
|
+
|
|
509
|
+
// Primitive
|
|
510
|
+
return {
|
|
511
|
+
value: this.parsePrimitive(content),
|
|
512
|
+
nextIndex: startIndex + 1
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Parse object from lines
|
|
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 };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Parse primitive value
|
|
557
|
+
*/
|
|
558
|
+
parsePrimitive(str) {
|
|
559
|
+
const trimmed = str.trim();
|
|
560
|
+
|
|
561
|
+
if (trimmed === 'null') return null;
|
|
562
|
+
if (trimmed === 'true') return true;
|
|
563
|
+
if (trimmed === 'false') return false;
|
|
564
|
+
|
|
565
|
+
// Number
|
|
566
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
|
|
567
|
+
return parseFloat(trimmed);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Quoted string
|
|
571
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
572
|
+
return this.unescapeString(trimmed.slice(1, -1));
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Unquoted string
|
|
576
|
+
return trimmed;
|
|
577
|
+
}
|
|
578
|
+
|
|
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, '\\');
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Unquote key
|
|
593
|
+
*/
|
|
594
|
+
unquoteKey(key) {
|
|
595
|
+
if (key.startsWith('"') && key.endsWith('"')) {
|
|
596
|
+
return this.unescapeString(key.slice(1, -1));
|
|
597
|
+
}
|
|
598
|
+
return key;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Get indentation depth
|
|
603
|
+
*/
|
|
604
|
+
getIndentDepth(line) {
|
|
605
|
+
const match = line.match(/^( *)/);
|
|
606
|
+
return match ? Math.floor(match[1].length / this.indent) : 0;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Split by delimiter (respecting quotes)
|
|
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
|
+
}
|
|
641
|
+
|
|
642
|
+
if (current) {
|
|
643
|
+
parts.push(current.trim());
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return parts;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Validate TOON string (enhanced)
|
|
651
|
+
*/
|
|
652
|
+
validate(toonString) {
|
|
653
|
+
const errors = [];
|
|
654
|
+
|
|
655
|
+
// Check balanced braces/brackets
|
|
656
|
+
let braceDepth = 0;
|
|
657
|
+
let bracketDepth = 0;
|
|
658
|
+
|
|
659
|
+
for (const char of toonString) {
|
|
660
|
+
if (char === '{') braceDepth++;
|
|
661
|
+
if (char === '}') braceDepth--;
|
|
662
|
+
if (char === '[') bracketDepth++;
|
|
663
|
+
if (char === ']') bracketDepth--;
|
|
664
|
+
|
|
665
|
+
if (braceDepth < 0) errors.push('Unbalanced braces: extra closing brace');
|
|
666
|
+
if (bracketDepth < 0) errors.push('Unbalanced brackets: extra closing bracket');
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (braceDepth > 0) errors.push('Unbalanced braces: missing closing braces');
|
|
670
|
+
if (bracketDepth > 0) errors.push('Unbalanced brackets: missing closing brackets');
|
|
671
|
+
|
|
672
|
+
return {
|
|
673
|
+
valid: errors.length === 0,
|
|
674
|
+
errors
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Estimate tokens (GPT-4 compatible)
|
|
680
|
+
*/
|
|
681
|
+
estimateTokens(toonString) {
|
|
682
|
+
// Simple estimation: ~4 chars per token (GPT-4 average)
|
|
683
|
+
return Math.ceil(toonString.length / 4);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Optimize TOON output
|
|
688
|
+
*/
|
|
689
|
+
optimize(toonString) {
|
|
690
|
+
const lines = toonString.split('\n');
|
|
691
|
+
const optimized = lines.map(line => line.trimEnd()).join('\n');
|
|
692
|
+
return optimized.replace(/\n{3,}/g, '\n\n');
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Minify TOON output
|
|
697
|
+
*/
|
|
698
|
+
minify(toonString) {
|
|
699
|
+
return toonString
|
|
700
|
+
.split('\n')
|
|
701
|
+
.map(line => line.trim())
|
|
702
|
+
.filter(line => line.length > 0)
|
|
703
|
+
.join('\n');
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Compare with JSON (token savings)
|
|
708
|
+
*/
|
|
709
|
+
compareWithJSON(data) {
|
|
710
|
+
const toon = this.encode(data);
|
|
711
|
+
const json = JSON.stringify(data, null, 2);
|
|
712
|
+
|
|
713
|
+
const toonSize = toon.length;
|
|
714
|
+
const jsonSize = json.length;
|
|
715
|
+
const savings = jsonSize - toonSize;
|
|
716
|
+
const savingsPercentage = ((savings / jsonSize) * 100).toFixed(1);
|
|
717
|
+
|
|
718
|
+
return {
|
|
719
|
+
toon,
|
|
720
|
+
json,
|
|
721
|
+
toonSize,
|
|
722
|
+
jsonSize,
|
|
723
|
+
savings,
|
|
724
|
+
savingsPercentage: parseFloat(savingsPercentage),
|
|
725
|
+
toonTokens: this.estimateTokens(toon),
|
|
726
|
+
jsonTokens: this.estimateTokens(json)
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
export default ToonFormatterV13;
|