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.
Files changed (223) hide show
  1. package/.ctxman/llm-profiles.json +1067 -39
  2. package/.ctxman/profiles/backend-team.json +13 -0
  3. package/.ctxman/profiles/default.json +12 -0
  4. package/.ctxman/profiles/devops-team.json +21 -0
  5. package/.ctxman/profiles/frontend-team.json +20 -0
  6. package/.ctxman/templates/bug-fix.json +17 -0
  7. package/.ctxman/templates/code-review.json +9 -0
  8. package/.ctxman/templates/documentation.json +9 -0
  9. package/.ctxman/templates/feature.json +9 -0
  10. package/.ctxman/templates/full-context.json +9 -0
  11. package/.ctxman/templates/refactoring.json +18 -0
  12. package/.ctxman/templates/testing.json +9 -0
  13. package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
  14. package/LICENSE +21 -0
  15. package/README.md +380 -136
  16. package/bin/cli.js +1880 -529
  17. package/bin/cm-ask.js +18 -13
  18. package/bin/cm-audit.js +124 -0
  19. package/bin/cm-count.js +151 -0
  20. package/bin/cm-gitingest.js +128 -115
  21. package/bin/cm-map.js +242 -0
  22. package/bin/cm-mcp.js +293 -0
  23. package/bin/cm-pack.js +405 -0
  24. package/bin/cm-policy.js +170 -0
  25. package/bin/cm-pr.js +248 -0
  26. package/bin/cm-search.js +178 -0
  27. package/bin/cm-update.js +128 -128
  28. package/bin/cm-why.js +197 -0
  29. package/bin/mcp-server.js +8 -24
  30. package/ctxman.js +237 -183
  31. package/examples/README.md +19 -4
  32. package/examples/github-actions/ctxman-check.yml +49 -0
  33. package/examples/wizard-profiles/security-audit/profile.json +7 -1
  34. package/index.d.ts +1422 -0
  35. package/index.js +55 -31
  36. package/lib/agents/pr-failure-agent.js +48 -45
  37. package/lib/analyzers/ai-suggester.js +451 -0
  38. package/lib/analyzers/dependency-scanner.js +505 -0
  39. package/lib/analyzers/duplicate-detector.js +264 -0
  40. package/lib/analyzers/go-method-analyzer.js +151 -110
  41. package/lib/analyzers/import-tracker.js +431 -0
  42. package/lib/analyzers/method-analyzer.js +1055 -434
  43. package/lib/analyzers/performance-dashboard.js +683 -0
  44. package/lib/analyzers/token-calculator.js +895 -504
  45. package/lib/analyzers/unused-exports-detector.js +336 -0
  46. package/lib/api/mcp/install.js +424 -0
  47. package/lib/api/mcp/prompts.js +281 -281
  48. package/lib/api/mcp/resources.js +300 -241
  49. package/lib/api/mcp/sandbox.js +89 -0
  50. package/lib/api/mcp/server.js +675 -312
  51. package/lib/api/mcp/start.js +125 -0
  52. package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
  53. package/lib/api/rest/server.js +105 -28
  54. package/lib/cache/CacheManager.js +9 -17
  55. package/lib/core/Analyzer.js +16 -10
  56. package/lib/core/ContextBuilder.js +19 -28
  57. package/lib/core/Reporter.js +15 -14
  58. package/lib/core/Scanner.js +81 -12
  59. package/lib/count/index.js +100 -0
  60. package/lib/formatters/format-registry.js +394 -372
  61. package/lib/formatters/gitingest-formatter.js +656 -621
  62. package/lib/formatters/toon-formatter-v1.3.js +637 -632
  63. package/lib/formatters/toon-formatter.js +239 -240
  64. package/lib/integrations/git/BlameTracker.js +4 -4
  65. package/lib/integrations/git/DiffAnalyzer.js +38 -31
  66. package/lib/integrations/git/GitClient.js +71 -53
  67. package/lib/lsp/server.js +311 -0
  68. package/lib/map/graph.js +299 -0
  69. package/lib/map/imports.js +233 -0
  70. package/lib/map/index.js +33 -0
  71. package/lib/map/rank.js +253 -0
  72. package/lib/map/render.js +346 -0
  73. package/lib/map/resolver.js +574 -0
  74. package/lib/pack/budget.js +111 -0
  75. package/lib/pack/code-lexer.js +360 -0
  76. package/lib/pack/compress.js +414 -0
  77. package/lib/pack/index.js +354 -0
  78. package/lib/pack/renderers/common.js +134 -0
  79. package/lib/pack/renderers/index.js +24 -0
  80. package/lib/pack/renderers/json.js +47 -0
  81. package/lib/pack/renderers/markdown.js +54 -0
  82. package/lib/pack/renderers/plain.js +38 -0
  83. package/lib/pack/renderers/xml.js +85 -0
  84. package/lib/pack/select.js +177 -0
  85. package/lib/pack/split.js +182 -0
  86. package/lib/pack/tree.js +35 -0
  87. package/lib/parsers/gitignore-parser.js +113 -107
  88. package/lib/parsers/method-filter-parser.js +50 -33
  89. package/lib/plugins/ExporterPlugin.js +3 -3
  90. package/lib/plugins/LanguagePlugin.js +3 -3
  91. package/lib/plugins/PluginManager.js +7 -13
  92. package/lib/policy/audit.js +294 -0
  93. package/lib/policy/enforce.js +125 -0
  94. package/lib/policy/ignore-files.js +169 -0
  95. package/lib/policy/index.js +52 -0
  96. package/lib/policy/policy.js +489 -0
  97. package/lib/pr/diff-parser.js +267 -0
  98. package/lib/pr/method-mapper.js +148 -0
  99. package/lib/pr/pr-context.js +829 -0
  100. package/lib/pr/pr-git.js +149 -0
  101. package/lib/pr/pr-renderer.js +644 -0
  102. package/lib/pr/related-tests.js +296 -0
  103. package/lib/rag/DataSourcePlugin.js +5 -5
  104. package/lib/rag/EmbeddingProvider.js +19 -8
  105. package/lib/rag/Indexer.js +14 -5
  106. package/lib/rag/VectorStoreFactory.js +3 -3
  107. package/lib/rag/sources/FileDataSource.js +6 -6
  108. package/lib/rag/stores/BaseVectorStore.js +5 -5
  109. package/lib/rag/stores/ChromaDBStore.js +10 -10
  110. package/lib/rag/stores/LanceDBStore.js +29 -19
  111. package/lib/rag/stores/PineconeStore.js +11 -9
  112. package/lib/search/bm25.js +433 -0
  113. package/lib/search/code-units.js +201 -0
  114. package/lib/search/symbols.js +189 -0
  115. package/lib/ui/dashboard.js +120 -89
  116. package/lib/ui/index.js +1 -6
  117. package/lib/ui/init-wizard-ui.js +550 -0
  118. package/lib/ui/progress-bar.js +94 -88
  119. package/lib/ui/select-input.js +40 -32
  120. package/lib/ui/template-selector.js +99 -0
  121. package/lib/ui/wizard.js +374 -329
  122. package/lib/utils/claude-token-counter.js +151 -0
  123. package/lib/utils/cli-output.js +17 -0
  124. package/lib/utils/clipboard-utils.js +49 -50
  125. package/lib/utils/config-utils.js +81 -66
  126. package/lib/utils/context-versioning.js +532 -0
  127. package/lib/utils/error-handler.js +112 -112
  128. package/lib/utils/file-utils.js +153 -69
  129. package/lib/utils/format-converter.js +356 -283
  130. package/lib/utils/git-utils.js +366 -294
  131. package/lib/utils/llm-cost-estimator.js +281 -0
  132. package/lib/utils/llm-detector.js +242 -78
  133. package/lib/utils/logger.js +243 -237
  134. package/lib/utils/messages.js +355 -0
  135. package/lib/utils/multi-repo-manager.js +364 -0
  136. package/lib/utils/profile-manager.js +380 -0
  137. package/lib/utils/safe-read.js +61 -0
  138. package/lib/utils/secret-redactor.js +61 -0
  139. package/lib/utils/snapshot-manager.js +679 -0
  140. package/lib/utils/template-manager.js +378 -0
  141. package/lib/utils/token-utils.js +201 -170
  142. package/lib/utils/tokenizer-adapter.js +421 -377
  143. package/lib/utils/updater.js +539 -506
  144. package/lib/watch/FileWatcher.js +12 -8
  145. package/lib/watch/IncrementalAnalyzer.js +7 -7
  146. package/lib/why/coupling.js +81 -0
  147. package/lib/why/errors.js +23 -0
  148. package/lib/why/git.js +169 -0
  149. package/lib/why/history.js +108 -0
  150. package/lib/why/index.js +720 -0
  151. package/lib/why/ownership.js +79 -0
  152. package/lib/why/paths.js +207 -0
  153. package/lib/why/references.js +259 -0
  154. package/lib/why/render.js +722 -0
  155. package/lib/why/search.js +358 -0
  156. package/lib/why/target.js +389 -0
  157. package/lib/wizards/config-generator.js +255 -0
  158. package/lib/wizards/project-detector.js +571 -0
  159. package/package.json +44 -10
  160. package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
  161. package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
  162. package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
  163. package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
  164. package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
  165. package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
  166. package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
  167. package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
  168. package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
  169. package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
  170. package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
  171. package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
  172. package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
  173. package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
  174. package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
  175. package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
  176. package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
  177. package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
  178. package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
  179. package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
  180. package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
  181. package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
  182. package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
  183. package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
  184. package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
  185. package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
  186. package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
  187. package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
  188. package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
  189. package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
  190. package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
  191. package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
  192. package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
  193. package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
  194. package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
  195. package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
  196. package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
  197. package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
  198. package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
  199. package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
  200. package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
  201. package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
  202. package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
  203. package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
  204. package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
  205. package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
  206. package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
  207. package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
  208. package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
  209. package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
  210. package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
  211. package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
  212. package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
  213. package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
  214. package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
  215. package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
  216. package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
  217. package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
  218. package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
  219. package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
  220. package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
  221. package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
  222. package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
  223. package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
