json-humanized 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  <div align="center">
2
2
 
3
- # json-humanized
3
+ # 🗣️ json-humanized
4
4
 
5
- **Transform any JSON / YAML / TOML into natural human language**
5
+ **📢Transform any JSON / YAML / TOML into natural human language**
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/json-humanized?color=00e5ff&style=flat-square)](https://www.npmjs.com/package/json-humanized)
8
8
  [![CI](https://img.shields.io/github/actions/workflow/status/AceAnomDev/json-humanized/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/AceAnomDev/json-humanized/actions)
package/docs/DEMO.html CHANGED
@@ -4,190 +4,59 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>json-humanized — Live Demo</title>
7
+ <!-- js-yaml for YAML parsing -->
8
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"></script>
7
9
  <style>
8
10
  @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Syne:wght@400;700;800&display=swap');
9
11
  :root {
10
- --bg: #0d0d12;
11
- --panel: #14141f;
12
- --border: #252535;
13
- --cyan: #00e5ff;
14
- --green: #00ff9d;
15
- --yellow: #ffd166;
16
- --red: #ff6b6b;
17
- --text: #e0e0f0;
18
- --muted: #6060a0;
19
- --radius: 12px;
12
+ --bg:#0d0d12; --panel:#14141f; --border:#252535;
13
+ --cyan:#00e5ff; --green:#00ff9d; --yellow:#ffd166;
14
+ --red:#ff6b6b; --text:#e0e0f0; --muted:#6060a0;
15
+ --radius:12px;
20
16
  }
21
- * { box-sizing: border-box; margin: 0; padding: 0; }
22
- body {
23
- background: var(--bg);
24
- color: var(--text);
25
- font-family: 'Syne', sans-serif;
26
- min-height: 100vh;
27
- display: grid;
28
- grid-template-rows: auto 1fr auto;
29
- }
30
- header {
31
- padding: 32px 48px 24px;
32
- border-bottom: 1px solid var(--border);
33
- display: flex;
34
- align-items: center;
35
- gap: 20px;
36
- }
37
- header h1 {
38
- font-size: 1.6rem;
39
- font-weight: 800;
40
- letter-spacing: -0.03em;
41
- }
42
- header h1 span { color: var(--cyan); }
43
- header .badge {
44
- font-family: 'JetBrains Mono', monospace;
45
- font-size: 0.7rem;
46
- background: var(--cyan)22;
47
- color: var(--cyan);
48
- border: 1px solid var(--cyan)44;
49
- padding: 3px 10px;
50
- border-radius: 99px;
51
- }
52
- header .links { margin-left: auto; display: flex; gap: 12px; }
53
- header .links a {
54
- font-size: 0.8rem;
55
- color: var(--muted);
56
- text-decoration: none;
57
- transition: color .2s;
58
- }
59
- header .links a:hover { color: var(--cyan); }
60
-
61
- main { display: grid; grid-template-columns: 1fr 1fr; gap: 0; }
62
- .pane {
63
- padding: 32px 40px;
64
- display: flex;
65
- flex-direction: column;
66
- gap: 16px;
67
- }
68
- .pane + .pane { border-left: 1px solid var(--border); }
69
- .pane-label {
70
- font-size: 0.7rem;
71
- font-weight: 700;
72
- letter-spacing: .12em;
73
- text-transform: uppercase;
74
- color: var(--muted);
75
- display: flex;
76
- align-items: center;
77
- gap: 8px;
78
- }
79
- .pane-label .dot {
80
- width: 8px; height: 8px; border-radius: 50%;
81
- }
82
- .dot-cyan { background: var(--cyan); }
83
- .dot-green { background: var(--green); }
84
-
85
- textarea, #output {
86
- flex: 1;
87
- min-height: 340px;
88
- font-family: 'JetBrains Mono', monospace;
89
- font-size: 0.82rem;
90
- line-height: 1.7;
91
- background: var(--panel);
92
- border: 1px solid var(--border);
93
- border-radius: var(--radius);
94
- padding: 20px;
95
- color: var(--text);
96
- resize: none;
97
- outline: none;
98
- transition: border-color .2s;
99
- }
100
- textarea:focus { border-color: var(--cyan)66; }
101
- #output {
102
- white-space: pre-wrap;
103
- overflow-y: auto;
104
- color: #c0c0e0;
105
- }
106
- #output.placeholder { color: var(--muted); font-style: italic; }
107
- #output.loading { color: var(--cyan); animation: pulse 1s ease-in-out infinite; }
108
- #output.error { color: var(--red); }
109
- @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.5} }
110
-
111
- .controls {
112
- display: flex;
113
- flex-wrap: wrap;
114
- gap: 10px;
115
- align-items: center;
116
- }
117
- select {
118
- background: var(--panel);
119
- border: 1px solid var(--border);
120
- color: var(--text);
121
- padding: 8px 12px;
122
- border-radius: 8px;
123
- font-family: 'Syne', sans-serif;
124
- font-size: 0.8rem;
125
- outline: none;
126
- cursor: pointer;
127
- transition: border-color .2s;
128
- }
129
- select:hover { border-color: var(--cyan)55; }
130
-
131
- #run-btn {
132
- margin-left: auto;
133
- background: var(--cyan);
134
- color: #000;
135
- border: none;
136
- padding: 10px 28px;
137
- border-radius: 8px;
138
- font-family: 'Syne', sans-serif;
139
- font-weight: 700;
140
- font-size: 0.85rem;
141
- cursor: pointer;
142
- transition: transform .15s, opacity .15s;
143
- letter-spacing: .04em;
144
- }
145
- #run-btn:hover { opacity: .9; transform: translateY(-1px); }
146
- #run-btn:active { transform: translateY(0); }
147
- #run-btn:disabled { opacity: .5; cursor: not-allowed; transform: none; }
148
-
149
- .samples {
150
- display: flex;
151
- gap: 8px;
152
- flex-wrap: wrap;
153
- }
154
- .sample-btn {
155
- background: transparent;
156
- border: 1px solid var(--border);
157
- color: var(--muted);
158
- padding: 5px 12px;
159
- border-radius: 6px;
160
- font-family: 'JetBrains Mono', monospace;
161
- font-size: 0.72rem;
162
- cursor: pointer;
163
- transition: all .2s;
164
- }
165
- .sample-btn:hover { border-color: var(--cyan)66; color: var(--cyan); }
166
-
167
- footer {
168
- padding: 16px 48px;
169
- border-top: 1px solid var(--border);
170
- font-size: 0.72rem;
171
- color: var(--muted);
172
- display: flex;
173
- justify-content: space-between;
174
- }
175
- footer a { color: var(--muted); text-decoration: none; }
176
- footer a:hover { color: var(--cyan); }
177
-
178
- .info-bar {
179
- font-family: 'JetBrains Mono', monospace;
180
- font-size: 0.72rem;
181
- color: var(--muted);
182
- padding: 6px 12px;
183
- background: var(--panel);
184
- border: 1px solid var(--border);
185
- border-radius: 6px;
186
- display: flex;
187
- gap: 16px;
188
- }
189
- .info-bar span.ok { color: var(--green); }
190
- .info-bar span.bad { color: var(--red); }
17
+ *{box-sizing:border-box;margin:0;padding:0}
18
+ body{background:var(--bg);color:var(--text);font-family:'Syne',sans-serif;min-height:100vh;display:grid;grid-template-rows:auto 1fr auto}
19
+ header{padding:28px 48px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:16px;flex-wrap:wrap}
20
+ header h1{font-size:1.5rem;font-weight:800;letter-spacing:-.03em}
21
+ header h1 span{color:var(--cyan)}
22
+ .badge{font-family:'JetBrains Mono',monospace;font-size:.7rem;background:var(--cyan)22;color:var(--cyan);border:1px solid var(--cyan)44;padding:3px 10px;border-radius:99px}
23
+ .links{margin-left:auto;display:flex;gap:12px}
24
+ .links a{font-size:.8rem;color:var(--muted);text-decoration:none;transition:color .2s}
25
+ .links a:hover{color:var(--cyan)}
26
+ main{display:grid;grid-template-columns:1fr 1fr;gap:0}
27
+ .pane{padding:28px 36px;display:flex;flex-direction:column;gap:14px}
28
+ .pane+.pane{border-left:1px solid var(--border)}
29
+ .pane-label{font-size:.7rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);display:flex;align-items:center;gap:8px}
30
+ .dot{width:8px;height:8px;border-radius:50%}
31
+ .dot-cyan{background:var(--cyan)}.dot-green{background:var(--green)}
32
+ /* format tabs */
33
+ .format-tabs{display:flex;gap:6px;border-bottom:1px solid var(--border);padding-bottom:10px}
34
+ .tab{background:transparent;border:1px solid var(--border);color:var(--muted);padding:5px 14px;border-radius:6px;font-family:'JetBrains Mono',monospace;font-size:.72rem;cursor:pointer;transition:all .2s;text-transform:uppercase;letter-spacing:.06em}
35
+ .tab.active{border-color:var(--cyan);color:var(--cyan);background:var(--cyan)11}
36
+ .tab:hover:not(.active){border-color:var(--border);color:var(--text)}
37
+ textarea,#output{flex:1;min-height:320px;font-family:'JetBrains Mono',monospace;font-size:.8rem;line-height:1.75;background:var(--panel);border:1px solid var(--border);border-radius:var(--radius);padding:18px;color:var(--text);resize:none;outline:none;transition:border-color .2s}
38
+ textarea:focus{border-color:var(--cyan)66}
39
+ #output{white-space:pre-wrap;overflow-y:auto;color:#c0c0e0}
40
+ #output.placeholder{color:var(--muted);font-style:italic}
41
+ #output.loading{color:var(--cyan);animation:pulse 1s ease-in-out infinite}
42
+ #output.error{color:var(--red)}
43
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.45}}
44
+ .controls{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
45
+ select{background:var(--panel);border:1px solid var(--border);color:var(--text);padding:8px 12px;border-radius:8px;font-family:'Syne',sans-serif;font-size:.8rem;outline:none;cursor:pointer;transition:border-color .2s}
46
+ select:hover{border-color:var(--cyan)55}
47
+ #run-btn{margin-left:auto;background:var(--cyan);color:#000;border:none;padding:10px 28px;border-radius:8px;font-family:'Syne',sans-serif;font-weight:700;font-size:.85rem;cursor:pointer;transition:transform .15s,opacity .15s;letter-spacing:.04em}
48
+ #run-btn:hover{opacity:.9;transform:translateY(-1px)}
49
+ #run-btn:active{transform:translateY(0)}
50
+ #run-btn:disabled{opacity:.5;cursor:not-allowed;transform:none}
51
+ .samples{display:flex;gap:6px;flex-wrap:wrap;align-items:center}
52
+ .sample-btn{background:transparent;border:1px solid var(--border);color:var(--muted);padding:4px 10px;border-radius:5px;font-family:'JetBrains Mono',monospace;font-size:.7rem;cursor:pointer;transition:all .2s}
53
+ .sample-btn:hover{border-color:var(--cyan)66;color:var(--cyan)}
54
+ .info-bar{font-family:'JetBrains Mono',monospace;font-size:.72rem;color:var(--muted);padding:6px 12px;background:var(--panel);border:1px solid var(--border);border-radius:6px;display:flex;gap:14px;flex-wrap:wrap}
55
+ .info-bar .ok{color:var(--green)}.info-bar .bad{color:var(--red)}.info-bar .hi{color:var(--cyan)}
56
+ footer{padding:14px 48px;border-top:1px solid var(--border);font-size:.72rem;color:var(--muted);display:flex;justify-content:space-between}
57
+ footer a{color:var(--muted);text-decoration:none}.footer a:hover{color:var(--cyan)}
58
+ .copy-btn{background:transparent;border:1px solid var(--border);color:var(--muted);padding:4px 10px;border-radius:5px;font-family:'JetBrains Mono',monospace;font-size:.7rem;cursor:pointer;transition:all .2s;margin-left:auto}
59
+ .copy-btn:hover{border-color:var(--green)66;color:var(--green)}
191
60
  </style>
192
61
  </head>
193
62
  <body>
@@ -198,72 +67,72 @@
198
67
  <div class="links">
199
68
  <a href="https://github.com/AceAnomDev/json-humanized" target="_blank">GitHub</a>
200
69
  <a href="https://www.npmjs.com/package/json-humanized" target="_blank">npm</a>
201
- <a href="https://github.com/AceAnomDev/json-humanized#readme" target="_blank">Docs</a>
202
70
  </div>
203
71
  </header>
204
72
 
205
73
  <main>
206
- <!-- LEFT: input -->
74
+ <!-- INPUT -->
207
75
  <div class="pane">
208
- <div class="pane-label"><span class="dot dot-cyan"></span>JSON / YAML / TOML Input</div>
76
+ <div class="pane-label"><span class="dot dot-cyan"></span>Input</div>
77
+
78
+ <!-- format tabs -->
79
+ <div class="format-tabs">
80
+ <button class="tab active" onclick="switchInputFormat('json')">JSON</button>
81
+ <button class="tab" onclick="switchInputFormat('yaml')">YAML</button>
82
+ <button class="tab" onclick="switchInputFormat('toml')">TOML</button>
83
+ </div>
209
84
 
210
85
  <div class="samples">
211
- <span style="font-size:.72rem;color:var(--muted);margin-right:4px">Examples:</span>
212
- <button class="sample-btn" onclick="loadSample('user')">user profile</button>
213
- <button class="sample-btn" onclick="loadSample('api')">api response</button>
86
+ <span style="font-size:.7rem;color:var(--muted)">Examples:</span>
87
+ <button class="sample-btn" onclick="loadSample('user')">user</button>
88
+ <button class="sample-btn" onclick="loadSample('api')">api</button>
214
89
  <button class="sample-btn" onclick="loadSample('error')">error</button>
215
- <button class="sample-btn" onclick="loadSample('array')">list</button>
90
+ <button class="sample-btn" onclick="loadSample('list')">list</button>
216
91
  </div>
217
92
 
218
- <textarea id="input" spellcheck="false" placeholder='Paste JSON, YAML, or TOML here…
219
-
220
- {
221
- "name": "Alice",
222
- "age": 30
223
- }'></textarea>
93
+ <textarea id="input" spellcheck="false" placeholder="Paste JSON, YAML, or TOML here…"></textarea>
224
94
 
225
95
  <div class="controls">
226
- <select id="sel-engine">
227
- <option value="local">⚡ local (offline)</option>
228
- <option value="ai">🤖 ai (Claude API)</option>
229
- </select>
230
96
  <select id="sel-format">
231
97
  <option value="plain">plain</option>
232
98
  <option value="markdown">markdown</option>
233
99
  <option value="story">story</option>
234
- <option value="json">json</option>
235
100
  </select>
236
101
  <select id="sel-mode">
237
102
  <option value="structured">structured</option>
238
103
  <option value="prose">prose</option>
239
- <option value="story">story</option>
240
104
  </select>
105
+ <span style="font-size:.72rem;color:var(--muted);font-family:'JetBrains Mono',monospace">Ctrl+↵</span>
241
106
  <button id="run-btn" onclick="run()">Humanize →</button>
242
107
  </div>
243
108
  </div>
244
109
 
245
- <!-- RIGHT: output -->
110
+ <!-- OUTPUT -->
246
111
  <div class="pane">
247
- <div class="pane-label"><span class="dot dot-green"></span>Human-Readable Output</div>
112
+ <div class="pane-label">
113
+ <span class="dot dot-green"></span>Output
114
+ <button class="copy-btn" onclick="copyOutput()">copy</button>
115
+ </div>
248
116
  <div id="output" class="placeholder">Your output will appear here…</div>
249
- <div class="info-bar" id="info-bar">
250
- <span id="stat-chars">chars: —</span>
251
- <span id="stat-keys">keys: —</span>
252
- <span id="stat-engine">engine: —</span>
253
- <span id="stat-time">time: —</span>
117
+ <div class="info-bar">
118
+ <span id="s-fmt">format: —</span>
119
+ <span id="s-type">type: —</span>
120
+ <span id="s-keys">fields: —</span>
121
+ <span id="s-time">time: —</span>
254
122
  </div>
255
123
  </div>
256
124
  </main>
257
125
 
258
126
  <footer>
259
- <span>json-humanized · MIT license</span>
260
- <span><a href="https://github.com/AceAnomDev/json-humanized/issues" target="_blank">Report an issue</a></span>
127
+ <span>json-humanized · MIT</span>
128
+ <span><a href="https://github.com/AceAnomDev/json-humanized/issues" target="_blank">Report issue</a></span>
261
129
  </footer>
262
130
 
263
131
  <script>
264
- // ── samples ──────────────────────────────────────────────────────────────────
132
+ // ── samples per format ────────────────────────────────────────────────────────
265
133
  const SAMPLES = {
266
- user: `{
134
+ json: {
135
+ user: `{
267
136
  "id": "usr_8f3k2",
268
137
  "name": "Alice Johnson",
269
138
  "email": "alice@example.com",
@@ -272,31 +141,21 @@ const SAMPLES = {
272
141
  "created_at": "2024-03-15T10:30:00Z",
273
142
  "balance": 4250.75,
274
143
  "is_active": true,
275
- "address": {
276
- "city": "San Francisco",
277
- "country": "US",
278
- "zip": "94105"
279
- },
144
+ "address": { "city": "San Francisco", "country": "US" },
280
145
  "tags": ["premium", "early-adopter"]
281
146
  }`,
282
- api: `{
147
+ api: `{
283
148
  "data": {
284
149
  "results": [
285
- { "id": 1, "title": "Introduction to AI", "score": 9.2 },
286
- { "id": 2, "title": "Machine Learning 101", "score": 8.7 }
150
+ { "id": 1, "title": "Intro to AI", "score": 9.2 },
151
+ { "id": 2, "title": "ML 101", "score": 8.7 }
287
152
  ],
288
153
  "total": 42
289
154
  },
290
- "meta": {
291
- "page": 1,
292
- "per_page": 2,
293
- "took_ms": 34
294
- },
295
- "links": {
296
- "next": "/api/v1/courses?page=2"
297
- }
155
+ "meta": { "page": 1, "per_page": 2, "took_ms": 34 },
156
+ "links": { "next": "/api/v1/courses?page=2" }
298
157
  }`,
299
- error: `{
158
+ error: `{
300
159
  "error": {
301
160
  "code": 422,
302
161
  "message": "Validation failed",
@@ -307,73 +166,308 @@ const SAMPLES = {
307
166
  },
308
167
  "request_id": "req_7fh2k"
309
168
  }`,
310
- array: `[
169
+ list: `[
311
170
  { "user_id": "u1", "name": "Bob", "score": 98, "country": "UK" },
312
171
  { "user_id": "u2", "name": "Carol", "score": 87, "country": "DE" },
313
172
  { "user_id": "u3", "name": "Dave", "score": 73, "country": "US" }
314
173
  ]`,
174
+ },
175
+
176
+ yaml: {
177
+ user: `id: usr_8f3k2
178
+ name: Alice Johnson
179
+ email: alice@example.com
180
+ age: 28
181
+ password: hunter2
182
+ created_at: "2024-03-15T10:30:00Z"
183
+ balance: 4250.75
184
+ is_active: true
185
+ address:
186
+ city: San Francisco
187
+ country: US
188
+ tags:
189
+ - premium
190
+ - early-adopter`,
191
+ api: `data:
192
+ results:
193
+ - id: 1
194
+ title: Intro to AI
195
+ score: 9.2
196
+ - id: 2
197
+ title: ML 101
198
+ score: 8.7
199
+ total: 42
200
+ meta:
201
+ page: 1
202
+ per_page: 2
203
+ took_ms: 34
204
+ links:
205
+ next: /api/v1/courses?page=2`,
206
+ error: `error:
207
+ code: 422
208
+ message: Validation failed
209
+ details:
210
+ - field: email
211
+ reason: Invalid email format
212
+ - field: age
213
+ reason: Must be between 18 and 120
214
+ request_id: req_7fh2k`,
215
+ list: `- user_id: u1
216
+ name: Bob
217
+ score: 98
218
+ country: UK
219
+ - user_id: u2
220
+ name: Carol
221
+ score: 87
222
+ country: DE
223
+ - user_id: u3
224
+ name: Dave
225
+ score: 73
226
+ country: US`,
227
+ },
228
+
229
+ toml: {
230
+ user: `id = "usr_8f3k2"
231
+ name = "Alice Johnson"
232
+ email = "alice@example.com"
233
+ age = 28
234
+ password = "hunter2"
235
+ created_at = "2024-03-15T10:30:00Z"
236
+ balance = 4250.75
237
+ is_active = true
238
+ tags = ["premium", "early-adopter"]
239
+
240
+ [address]
241
+ city = "San Francisco"
242
+ country = "US"`,
243
+ api: `[meta]
244
+ page = 1
245
+ per_page = 2
246
+ took_ms = 34
247
+
248
+ [links]
249
+ next = "/api/v1/courses?page=2"
250
+
251
+ [[data.results]]
252
+ id = 1
253
+ title = "Intro to AI"
254
+ score = 9.2
255
+
256
+ [[data.results]]
257
+ id = 2
258
+ title = "ML 101"
259
+ score = 8.7`,
260
+ error: `request_id = "req_7fh2k"
261
+
262
+ [error]
263
+ code = 422
264
+ message = "Validation failed"
265
+
266
+ [[error.details]]
267
+ field = "email"
268
+ reason = "Invalid email format"
269
+
270
+ [[error.details]]
271
+ field = "age"
272
+ reason = "Must be between 18 and 120"`,
273
+ list: `[[items]]
274
+ user_id = "u1"
275
+ name = "Bob"
276
+ score = 98
277
+ country = "UK"
278
+
279
+ [[items]]
280
+ user_id = "u2"
281
+ name = "Carol"
282
+ score = 87
283
+ country = "DE"
284
+
285
+ [[items]]
286
+ user_id = "u3"
287
+ name = "Dave"
288
+ score = 73
289
+ country = "US"`,
290
+ },
315
291
  };
316
292
 
293
+ // ── minimal inline TOML parser ─────────────────────────────────────────────
294
+ // Handles the common subset: key = value, [section], [[array]]
295
+ function parseTOML(text) {
296
+ const root = {};
297
+ let cur = root;
298
+ let arrKey = null;
299
+ let arrRef = null;
300
+
301
+ const lines = text.split('\n');
302
+ for (let raw of lines) {
303
+ const line = raw.trim();
304
+ if (!line || line.startsWith('#')) continue;
305
+
306
+ // [[array of tables]]
307
+ const arrMatch = line.match(/^\[\[(.+)\]\]$/);
308
+ if (arrMatch) {
309
+ const parts = arrMatch[1].trim().split('.');
310
+ let obj = root;
311
+ for (let i = 0; i < parts.length - 1; i++) {
312
+ if (!obj[parts[i]]) obj[parts[i]] = {};
313
+ obj = obj[parts[i]];
314
+ }
315
+ const last = parts[parts.length - 1];
316
+ if (!Array.isArray(obj[last])) obj[last] = [];
317
+ const newEntry = {};
318
+ obj[last].push(newEntry);
319
+ cur = newEntry;
320
+ arrKey = null;
321
+ continue;
322
+ }
323
+
324
+ // [section]
325
+ const secMatch = line.match(/^\[(.+)\]$/);
326
+ if (secMatch) {
327
+ const parts = secMatch[1].trim().split('.');
328
+ cur = root;
329
+ for (const p of parts) {
330
+ if (!cur[p]) cur[p] = {};
331
+ cur = cur[p];
332
+ }
333
+ arrKey = null;
334
+ continue;
335
+ }
336
+
337
+ // key = value
338
+ const eqIdx = line.indexOf('=');
339
+ if (eqIdx < 0) continue;
340
+ const key = line.slice(0, eqIdx).trim();
341
+ const val = line.slice(eqIdx + 1).trim();
342
+ cur[key] = parseTOMLValue(val);
343
+ }
344
+ return root;
345
+ }
346
+
347
+ function parseTOMLValue(val) {
348
+ if (val === 'true') return true;
349
+ if (val === 'false') return false;
350
+ if (/^-?\d+\.\d+$/.test(val)) return parseFloat(val);
351
+ if (/^-?\d+$/.test(val)) return parseInt(val, 10);
352
+ if (val.startsWith('"') && val.endsWith('"')) return val.slice(1,-1).replace(/\\n/g,'\n').replace(/\\t/g,'\t');
353
+ if (val.startsWith("'") && val.endsWith("'")) return val.slice(1,-1);
354
+ if (val.startsWith('[') && val.endsWith(']')) {
355
+ const inner = val.slice(1,-1).trim();
356
+ if (!inner) return [];
357
+ return inner.split(',').map(s => parseTOMLValue(s.trim()));
358
+ }
359
+ return val;
360
+ }
361
+
362
+ // ── state ────────────────────────────────────────────────────────────────────
363
+ let currentFmt = 'json';
364
+
365
+ function switchInputFormat(fmt) {
366
+ currentFmt = fmt;
367
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
368
+ event.target.classList.add('active');
369
+ loadSample('user'); // reload current sample in new format
370
+ document.getElementById('output').textContent = 'Your output will appear here…';
371
+ document.getElementById('output').className = 'placeholder';
372
+ }
373
+
317
374
  function loadSample(key) {
318
- document.getElementById('input').value = SAMPLES[key];
375
+ document.getElementById('input').value = SAMPLES[currentFmt][key] || '';
319
376
  }
320
377
 
321
- // ── rule-based local engine (client-side) ───────────────────────────────────
322
- function humanizeLocal(data, format) {
323
- function fmtKey(k) {
324
- return k.replace(/([A-Z])/g,' $1').replace(/[_\-]+/g,' ').trim().toLowerCase();
378
+ // ── parse input ──────────────────────────────────────────────────────────────
379
+ function parseInput(text, fmt) {
380
+ if (fmt === 'yaml') {
381
+ if (typeof jsyaml === 'undefined') throw new Error('js-yaml failed to load');
382
+ return jsyaml.load(text);
325
383
  }
326
- function fmtVal(v, key='') {
327
- if (v === null || v === undefined) return 'not specified';
328
- if (v === '') return 'empty';
329
- const k = (key||'').toLowerCase();
330
- if (typeof v === 'boolean') return v ? 'yes' : 'no';
331
- if (/(password|secret|token|hash)/i.test(k)) return '*** (hidden)';
332
- if (/(created|updated|at)$/i.test(k) && typeof v==='string') {
333
- try { return new Date(v).toLocaleString('en-US',{year:'numeric',month:'long',day:'numeric',hour:'2-digit',minute:'2-digit'}); } catch{}
334
- }
335
- if (/(price|cost|amount|salary|balance)/i.test(k) && typeof v==='number') {
336
- return v>=1e6?`$${(v/1e6).toFixed(2)}M`:v>=1e3?`$${(v/1e3).toFixed(1)}K`:`$${v.toFixed(2)}`;
337
- }
338
- if (/(email)/i.test(k)) return `email address: ${v}`;
339
- if (typeof v === 'number') return v.toLocaleString();
340
- if (typeof v === 'string' && v.length > 150) return v.slice(0,150)+'… (truncated)';
341
- if (Array.isArray(v)) {
342
- if (v.length===0) return 'none';
343
- if (v.every(x=>typeof x !== 'object')) return v.join(', ');
344
- return `${v.length} item(s)`;
345
- }
346
- if (typeof v === 'object') return `{${Object.keys(v).length} field(s)}`;
347
- return String(v);
384
+ if (fmt === 'toml') {
385
+ return parseTOML(text);
348
386
  }
349
- function objLines(obj, depth=0) {
350
- const pfx = ' '.repeat(depth);
351
- const lines = [];
352
- for (const [k,v] of Object.entries(obj)) {
353
- const label = fmtKey(k);
354
- if (Array.isArray(v) && v.length && typeof v[0]==='object') {
355
- lines.push(`${pfx}• ${cap(label)}: ${v.length} entr${v.length===1?'y':'ies'}`);
356
- v.slice(0,3).forEach((it,i)=>{
357
- lines.push(`${pfx} [${i+1}] ${objLines(it, depth+2).join(' ')}`);
358
- });
359
- if (v.length>3) lines.push(`${pfx} … and ${v.length-3} more`);
360
- } else if (v && typeof v==='object' && !Array.isArray(v)) {
361
- lines.push(`${pfx}• ${cap(label)}:`);
362
- lines.push(...objLines(v, depth+1));
363
- } else {
364
- lines.push(`${pfx}• ${cap(label)}: ${fmtVal(v,k)}`);
365
- }
387
+ return JSON.parse(text);
388
+ }
389
+
390
+ // ── humanizer (client-side, mirrors humanizer.js) ────────────────────────────
391
+ function fmtLabel(fmt) {
392
+ if (fmt === 'yaml') return 'YAML';
393
+ if (fmt === 'toml') return 'TOML';
394
+ return 'data';
395
+ }
396
+
397
+ function detectCtx(k) {
398
+ k = k.toLowerCase();
399
+ if (/^(id|uuid|_id)$/.test(k)) return 'identifier';
400
+ if (/(password|secret|token|key|hash)/i.test(k)) return 'sensitive';
401
+ if (/(created|updated|at|date|time)$/i.test(k)) return 'datetime';
402
+ if (/(email|mail)$/i.test(k)) return 'email';
403
+ if (/(price|cost|amount|balance|fee)/i.test(k)) return 'money';
404
+ if (/(count|qty|total|num)/i.test(k)) return 'count';
405
+ if (/(age)$/i.test(k)) return 'age';
406
+ if (/(rating|score|rank)/i.test(k)) return 'rating';
407
+ return 'generic';
408
+ }
409
+
410
+ function fmtKey(k) {
411
+ return k.replace(/([A-Z])/g,' $1').replace(/[_\-]+/g,' ').trim().toLowerCase();
412
+ }
413
+
414
+ function fmtVal(v, key='') {
415
+ if (v === null || v === undefined) return 'not specified';
416
+ if (v === '') return 'empty';
417
+ const ctx = detectCtx(key);
418
+ if (typeof v === 'boolean') return v ? 'yes' : 'no';
419
+ if (ctx === 'sensitive') return '*** (hidden)';
420
+ if (ctx === 'money' && typeof v==='number') {
421
+ return v>=1e6?`$${(v/1e6).toFixed(2)}M`:v>=1e3?`$${(v/1e3).toFixed(1)}K`:`$${v.toFixed(2)}`;
422
+ }
423
+ if (ctx === 'age' && typeof v==='number') return `${v} year${v!==1?'s':''} old`;
424
+ if (ctx === 'rating' && typeof v==='number') return `${v}/10`;
425
+ if (ctx === 'datetime' && typeof v==='string') {
426
+ try { const d=new Date(v); if(!isNaN(d)) return d.toLocaleString('en-US',{year:'numeric',month:'long',day:'numeric'}); } catch{}
427
+ }
428
+ if (ctx === 'email') return `email: ${v}`;
429
+ if (Array.isArray(v)) {
430
+ if (!v.length) return 'none';
431
+ if (v.every(x=>typeof x!=='object')) return v.join(', ');
432
+ return `${v.length} item(s)`;
433
+ }
434
+ if (typeof v==='object') return `{${Object.keys(v).length} field(s)}`;
435
+ if (typeof v==='number') return v.toLocaleString();
436
+ if (typeof v==='string' && v.length>150) return v.slice(0,150)+'…';
437
+ return String(v);
438
+ }
439
+
440
+ function objLines(obj, depth=0) {
441
+ const pfx=' '.repeat(depth), lines=[];
442
+ for (const [k,v] of Object.entries(obj)) {
443
+ const label = fmtKey(k);
444
+ if (Array.isArray(v) && v.length && typeof v[0]==='object') {
445
+ lines.push(`${pfx}• ${cap(label)}: ${v.length} entr${v.length===1?'y':'ies'}`);
446
+ v.slice(0,3).forEach((it,i) => {
447
+ lines.push(`${pfx} [${i+1}] ${objLines(it,0).join(' ').slice(0,120)}`);
448
+ });
449
+ if (v.length>3) lines.push(`${pfx} … and ${v.length-3} more`);
450
+ } else if (v && typeof v==='object' && !Array.isArray(v)) {
451
+ lines.push(`${pfx}• ${cap(label)}:`);
452
+ lines.push(...objLines(v,depth+1));
453
+ } else {
454
+ lines.push(`${pfx}• ${cap(label)}: ${fmtVal(v,k)}`);
366
455
  }
367
- return lines;
368
456
  }
369
- function cap(s) { return s ? s[0].toUpperCase()+s.slice(1) : ''; }
457
+ return lines;
458
+ }
459
+
460
+ function cap(s) { return s?s[0].toUpperCase()+s.slice(1):'' }
370
461
 
462
+ function humanizeData(data, fmt, outFmt) {
463
+ const lbl = fmtLabel(fmt);
371
464
  let intro='', body='';
465
+
372
466
  if (Array.isArray(data)) {
373
- intro = `This JSON contains a list of ${data.length} record${data.length!==1?'s':''}.`;
467
+ intro = `This ${lbl} contains a list of ${data.length} record${data.length!==1?'s':''}.`;
374
468
  const limit = Math.min(data.length, 10);
375
469
  const lines = [];
376
- for(let i=0;i<limit;i++) {
470
+ for(let i=0;i<limit;i++){
377
471
  if (typeof data[i]==='object') lines.push(`\nRecord ${i+1}:\n${objLines(data[i],1).join('\n')}`);
378
472
  else lines.push(`\nItem ${i+1}: ${fmtVal(data[i])}`);
379
473
  }
@@ -381,66 +475,64 @@ function humanizeLocal(data, format) {
381
475
  body = lines.join('');
382
476
  } else if (data && typeof data==='object') {
383
477
  const keys = Object.keys(data);
384
- if ('error' in data || 'errors' in data) intro='This JSON describes an error or failure response.';
385
- else if ('data' in data && ('meta' in data || 'links' in data)) intro='This JSON is an API response with data and metadata.';
386
- else intro=`This JSON contains a structured object with ${keys.length} field${keys.length!==1?'s':''}.`;
478
+ if ('error' in data || 'errors' in data)
479
+ intro=`This ${lbl} describes an error or failure response.`;
480
+ else if ('data' in data && ('meta' in data||'links' in data))
481
+ intro=`This ${lbl} is an API response with data and metadata.`;
482
+ else
483
+ intro=`This ${lbl} contains a structured object with ${keys.length} field${keys.length!==1?'s':''}.`;
387
484
  body = '\n' + objLines(data,0).join('\n');
388
485
  } else {
389
- return `This JSON contains a single ${typeof data} value: ${data}`;
486
+ return `This ${lbl} contains a single value: ${data}`;
390
487
  }
391
488
 
392
489
  const text = `${intro}\n${body}`;
393
490
 
394
- if (format === 'markdown') {
395
- return `# JSON Analysis\n\n## Summary\n\n${text}\n\n---\n*Processed by json-humanized (local engine)*`;
396
- }
397
- if (format === 'story') {
398
- return `${'━'.repeat(50)}\n 📖 THE DATA STORY\n${'━'.repeat(50)}\n\n${text}\n\n${'━'.repeat(50)}`;
491
+ if (outFmt==='markdown') {
492
+ return `# ${lbl.toUpperCase()} Analysis\n\n## Summary\n\n${text}\n\n---\n*Processed by json-humanized (local engine)*`;
399
493
  }
400
- if (format === 'json') {
401
- return JSON.stringify({ humanized: text, metadata: { engine: 'local', timestamp: new Date().toISOString() }}, null, 2);
494
+ if (outFmt==='story') {
495
+ return `${'━'.repeat(52)}\n 📖 THE ${lbl.toUpperCase()} STORY\n${''.repeat(52)}\n\n${text}\n\n${'━'.repeat(52)}`;
402
496
  }
403
- return `${text}\n\n[Processed by: local]`;
497
+ return text;
404
498
  }
405
499
 
406
500
  // ── run ──────────────────────────────────────────────────────────────────────
407
501
  async function run() {
408
502
  const raw = document.getElementById('input').value.trim();
409
- const engine = document.getElementById('sel-engine').value;
410
- const format = document.getElementById('sel-format').value;
503
+ const outFmt = document.getElementById('sel-format').value;
411
504
  const out = document.getElementById('output');
412
505
  const btn = document.getElementById('run-btn');
413
506
 
414
- if (!raw) { out.textContent='⚠ Paste some JSON first'; out.className='error'; return; }
507
+ if (!raw) { out.textContent='⚠ Paste some input first'; out.className='error'; return; }
415
508
 
416
509
  let data;
417
- try { data = JSON.parse(raw); }
418
- catch { out.textContent='✖ Invalid JSON — please check your input'; out.className='error'; return; }
419
-
420
- // update info bar
421
- const chars = raw.length;
422
- const keys = Array.isArray(data) ? data.length : typeof data==='object' ? Object.keys(data).length : 1;
423
- document.getElementById('stat-chars').textContent = `chars: ${chars}`;
424
- document.getElementById('stat-keys').textContent = Array.isArray(data) ? `items: ${keys}` : `keys: ${keys}`;
425
- document.getElementById('stat-engine').textContent = `engine: ${engine}`;
426
-
427
- if (engine === 'ai') {
428
- out.textContent = '⚠ AI mode is not available in the browser demo.\n\nInstall the CLI and use:\n jh data.json --engine ai';
510
+ try {
511
+ data = parseInput(raw, currentFmt);
512
+ } catch(e) {
513
+ out.textContent = `✖ Invalid ${currentFmt.toUpperCase()}: ${e.message}`;
429
514
  out.className = 'error';
430
515
  return;
431
516
  }
432
517
 
518
+ // info bar
519
+ const keys = Array.isArray(data)?data.length:typeof data==='object'?Object.keys(data).length:1;
520
+ document.getElementById('s-fmt').innerHTML = `format: <span class="hi">${currentFmt.toUpperCase()}</span>`;
521
+ document.getElementById('s-type').textContent = `type: ${Array.isArray(data)?'array':typeof data}`;
522
+ document.getElementById('s-keys').textContent = Array.isArray(data)?`items: ${keys}`:`fields: ${keys}`;
523
+
433
524
  btn.disabled = true;
434
525
  out.className = 'loading';
435
526
  out.textContent = 'Humanizing…';
436
527
  const t0 = performance.now();
437
528
 
438
529
  try {
439
- const result = humanizeLocal(data, format);
440
- const ms = (performance.now() - t0).toFixed(0);
530
+ await new Promise(r => setTimeout(r, 40)); // let spinner render
531
+ const result = humanizeData(data, currentFmt, outFmt);
532
+ const ms = (performance.now()-t0).toFixed(0);
441
533
  out.textContent = result;
442
534
  out.className = '';
443
- document.getElementById('stat-time').innerHTML = `time: <span class="ok">${ms}ms</span>`;
535
+ document.getElementById('s-time').innerHTML = `time: <span class="ok">${ms}ms</span>`;
444
536
  } catch(e) {
445
537
  out.textContent = '✖ ' + e.message;
446
538
  out.className = 'error';
@@ -449,12 +541,17 @@ async function run() {
449
541
  }
450
542
  }
451
543
 
452
- // Auto-run on Ctrl+Enter
544
+ function copyOutput() {
545
+ const txt = document.getElementById('output').textContent;
546
+ if (!txt || document.getElementById('output').classList.contains('placeholder')) return;
547
+ navigator.clipboard.writeText(txt).catch(()=>{});
548
+ }
549
+
453
550
  document.addEventListener('keydown', e => {
454
- if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') run();
551
+ if ((e.ctrlKey||e.metaKey) && e.key==='Enter') run();
455
552
  });
456
553
 
457
- // Load user sample on start
554
+ // init
458
555
  loadSample('user');
459
556
  </script>
460
557
  </body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-humanized",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Transform any JSON/YAML/TOML into human-readable prose — powered by Claude AI, OpenAI, Ollama, or built-in rule-based engine",
5
5
  "main": "src/index.js",
6
6
  "types": "index.d.ts",
@@ -39,14 +39,14 @@
39
39
  "dependencies": {
40
40
  "chalk": "4.1.2",
41
41
  "commander": "11.1.0",
42
- "ora": "5.4.1"
42
+ "ora": "5.4.1",
43
+ "js-yaml": "^4.1.0",
44
+ "@iarna/toml": "^2.2.5"
43
45
  },
44
46
  "optionalDependencies": {
45
47
  "@anthropic-ai/sdk": "^0.20.0",
46
- "openai": "^4.0.0",
47
- "js-yaml": "^4.1.0",
48
- "@iarna/toml": "^2.2.5",
49
- "handlebars": "^4.7.8"
48
+ "handlebars": "^4.7.8",
49
+ "openai": "^4.0.0"
50
50
  },
51
51
  "engines": {
52
52
  "node": ">=14.0.0"
@@ -4,9 +4,19 @@
4
4
  // json-humanized · Output formatters
5
5
  // ─────────────────────────────────────────────────────────────────────────────
6
6
 
7
- /**
8
- * Plain text formatter (default)
9
- */
7
+ /** Derive a readable label from sourceFormat option or filename extension */
8
+ function sourceLabel(meta = {}) {
9
+ const sf = (meta.sourceFormat || '').toLowerCase();
10
+ if (sf === 'yaml' || sf === 'yml') return 'YAML';
11
+ if (sf === 'toml') return 'TOML';
12
+ if (sf === 'json') return 'JSON';
13
+ const ext = (meta.filename || '').split('.').pop().toLowerCase();
14
+ if (ext === 'yaml' || ext === 'yml') return 'YAML';
15
+ if (ext === 'toml') return 'TOML';
16
+ if (ext === 'json') return 'JSON';
17
+ return 'Data';
18
+ }
19
+
10
20
  function formatPlain(text, meta = {}) {
11
21
  const lines = [];
12
22
  if (meta.filename) {
@@ -21,19 +31,14 @@ function formatPlain(text, meta = {}) {
21
31
  return lines.join('\n');
22
32
  }
23
33
 
24
- /**
25
- * Markdown formatter
26
- */
27
34
  function formatMarkdown(text, meta = {}) {
35
+ const lbl = sourceLabel(meta);
28
36
  const lines = [];
29
37
 
30
- if (meta.filename) {
31
- lines.push(`# JSON Analysis: \`${meta.filename}\``);
32
- lines.push('');
33
- } else {
34
- lines.push('# JSON Analysis');
35
- lines.push('');
36
- }
38
+ lines.push(meta.filename
39
+ ? `# ${lbl} Analysis: \`${meta.filename}\``
40
+ : `# ${lbl} Analysis`);
41
+ lines.push('');
37
42
 
38
43
  if (meta.timestamp) {
39
44
  lines.push(`> Generated on ${new Date(meta.timestamp).toLocaleString()}`);
@@ -64,43 +69,32 @@ function formatMarkdown(text, meta = {}) {
64
69
  return lines.join('\n');
65
70
  }
66
71
 
67
- /**
68
- * Story/narrative formatter — wraps output in a storytelling frame
69
- */
70
72
  function formatStory(text, meta = {}) {
73
+ const lbl = sourceLabel(meta);
71
74
  const lines = [];
72
75
  lines.push('━'.repeat(60));
73
- lines.push(' 📖 THE DATA STORY');
76
+ lines.push(` 📖 THE ${lbl.toUpperCase()} STORY`);
74
77
  lines.push('━'.repeat(60));
75
78
  lines.push('');
76
79
  lines.push(text);
77
80
  lines.push('');
78
81
  lines.push('━'.repeat(60));
79
- if (meta.filename) {
80
- lines.push(` Source: ${meta.filename}`);
81
- }
82
+ if (meta.filename) lines.push(` Source: ${meta.filename}`);
82
83
  return lines.join('\n');
83
84
  }
84
85
 
85
- /**
86
- * JSON formatter — outputs structured metadata alongside the description
87
- */
88
86
  function formatJSON(text, meta = {}) {
89
- const output = {
87
+ return JSON.stringify({
90
88
  humanized: text,
91
89
  metadata: {
92
- engine: meta.engine || 'local',
93
- filename: meta.filename || null,
90
+ engine: meta.engine || 'local',
91
+ filename: meta.filename || null,
94
92
  timestamp: meta.timestamp || new Date().toISOString(),
95
- stats: meta.stats || {},
93
+ stats: meta.stats || {},
96
94
  },
97
- };
98
- return JSON.stringify(output, null, 2);
95
+ }, null, 2);
99
96
  }
100
97
 
101
- /**
102
- * Apply a named formatter
103
- */
104
98
  function applyFormat(text, format = 'plain', meta = {}) {
105
99
  switch (format) {
106
100
  case 'markdown': return formatMarkdown(text, meta);
package/src/humanizer.js CHANGED
@@ -14,12 +14,8 @@ const TYPE_LABELS = {
14
14
  null: 'empty value',
15
15
  };
16
16
 
17
- /**
18
- * Detect semantic meaning from a key name
19
- */
20
17
  function detectKeyContext(key) {
21
18
  const k = key.toLowerCase();
22
-
23
19
  if (/^(id|uuid|guid|_id)$/.test(k)) return 'identifier';
24
20
  if (/(_id|Id)$/.test(key)) return 'reference';
25
21
  if (/(created|updated|modified|timestamp|date|time|at)$/i.test(k)) return 'datetime';
@@ -42,30 +38,23 @@ function detectKeyContext(key) {
42
38
  if (/(address|city|country|region|zip|postal)/i.test(k)) return 'location';
43
39
  if (/(rating|score|rank)/i.test(k)) return 'rating';
44
40
  if (/(error|err|exception|message|msg)/i.test(k)) return 'error';
45
-
46
41
  return 'generic';
47
42
  }
48
43
 
49
- /**
50
- * Format a key name into natural English label
51
- */
52
44
  function humanizeKey(key) {
53
45
  return key
54
- .replace(/([A-Z])/g, ' $1') // camelCase → spaced
55
- .replace(/[_\-\.]+/g, ' ') // snake_case / kebab-case → spaced
46
+ .replace(/([A-Z])/g, ' $1')
47
+ .replace(/[_\-\.]+/g, ' ')
56
48
  .replace(/\s+/g, ' ')
57
49
  .trim()
58
50
  .toLowerCase();
59
51
  }
60
52
 
61
- /**
62
- * Format a value with contextual awareness
63
- */
64
53
  function humanizeValue(value, key = '', depth = 0) {
65
54
  if (value === null || value === undefined) return 'not specified';
66
55
  if (value === '') return 'empty';
67
56
 
68
- const ctx = detectKeyContext(key);
57
+ const ctx = detectKeyContext(key);
69
58
  const type = typeof value;
70
59
 
71
60
  if (type === 'boolean') {
@@ -98,13 +87,8 @@ function humanizeValue(value, key = '', depth = 0) {
98
87
  return value;
99
88
  }
100
89
 
101
- if (Array.isArray(value)) {
102
- return humanizeArray(value, key, depth);
103
- }
104
-
105
- if (type === 'object') {
106
- return humanizeObject(value, depth + 1);
107
- }
90
+ if (Array.isArray(value)) return humanizeArray(value, key, depth);
91
+ if (type === 'object') return humanizeObject(value, depth + 1);
108
92
 
109
93
  return String(value);
110
94
  }
@@ -128,17 +112,13 @@ function formatDatetime(str) {
128
112
  }
129
113
  }
130
114
 
131
- /**
132
- * Humanize an array value
133
- */
134
115
  function humanizeArray(arr, key = '', depth = 0) {
135
116
  if (arr.length === 0) return 'an empty list';
136
117
 
137
- const itemType = typeof arr[0];
118
+ const itemType = typeof arr[0];
138
119
  const allSameType = arr.every(i => typeof i === itemType);
139
- const label = humanizeKey(key) || 'items';
120
+ const label = humanizeKey(key) || 'items';
140
121
 
141
- // Simple scalar arrays → inline sentence
142
122
  if (allSameType && ['string', 'number', 'boolean'].includes(itemType) && arr.length <= 8) {
143
123
  const formatted = arr.map(v => humanizeValue(v, '', depth));
144
124
  if (formatted.length === 1) return formatted[0];
@@ -146,20 +126,16 @@ function humanizeArray(arr, key = '', depth = 0) {
146
126
  return `${formatted.join(', ')} and ${last}`;
147
127
  }
148
128
 
149
- // Complex arrays → count + describe first element
150
129
  const preview = humanizeValue(arr[0], '', depth + 1);
151
130
  return `a collection of ${arr.length} ${label} (e.g. ${preview}${arr.length > 1 ? ', and more' : ''})`;
152
131
  }
153
132
 
154
- /**
155
- * Humanize an object recursively, returns array of sentences
156
- */
157
133
  function humanizeObject(obj, depth = 0) {
158
134
  const entries = Object.entries(obj).filter(([, v]) => v !== undefined);
159
135
  if (entries.length === 0) return 'an empty object';
160
136
 
161
137
  const sentences = [];
162
- const indent = ' '.repeat(depth);
138
+ const indent = ' '.repeat(depth);
163
139
 
164
140
  for (const [key, value] of entries) {
165
141
  const label = humanizeKey(key);
@@ -174,20 +150,16 @@ function humanizeObject(obj, depth = 0) {
174
150
  sentences.push(`${indent}• ${capitalize(label)}: ${value.length} entr${value.length === 1 ? 'y' : 'ies'}`);
175
151
  value.slice(0, 5).forEach((item, i) => {
176
152
  if (typeof item === 'object' && item !== null) {
177
- const sub = humanizeObject(item, depth + 1);
178
- sentences.push(`${indent} [${i + 1}] ${sub}`);
153
+ sentences.push(`${indent} [${i + 1}] ${humanizeObject(item, depth + 1)}`);
179
154
  } else {
180
155
  sentences.push(`${indent} [${i + 1}] ${humanizeValue(item, '', depth + 1)}`);
181
156
  }
182
157
  });
183
- if (value.length > 5) {
184
- sentences.push(`${indent} … and ${value.length - 5} more`);
185
- }
158
+ if (value.length > 5) sentences.push(`${indent} … and ${value.length - 5} more`);
186
159
  }
187
160
  } else if (typeof value === 'object' && value !== null) {
188
161
  sentences.push(`${indent}• ${capitalize(label)}:`);
189
- const sub = humanizeObject(value, depth + 1);
190
- sentences.push(sub);
162
+ sentences.push(humanizeObject(value, depth + 1));
191
163
  } else {
192
164
  const hval = humanizeValue(value, key, depth);
193
165
  if (ctx === 'identifier') {
@@ -213,70 +185,86 @@ function capitalize(str) {
213
185
  }
214
186
 
215
187
  // ─────────────────────────────────────────────────────────────────────────────
216
- // Top-level entry point
188
+ // Top-level shape detection
217
189
  // ─────────────────────────────────────────────────────────────────────────────
218
190
 
219
- /**
220
- * Detect what kind of structure this JSON represents
221
- */
222
191
  function detectTopLevelShape(data) {
223
192
  if (Array.isArray(data)) {
224
193
  if (data.length === 0) return { shape: 'empty-array' };
225
- const first = data[0];
226
- if (typeof first === 'object' && first !== null) return { shape: 'record-list', count: data.length };
194
+ if (typeof data[0] === 'object' && data[0] !== null)
195
+ return { shape: 'record-list', count: data.length };
227
196
  return { shape: 'scalar-list', count: data.length };
228
197
  }
229
198
  if (typeof data === 'object' && data !== null) {
230
199
  const keys = Object.keys(data);
231
- // Detect common API shapes
232
- if ('data' in data && ('meta' in data || 'links' in data || 'pagination' in data)) return { shape: 'api-response' };
233
- if ('error' in data || 'errors' in data || 'message' in data && 'code' in data) return { shape: 'error-response' };
234
- if ('users' in data || 'items' in data || 'results' in data || 'records' in data) return { shape: 'collection' };
235
- if (keys.length <= 2) return { shape: 'simple-object' };
200
+ if ('data' in data && ('meta' in data || 'links' in data || 'pagination' in data))
201
+ return { shape: 'api-response' };
202
+ if ('error' in data || 'errors' in data || ('message' in data && 'code' in data))
203
+ return { shape: 'error-response' };
204
+ if ('users' in data || 'items' in data || 'results' in data || 'records' in data)
205
+ return { shape: 'collection' };
206
+ if (keys.length <= 2)
207
+ return { shape: 'simple-object' };
236
208
  return { shape: 'complex-object', keys: keys.length };
237
209
  }
238
210
  return { shape: 'primitive' };
239
211
  }
240
212
 
241
213
  /**
242
- * Build a natural-language introduction for the top-level shape
214
+ * Returns a format label for use in intro sentences.
215
+ * @param {'json'|'yaml'|'toml'|string} [sourceFormat]
243
216
  */
244
- function buildIntro(data, shape) {
217
+ function formatLabel(sourceFormat) {
218
+ switch ((sourceFormat || 'json').toLowerCase()) {
219
+ case 'yaml': case 'yml': return 'YAML';
220
+ case 'toml': return 'TOML';
221
+ default: return 'data';
222
+ }
223
+ }
224
+
225
+ function buildIntro(data, shape, sourceFormat) {
226
+ const lbl = formatLabel(sourceFormat);
227
+
245
228
  switch (shape.shape) {
246
229
  case 'empty-array':
247
- return 'This JSON contains an empty list with no items.';
230
+ return `This ${lbl} contains an empty list with no items.`;
248
231
  case 'record-list':
249
- return `This JSON contains a list of ${shape.count} record${shape.count !== 1 ? 's' : ''}.`;
232
+ return `This ${lbl} contains a list of ${shape.count} record${shape.count !== 1 ? 's' : ''}.`;
250
233
  case 'scalar-list':
251
- return `This JSON contains a list of ${shape.count} value${shape.count !== 1 ? 's' : ''}.`;
234
+ return `This ${lbl} contains a list of ${shape.count} value${shape.count !== 1 ? 's' : ''}.`;
252
235
  case 'api-response':
253
- return 'This JSON is an API response with data and metadata.';
236
+ return `This ${lbl} is an API response with data and metadata.`;
254
237
  case 'error-response':
255
- return 'This JSON describes an error or failure response.';
238
+ return `This ${lbl} describes an error or failure response.`;
256
239
  case 'collection':
257
- return 'This JSON contains a collection of resources.';
240
+ return `This ${lbl} contains a collection of resources.`;
258
241
  case 'simple-object':
259
- return 'This JSON contains a simple object with a few fields.';
242
+ return `This ${lbl} contains a simple object with a few fields.`;
260
243
  case 'complex-object':
261
- return `This JSON contains a structured object with ${shape.keys} fields.`;
244
+ return `This ${lbl} contains a structured object with ${shape.keys} fields.`;
262
245
  case 'primitive':
263
- return `This JSON contains a single value: ${String(data)}.`;
246
+ return `This ${lbl} contains a single value: ${String(data)}.`;
264
247
  default:
265
- return 'This JSON contains the following data:';
248
+ return `This ${lbl} contains the following data:`;
266
249
  }
267
250
  }
268
251
 
252
+ // ─────────────────────────────────────────────────────────────────────────────
253
+ // Main export
254
+ // ─────────────────────────────────────────────────────────────────────────────
255
+
269
256
  /**
270
- * Generate a human-readable summary using the rule-based engine
257
+ * @param {any} data
258
+ * @param {object} [options]
259
+ * @param {string} [options.mode='structured']
260
+ * @param {string} [options.sourceFormat] 'json' | 'yaml' | 'toml'
271
261
  */
272
262
  function humanizeLocal(data, options = {}) {
273
- const { mode = 'structured', maxDepth = 10 } = options;
263
+ const { mode = 'structured', sourceFormat } = options;
274
264
  const shape = detectTopLevelShape(data);
275
- const intro = buildIntro(data, shape);
265
+ const intro = buildIntro(data, shape, sourceFormat);
276
266
 
277
- if (shape.shape === 'primitive') {
278
- return intro;
279
- }
267
+ if (shape.shape === 'primitive') return intro;
280
268
 
281
269
  let body = '';
282
270
 
package/src/index.js CHANGED
@@ -78,11 +78,13 @@ async function humanize(data, options = {}) {
78
78
  cacheTTL = 3600,
79
79
  } = merged;
80
80
 
81
+ const sourceFormat = merged.sourceFormat || null;
82
+
81
83
  const engineFn = async () => {
82
84
  if (engine === 'ai') {
83
85
  return humanizeWithAI(data, { apiKey, aiProvider, mode, lang, context, maxChars, ...merged });
84
86
  }
85
- return humanizeLocal(data, { mode });
87
+ return humanizeLocal(data, { mode, sourceFormat });
86
88
  };
87
89
 
88
90
  // Use cache only for AI engine (local is instant)
@@ -91,7 +93,7 @@ async function humanize(data, options = {}) {
91
93
  : await engineFn();
92
94
 
93
95
  const stats = computeStats(data);
94
- const meta = { engine, aiProvider, filename, timestamp: new Date().toISOString(), stats, data };
96
+ const meta = { engine, aiProvider, filename, sourceFormat, timestamp: new Date().toISOString(), stats, data };
95
97
 
96
98
  // Template output takes priority over standard formatters
97
99
  if (template) {
@@ -119,9 +121,13 @@ async function humanizeFile(filePath, options = {}) {
119
121
 
120
122
  const data = parseFile(resolved);
121
123
 
124
+ const ext = path.extname(resolved).slice(1).toLowerCase();
125
+ const sourceFormat = ['yaml', 'yml', 'toml', 'json'].includes(ext) ? ext : undefined;
126
+
122
127
  return humanize(data, {
123
128
  ...options,
124
129
  filename: options.filename || path.basename(resolved),
130
+ sourceFormat: options.sourceFormat || sourceFormat,
125
131
  });
126
132
  }
127
133