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 +2 -2
- package/docs/DEMO.html +406 -309
- package/package.json +6 -6
- package/src/formatters/index.js +26 -32
- package/src/humanizer.js +56 -68
- package/src/index.js +8 -2
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
# json-humanized
|
|
3
|
+
# 🗣️ json-humanized
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**📢Transform any JSON / YAML / TOML into natural human language**
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/json-humanized)
|
|
8
8
|
[](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
|
|
11
|
-
--
|
|
12
|
-
--
|
|
13
|
-
--
|
|
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
|
-
*
|
|
22
|
-
body
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
.
|
|
63
|
-
|
|
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
|
-
<!--
|
|
74
|
+
<!-- INPUT -->
|
|
207
75
|
<div class="pane">
|
|
208
|
-
<div class="pane-label"><span class="dot dot-cyan"></span>
|
|
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:.
|
|
212
|
-
<button class="sample-btn" onclick="loadSample('user')">user
|
|
213
|
-
<button class="sample-btn" onclick="loadSample('api')">api
|
|
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('
|
|
90
|
+
<button class="sample-btn" onclick="loadSample('list')">list</button>
|
|
216
91
|
</div>
|
|
217
92
|
|
|
218
|
-
<textarea id="input" spellcheck="false" placeholder=
|
|
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
|
-
<!--
|
|
110
|
+
<!-- OUTPUT -->
|
|
246
111
|
<div class="pane">
|
|
247
|
-
<div class="pane-label"
|
|
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"
|
|
250
|
-
<span id="
|
|
251
|
-
<span id="
|
|
252
|
-
<span id="
|
|
253
|
-
<span id="
|
|
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
|
|
260
|
-
<span><a href="https://github.com/AceAnomDev/json-humanized/issues" target="_blank">Report
|
|
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
|
-
|
|
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
|
-
|
|
147
|
+
api: `{
|
|
283
148
|
"data": {
|
|
284
149
|
"results": [
|
|
285
|
-
{ "id": 1, "title": "
|
|
286
|
-
{ "id": 2, "title": "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// ──
|
|
322
|
-
function
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
327
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
385
|
-
|
|
386
|
-
else
|
|
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
|
|
486
|
+
return `This ${lbl} contains a single value: ${data}`;
|
|
390
487
|
}
|
|
391
488
|
|
|
392
489
|
const text = `${intro}\n${body}`;
|
|
393
490
|
|
|
394
|
-
if (
|
|
395
|
-
return `#
|
|
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 (
|
|
401
|
-
return
|
|
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
|
|
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
|
|
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='⚠
|
|
507
|
+
if (!raw) { out.textContent='⚠ Paste some input first'; out.className='error'; return; }
|
|
415
508
|
|
|
416
509
|
let data;
|
|
417
|
-
try {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
440
|
-
const
|
|
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('
|
|
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
|
-
|
|
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
|
|
551
|
+
if ((e.ctrlKey||e.metaKey) && e.key==='Enter') run();
|
|
455
552
|
});
|
|
456
553
|
|
|
457
|
-
//
|
|
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.
|
|
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
|
-
"
|
|
47
|
-
"
|
|
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"
|
package/src/formatters/index.js
CHANGED
|
@@ -4,9 +4,19 @@
|
|
|
4
4
|
// json-humanized · Output formatters
|
|
5
5
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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(
|
|
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
|
-
|
|
87
|
+
return JSON.stringify({
|
|
90
88
|
humanized: text,
|
|
91
89
|
metadata: {
|
|
92
|
-
engine:
|
|
93
|
-
filename:
|
|
90
|
+
engine: meta.engine || 'local',
|
|
91
|
+
filename: meta.filename || null,
|
|
94
92
|
timestamp: meta.timestamp || new Date().toISOString(),
|
|
95
|
-
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')
|
|
55
|
-
.replace(/[_\-\.]+/g, ' ')
|
|
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
|
|
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
|
|
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
|
|
118
|
+
const itemType = typeof arr[0];
|
|
138
119
|
const allSameType = arr.every(i => typeof i === itemType);
|
|
139
|
-
const label
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
226
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
if ('error' in data || 'errors' in data || 'message' in data && 'code' in data)
|
|
234
|
-
|
|
235
|
-
if (
|
|
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
|
-
*
|
|
214
|
+
* Returns a format label for use in intro sentences.
|
|
215
|
+
* @param {'json'|'yaml'|'toml'|string} [sourceFormat]
|
|
243
216
|
*/
|
|
244
|
-
function
|
|
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
|
|
230
|
+
return `This ${lbl} contains an empty list with no items.`;
|
|
248
231
|
case 'record-list':
|
|
249
|
-
return `This
|
|
232
|
+
return `This ${lbl} contains a list of ${shape.count} record${shape.count !== 1 ? 's' : ''}.`;
|
|
250
233
|
case 'scalar-list':
|
|
251
|
-
return `This
|
|
234
|
+
return `This ${lbl} contains a list of ${shape.count} value${shape.count !== 1 ? 's' : ''}.`;
|
|
252
235
|
case 'api-response':
|
|
253
|
-
return
|
|
236
|
+
return `This ${lbl} is an API response with data and metadata.`;
|
|
254
237
|
case 'error-response':
|
|
255
|
-
return
|
|
238
|
+
return `This ${lbl} describes an error or failure response.`;
|
|
256
239
|
case 'collection':
|
|
257
|
-
return
|
|
240
|
+
return `This ${lbl} contains a collection of resources.`;
|
|
258
241
|
case 'simple-object':
|
|
259
|
-
return
|
|
242
|
+
return `This ${lbl} contains a simple object with a few fields.`;
|
|
260
243
|
case 'complex-object':
|
|
261
|
-
return `This
|
|
244
|
+
return `This ${lbl} contains a structured object with ${shape.keys} fields.`;
|
|
262
245
|
case 'primitive':
|
|
263
|
-
return `This
|
|
246
|
+
return `This ${lbl} contains a single value: ${String(data)}.`;
|
|
264
247
|
default:
|
|
265
|
-
return
|
|
248
|
+
return `This ${lbl} contains the following data:`;
|
|
266
249
|
}
|
|
267
250
|
}
|
|
268
251
|
|
|
252
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
253
|
+
// Main export
|
|
254
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
255
|
+
|
|
269
256
|
/**
|
|
270
|
-
*
|
|
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',
|
|
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
|
|