@@ -16,716 +16,721 @@
16
16
  */
17
17
 
18
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);
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
- * 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();
80
+ // Date → ISO string
81
+ if (value instanceof Date) {
82
+ return value.toISOString();
38
83
  }
39
84
 
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';
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
- * Normalize non-JSON values (Spec: Type Conversions)
58
- */
59
- normalizeValue(value) {
60
- // undefined → null
61
- if (value === undefined) return null;
130
+ return escaped;
131
+ }
62
132
 
63
- // function → null
64
- if (typeof value === 'function') return null;
133
+ /**
134
+ * Encode object
135
+ */
136
+ encodeObject(obj, depth = 0) {
137
+ const keys = Object.keys(obj);
65
138
 
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;
139
+ if (keys.length === 0) {
140
+ return ''; // Empty object
86
141
  }
87
142
 
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
- }
143
+ const prefix = this.indentChar.repeat(depth);
144
+ const lines = [];
101
145
 
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];
146
+ for (const key of keys) {
147
+ const quotedKey = this.needsQuotingAsKey(key) ? `"${key}"` : key;
148
+ const value = obj[key];
149
149
 
150
- const encoded = this.encodeValue(value, depth + 1);
150
+ const encoded = this.encodeValue(value, depth + 1);
151
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
- }
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
- 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);
158
+ } else {
159
+ lines.push(`${prefix}${quotedKey}: ${encoded}`);
160
+ }
172
161
  }
