json-humanized 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +351 -0
- package/bin/cli.js +319 -0
- package/docs/ARCHITECTURE.md +139 -0
- package/docs/DEMO.html +461 -0
- package/docs/PUBLISHING.md +124 -0
- package/examples/api-response.json +42 -0
- package/examples/demo.js +50 -0
- package/examples/user-profile.json +36 -0
- package/index.d.ts +138 -0
- package/package.json +71 -0
- package/src/cache.js +172 -0
- package/src/config.js +259 -0
- package/src/diff.js +284 -0
- package/src/formatters/index.js +113 -0
- package/src/formatters/template.js +132 -0
- package/src/humanizer.js +307 -0
- package/src/index.js +157 -0
- package/src/parsers/index.js +119 -0
- package/src/strategies/ai.js +108 -0
- package/src/strategies/ollama.js +135 -0
- package/src/strategies/openai.js +82 -0
- package/src/watch.js +133 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
Deep-dive into how json-humanized works internally.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Data flow
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Input (file / string / value)
|
|
11
|
+
│
|
|
12
|
+
▼
|
|
13
|
+
src/index.js ← Public API layer
|
|
14
|
+
humanize() / humanizeFile() / humanizeString()
|
|
15
|
+
│
|
|
16
|
+
├─── engine: 'local' ──────────────────────────────────────────────────
|
|
17
|
+
│ │
|
|
18
|
+
│ ▼
|
|
19
|
+
│ src/humanizer.js
|
|
20
|
+
│ detectTopLevelShape(data) → What is this? (array, API resp, …)
|
|
21
|
+
│ buildIntro(data, shape) → Opening sentence
|
|
22
|
+
│ humanizeObject(data, depth) → Recursive field walk
|
|
23
|
+
│ │
|
|
24
|
+
│ ├─ humanizeKey(key) → "createdAt" → "created at"
|
|
25
|
+
│ ├─ detectKeyContext(key) → "email", "money", "datetime", …
|
|
26
|
+
│ └─ humanizeValue(v, key) → contextual formatting
|
|
27
|
+
│
|
|
28
|
+
└─── engine: 'ai' ─────────────────────────────────────────────────────
|
|
29
|
+
│
|
|
30
|
+
▼
|
|
31
|
+
src/strategies/ai.js
|
|
32
|
+
Build system prompt + user message
|
|
33
|
+
Call Claude API (claude-opus-4-5)
|
|
34
|
+
Extract text from response blocks
|
|
35
|
+
│
|
|
36
|
+
▼
|
|
37
|
+
Raw text string
|
|
38
|
+
│
|
|
39
|
+
▼
|
|
40
|
+
src/formatters/index.js
|
|
41
|
+
applyFormat(text, format, meta)
|
|
42
|
+
→ plain / markdown / story / json
|
|
43
|
+
│
|
|
44
|
+
▼
|
|
45
|
+
Final string output
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Key design decisions
|
|
51
|
+
|
|
52
|
+
### Why `commander` for CLI?
|
|
53
|
+
|
|
54
|
+
Commander is the most widely used Node.js CLI framework, well-maintained, and produces clean `--help` output with zero config. It handles argument parsing, option validation, and subcommand support if we ever need it.
|
|
55
|
+
|
|
56
|
+
### Why `ora` for spinners?
|
|
57
|
+
|
|
58
|
+
Ora is the de facto standard for CLI spinners in Node.js. Version 5 is used (not 6+) because v6+ switched to pure ESM, which would require all consuming code to be ESM too. We stay at v5 to maintain CommonJS compatibility without bundling complexity.
|
|
59
|
+
|
|
60
|
+
### Why `chalk@4` not `chalk@5`?
|
|
61
|
+
|
|
62
|
+
Same reason: chalk v5 is pure ESM. We use v4 for CommonJS compatibility.
|
|
63
|
+
|
|
64
|
+
### Why no external test runner?
|
|
65
|
+
|
|
66
|
+
The test suite in `test/index.test.js` uses only Node.js built-ins. This means:
|
|
67
|
+
- Zero extra install time for contributors
|
|
68
|
+
- No Jest/Mocha configuration files
|
|
69
|
+
- Works on Node 14+
|
|
70
|
+
- `npm test` just works
|
|
71
|
+
|
|
72
|
+
### Why optional dependency for `@anthropic-ai/sdk`?
|
|
73
|
+
|
|
74
|
+
Not all users need AI. Making it optional means:
|
|
75
|
+
- `npm install json-humanized` is fast (no heavy SDK)
|
|
76
|
+
- Users on restricted networks can use the local engine
|
|
77
|
+
- The package doesn't fail to install if the SDK has peer dep issues
|
|
78
|
+
|
|
79
|
+
The code in `src/strategies/ai.js` uses `require()` inside a try/catch and shows a helpful error message if the SDK isn't installed.
|
|
80
|
+
|
|
81
|
+
### Why `humanizeObject` returns a string of newlines, not an array?
|
|
82
|
+
|
|
83
|
+
Early versions returned an array of sentences and joined at the top level. This was changed so that nested objects could naturally indent their output — an array of strings can't carry indentation context across recursion levels. Returning pre-indented strings makes the recursion simpler.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Adding a new strategy
|
|
88
|
+
|
|
89
|
+
Create `src/strategies/my-strategy.js`:
|
|
90
|
+
|
|
91
|
+
```javascript
|
|
92
|
+
'use strict';
|
|
93
|
+
|
|
94
|
+
async function humanizeWithMyStrategy(data, options = {}) {
|
|
95
|
+
// ... your logic
|
|
96
|
+
return 'Human-readable string';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = { humanizeWithMyStrategy };
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Then register it in `src/index.js`:
|
|
103
|
+
|
|
104
|
+
```javascript
|
|
105
|
+
const { humanizeWithMyStrategy } = require('./strategies/my-strategy');
|
|
106
|
+
|
|
107
|
+
// In humanize():
|
|
108
|
+
if (engine === 'my-strategy') {
|
|
109
|
+
text = await humanizeWithMyStrategy(data, options);
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Add it to the CLI validation:
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
const validEngines = ['local', 'ai', 'my-strategy'];
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Field context detection priority
|
|
122
|
+
|
|
123
|
+
`detectKeyContext()` in `src/humanizer.js` tests conditions in this order:
|
|
124
|
+
|
|
125
|
+
1. Exact match: `id`, `uuid`, `guid` → `identifier`
|
|
126
|
+
2. Suffix match: `_id` → `reference`
|
|
127
|
+
3. Datetime patterns: `created_at`, `timestamp`, etc.
|
|
128
|
+
4. Communication: email, url, phone
|
|
129
|
+
5. Financial: price, cost, amount, salary, etc.
|
|
130
|
+
6. Counts and quantities
|
|
131
|
+
7. Geographic: latitude, longitude
|
|
132
|
+
8. Security: password, token, secret, key, hash
|
|
133
|
+
9. Descriptive: name, title, description, bio, etc.
|
|
134
|
+
10. Status, type, category
|
|
135
|
+
11. Boolean flags: `is_*`, `has_*`, `enabled`, `active`
|
|
136
|
+
12. Miscellaneous: age, version, color, rating, error
|
|
137
|
+
13. Fallback: `generic`
|
|
138
|
+
|
|
139
|
+
The first matching condition wins.
|
package/docs/DEMO.html
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>json-humanized — Live Demo</title>
|
|
7
|
+
<style>
|
|
8
|
+
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Syne:wght@400;700;800&display=swap');
|
|
9
|
+
: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;
|
|
20
|
+
}
|
|
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); }
|
|
191
|
+
</style>
|
|
192
|
+
</head>
|
|
193
|
+
<body>
|
|
194
|
+
|
|
195
|
+
<header>
|
|
196
|
+
<h1>json-<span>humanized</span></h1>
|
|
197
|
+
<span class="badge">v2.0</span>
|
|
198
|
+
<div class="links">
|
|
199
|
+
<a href="https://github.com/AceAnomDev/json-humanized" target="_blank">GitHub</a>
|
|
200
|
+
<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
|
+
</div>
|
|
203
|
+
</header>
|
|
204
|
+
|
|
205
|
+
<main>
|
|
206
|
+
<!-- LEFT: input -->
|
|
207
|
+
<div class="pane">
|
|
208
|
+
<div class="pane-label"><span class="dot dot-cyan"></span>JSON / YAML / TOML Input</div>
|
|
209
|
+
|
|
210
|
+
<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>
|
|
214
|
+
<button class="sample-btn" onclick="loadSample('error')">error</button>
|
|
215
|
+
<button class="sample-btn" onclick="loadSample('array')">list</button>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<textarea id="input" spellcheck="false" placeholder='Paste JSON, YAML, or TOML here…
|
|
219
|
+
|
|
220
|
+
{
|
|
221
|
+
"name": "Alice",
|
|
222
|
+
"age": 30
|
|
223
|
+
}'></textarea>
|
|
224
|
+
|
|
225
|
+
<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
|
+
<select id="sel-format">
|
|
231
|
+
<option value="plain">plain</option>
|
|
232
|
+
<option value="markdown">markdown</option>
|
|
233
|
+
<option value="story">story</option>
|
|
234
|
+
<option value="json">json</option>
|
|
235
|
+
</select>
|
|
236
|
+
<select id="sel-mode">
|
|
237
|
+
<option value="structured">structured</option>
|
|
238
|
+
<option value="prose">prose</option>
|
|
239
|
+
<option value="story">story</option>
|
|
240
|
+
</select>
|
|
241
|
+
<button id="run-btn" onclick="run()">Humanize →</button>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<!-- RIGHT: output -->
|
|
246
|
+
<div class="pane">
|
|
247
|
+
<div class="pane-label"><span class="dot dot-green"></span>Human-Readable Output</div>
|
|
248
|
+
<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>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
</main>
|
|
257
|
+
|
|
258
|
+
<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>
|
|
261
|
+
</footer>
|
|
262
|
+
|
|
263
|
+
<script>
|
|
264
|
+
// ── samples ──────────────────────────────────────────────────────────────────
|
|
265
|
+
const SAMPLES = {
|
|
266
|
+
user: `{
|
|
267
|
+
"id": "usr_8f3k2",
|
|
268
|
+
"name": "Alice Johnson",
|
|
269
|
+
"email": "alice@example.com",
|
|
270
|
+
"age": 28,
|
|
271
|
+
"password": "hunter2",
|
|
272
|
+
"created_at": "2024-03-15T10:30:00Z",
|
|
273
|
+
"balance": 4250.75,
|
|
274
|
+
"is_active": true,
|
|
275
|
+
"address": {
|
|
276
|
+
"city": "San Francisco",
|
|
277
|
+
"country": "US",
|
|
278
|
+
"zip": "94105"
|
|
279
|
+
},
|
|
280
|
+
"tags": ["premium", "early-adopter"]
|
|
281
|
+
}`,
|
|
282
|
+
api: `{
|
|
283
|
+
"data": {
|
|
284
|
+
"results": [
|
|
285
|
+
{ "id": 1, "title": "Introduction to AI", "score": 9.2 },
|
|
286
|
+
{ "id": 2, "title": "Machine Learning 101", "score": 8.7 }
|
|
287
|
+
],
|
|
288
|
+
"total": 42
|
|
289
|
+
},
|
|
290
|
+
"meta": {
|
|
291
|
+
"page": 1,
|
|
292
|
+
"per_page": 2,
|
|
293
|
+
"took_ms": 34
|
|
294
|
+
},
|
|
295
|
+
"links": {
|
|
296
|
+
"next": "/api/v1/courses?page=2"
|
|
297
|
+
}
|
|
298
|
+
}`,
|
|
299
|
+
error: `{
|
|
300
|
+
"error": {
|
|
301
|
+
"code": 422,
|
|
302
|
+
"message": "Validation failed",
|
|
303
|
+
"details": [
|
|
304
|
+
{ "field": "email", "reason": "Invalid email format" },
|
|
305
|
+
{ "field": "age", "reason": "Must be between 18 and 120" }
|
|
306
|
+
]
|
|
307
|
+
},
|
|
308
|
+
"request_id": "req_7fh2k"
|
|
309
|
+
}`,
|
|
310
|
+
array: `[
|
|
311
|
+
{ "user_id": "u1", "name": "Bob", "score": 98, "country": "UK" },
|
|
312
|
+
{ "user_id": "u2", "name": "Carol", "score": 87, "country": "DE" },
|
|
313
|
+
{ "user_id": "u3", "name": "Dave", "score": 73, "country": "US" }
|
|
314
|
+
]`,
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
function loadSample(key) {
|
|
318
|
+
document.getElementById('input').value = SAMPLES[key];
|
|
319
|
+
}
|
|
320
|
+
|
|
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();
|
|
325
|
+
}
|
|
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);
|
|
348
|
+
}
|
|
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
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return lines;
|
|
368
|
+
}
|
|
369
|
+
function cap(s) { return s ? s[0].toUpperCase()+s.slice(1) : ''; }
|
|
370
|
+
|
|
371
|
+
let intro='', body='';
|
|
372
|
+
if (Array.isArray(data)) {
|
|
373
|
+
intro = `This JSON contains a list of ${data.length} record${data.length!==1?'s':''}.`;
|
|
374
|
+
const limit = Math.min(data.length, 10);
|
|
375
|
+
const lines = [];
|
|
376
|
+
for(let i=0;i<limit;i++) {
|
|
377
|
+
if (typeof data[i]==='object') lines.push(`\nRecord ${i+1}:\n${objLines(data[i],1).join('\n')}`);
|
|
378
|
+
else lines.push(`\nItem ${i+1}: ${fmtVal(data[i])}`);
|
|
379
|
+
}
|
|
380
|
+
if (data.length>10) lines.push(`\n… and ${data.length-10} more records`);
|
|
381
|
+
body = lines.join('');
|
|
382
|
+
} else if (data && typeof data==='object') {
|
|
383
|
+
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':''}.`;
|
|
387
|
+
body = '\n' + objLines(data,0).join('\n');
|
|
388
|
+
} else {
|
|
389
|
+
return `This JSON contains a single ${typeof data} value: ${data}`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const text = `${intro}\n${body}`;
|
|
393
|
+
|
|
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)}`;
|
|
399
|
+
}
|
|
400
|
+
if (format === 'json') {
|
|
401
|
+
return JSON.stringify({ humanized: text, metadata: { engine: 'local', timestamp: new Date().toISOString() }}, null, 2);
|
|
402
|
+
}
|
|
403
|
+
return `${text}\n\n[Processed by: local]`;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ── run ──────────────────────────────────────────────────────────────────────
|
|
407
|
+
async function run() {
|
|
408
|
+
const raw = document.getElementById('input').value.trim();
|
|
409
|
+
const engine = document.getElementById('sel-engine').value;
|
|
410
|
+
const format = document.getElementById('sel-format').value;
|
|
411
|
+
const out = document.getElementById('output');
|
|
412
|
+
const btn = document.getElementById('run-btn');
|
|
413
|
+
|
|
414
|
+
if (!raw) { out.textContent='⚠ Paste some JSON first'; out.className='error'; return; }
|
|
415
|
+
|
|
416
|
+
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';
|
|
429
|
+
out.className = 'error';
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
btn.disabled = true;
|
|
434
|
+
out.className = 'loading';
|
|
435
|
+
out.textContent = 'Humanizing…';
|
|
436
|
+
const t0 = performance.now();
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
const result = humanizeLocal(data, format);
|
|
440
|
+
const ms = (performance.now() - t0).toFixed(0);
|
|
441
|
+
out.textContent = result;
|
|
442
|
+
out.className = '';
|
|
443
|
+
document.getElementById('stat-time').innerHTML = `time: <span class="ok">${ms}ms</span>`;
|
|
444
|
+
} catch(e) {
|
|
445
|
+
out.textContent = '✖ ' + e.message;
|
|
446
|
+
out.className = 'error';
|
|
447
|
+
} finally {
|
|
448
|
+
btn.disabled = false;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Auto-run on Ctrl+Enter
|
|
453
|
+
document.addEventListener('keydown', e => {
|
|
454
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') run();
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// Load user sample on start
|
|
458
|
+
loadSample('user');
|
|
459
|
+
</script>
|
|
460
|
+
</body>
|
|
461
|
+
</html>
|