173
162
 
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);
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
- * 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
- );
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
- * 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
- );
191
+ // Check if primitive array (inline)
192
+ if (this.isPrimitiveArray(arr)) {
193
+ return this.encodePrimitiveArray(arr);
244
194
  }
245
195
 
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
- }
196
+ // Non-uniform or complex: use list format
197
+ return this.encodeListArray(arr, depth);
198
+ }
255
199
 
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
- }
200
+ /**
201
+ * Check if array is suitable for tabular format
202
+ */
203
+ isTabularArray(arr) {
204
+ if (arr.length === 0) return false;
287
205
 
288
- return lines.join('\n');
206
+ // All must be plain objects
207
+ if (!arr.every((item) => this.isPlainObject(item))) {
208
+ return false;
289
209
  }
290
210
 
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)}`;
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
- * 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');
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
- return this.parseValue(lines, 0, 0).value;
290
+ lines.push(prefix + this.indentChar + values.join(this.delimiter));
365
291
  }
366
292
 
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 };
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
- const trimmed = line.trim();
359
+ return lines.join('\n');
360
+ }
378
361
 
379
- // Empty line
380
- if (trimmed === '') {
381
- return { value: null, nextIndex: startIndex + 1 };
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
- // 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
- }
369
+ return this.parseValue(lines, 0, 0).value;
370
+ }
407
371
 
408
- // List item
409
- if (trimmed.startsWith('- ')) {
410
- return this.parseListItem(lines, startIndex, expectedDepth);
411
- }
372
+ /**
373
+ * Parse value from lines starting at index
374
+ */
375
+ parseValue(lines, startIndex, expectedDepth) {
376
+ const line = lines[startIndex];
412
377
 
413
- // Primitive value
414
- return {
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
- // 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);
384
+ // Empty line
385
+ if (trimmed === '') {
386
+ return { value: null, nextIndex: startIndex + 1 };
469
387
  }
470
388
 
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
- }
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
- 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
- }
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
- // Primitive
400
+ // Check if value is on same line
401
+ if (valueStr) {
510
402
  return {
511
- value: this.parsePrimitive(content),
512
- nextIndex: startIndex + 1
403
+ value: { [this.unquoteKey(key)]: this.parsePrimitive(valueStr) },
404
+ nextIndex: startIndex + 1,
513
405
  };
514
- }
406
+ }
515
407
 
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 };
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
- * 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;
413
+ // List item
414
+ if (trimmed.startsWith('- ')) {
415
+ return this.parseListItem(lines, startIndex, expectedDepth);
416
+ }
564
417
 
565
- // Number
566
- if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
567
- return parseFloat(trimmed);
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
- // Quoted string
571
- if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
572
- return this.unescapeString(trimmed.slice(1, -1));
573
- }
446
+ const obj = {};
447
+ fields.forEach((field, idx) => {
448
+ obj[field] = this.parsePrimitive(values[idx] || 'null');
449
+ });
574
450
 
575
- // Unquoted string
576
- return trimmed;
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
- * Unquote key
593
- */
594
- unquoteKey(key) {
595
- if (key.startsWith('"') && key.endsWith('"')) {
596
- return this.unescapeString(key.slice(1, -1));
597
- }
598
- return key;
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
- * Get indentation depth
603
- */
604
- getIndentDepth(line) {
605
- const match = line.match(/^( *)/);
606
- return match ? Math.floor(match[1].length / this.indent) : 0;
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
- * 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
- }
498
+ return { value: items, nextIndex: index };
499
+ }
641
500
 
642
- if (current) {
643
- parts.push(current.trim());
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
- return parts;
508
+ if (content.includes(':')) {
509
+ // Object
510
+ const result = this.parseObject(lines, startIndex, depth);
511
+ return result;
647
512
  }
648
513
 
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;
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
- for (const char of toonString) {
660
- if (char === '{') braceDepth++;
661
- if (char === '}') braceDepth--;
662
- if (char === '[') bracketDepth++;
663
- if (char === ']') bracketDepth--;
557
+ return { value: obj, nextIndex: index };
558
+ }
664
559
 
665
- if (braceDepth < 0) errors.push('Unbalanced braces: extra closing brace');
666
- if (bracketDepth < 0) errors.push('Unbalanced brackets: extra closing bracket');
667
- }
560
+ /**
561
+ * Parse primitive value
562
+ */
563
+ parsePrimitive(str) {
564
+ const trimmed = str.trim();
668
565
 
669
- if (braceDepth > 0) errors.push('Unbalanced braces: missing closing braces');
670
- if (bracketDepth > 0) errors.push('Unbalanced brackets: missing closing brackets');
566
+ if (trimmed === 'null') return null;
567
+ if (trimmed === 'true') return true;
568
+ if (trimmed === 'false') return false;
671
569
 
672
- return {
673
- valid: errors.length === 0,
674
- errors
675
- };
570
+ // Number
571
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
572
+ return parseFloat(trimmed);
676
573
  }
677
574
 
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);
575
+ // Quoted string
576
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
577
+ return this.unescapeString(trimmed.slice(1, -1));
684
578
  }
685
579
 
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');
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
- * 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');
647
+ if (current) {
648
+ parts.push(current.trim());
704
649
  }
705
650
 
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);
651
+ return parts;
652
+ }
712
653
 
713
- const toonSize = toon.length;
714
- const jsonSize = json.length;
715
- const savings = jsonSize - toonSize;
716
- const savingsPercentage = ((savings / jsonSize) * 100).toFixed(1);
654
+ /**
655
+ * Validate TOON string (enhanced)
656
+ */
657
+ validate(toonString) {
658
+ const errors = [];
717
659
 
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
- };
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;