Skip to content

Commit b0a0808

Browse files
authored
1 parent 8bda7f7 commit b0a0808

File tree

1 file changed

+348
-0
lines changed

1 file changed

+348
-0
lines changed

json-string-extractor.html

Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
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 String Extractor</title>
7+
<style>
8+
* {
9+
box-sizing: border-box;
10+
}
11+
12+
body {
13+
font-family: Helvetica, Arial, sans-serif;
14+
margin: 0;
15+
padding: 20px;
16+
background: #f5f5f5;
17+
}
18+
19+
.container {
20+
max-width: 1200px;
21+
margin: 0 auto;
22+
background: white;
23+
padding: 30px;
24+
border-radius: 8px;
25+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
26+
}
27+
28+
h1 {
29+
margin: 0 0 20px 0;
30+
font-size: 24px;
31+
font-weight: normal;
32+
}
33+
34+
.input-section {
35+
margin-bottom: 30px;
36+
}
37+
38+
#jsonInput {
39+
width: 100%;
40+
min-height: 150px;
41+
padding: 12px;
42+
border: 2px solid #ddd;
43+
border-radius: 4px;
44+
font-family: 'Courier New', monospace;
45+
font-size: 16px;
46+
resize: vertical;
47+
}
48+
49+
#jsonInput:focus {
50+
outline: none;
51+
border-color: #4a90e2;
52+
}
53+
54+
.error {
55+
color: #e74c3c;
56+
padding: 12px;
57+
background: #fef5f5;
58+
border-radius: 4px;
59+
margin-top: 10px;
60+
display: none;
61+
}
62+
63+
.results {
64+
margin-top: 30px;
65+
}
66+
67+
.string-item {
68+
margin-bottom: 20px;
69+
padding: 15px;
70+
background: #fafafa;
71+
border-radius: 4px;
72+
border: 1px solid #e0e0e0;
73+
}
74+
75+
.string-path {
76+
font-size: 12px;
77+
color: #666;
78+
margin-bottom: 8px;
79+
font-family: 'Courier New', monospace;
80+
}
81+
82+
.string-content {
83+
position: relative;
84+
}
85+
86+
.string-textarea {
87+
width: 100%;
88+
padding: 10px;
89+
border: 1px solid #ddd;
90+
border-radius: 4px;
91+
font-family: 'Courier New', monospace;
92+
font-size: 16px;
93+
resize: none;
94+
background: white;
95+
overflow-y: hidden;
96+
}
97+
98+
.copy-button {
99+
margin-top: 8px;
100+
padding: 8px 16px;
101+
background: #4a90e2;
102+
color: white;
103+
border: none;
104+
border-radius: 4px;
105+
cursor: pointer;
106+
font-size: 14px;
107+
font-family: Helvetica, Arial, sans-serif;
108+
}
109+
110+
.copy-button:hover {
111+
background: #357abd;
112+
}
113+
114+
.copy-button:active {
115+
background: #2868a8;
116+
}
117+
118+
.copy-button.copied {
119+
background: #27ae60;
120+
}
121+
122+
.load-example-button {
123+
padding: 6px 12px;
124+
background: #666;
125+
color: white;
126+
border: none;
127+
border-radius: 4px;
128+
cursor: pointer;
129+
font-size: 13px;
130+
font-family: Helvetica, Arial, sans-serif;
131+
}
132+
133+
.load-example-button:hover {
134+
background: #555;
135+
}
136+
137+
.load-example-button:active {
138+
background: #444;
139+
}
140+
141+
.load-example-button:disabled {
142+
background: #ccc;
143+
cursor: not-allowed;
144+
}
145+
146+
.count {
147+
font-size: 14px;
148+
color: #666;
149+
margin-bottom: 15px;
150+
}
151+
</style>
152+
</head>
153+
<body>
154+
<div class="container">
155+
<h1>JSON string extractor</h1>
156+
<p style="margin: 0 0 20px 0; font-size: 14px; color: #666; line-height: 1.2;">Paste JSON below to extract all strings that are longer than 20 characters or contain newlines. Results are sorted by length and displayed as copyable textareas.</p>
157+
158+
<div class="input-section">
159+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
160+
<label for="jsonInput" style="font-size: 14px; color: #666;">Paste JSON here:</label>
161+
<button id="loadExample" class="load-example-button">Load example JSON</button>
162+
</div>
163+
<textarea id="jsonInput" placeholder="Paste your JSON here..."></textarea>
164+
<div id="error" class="error"></div>
165+
</div>
166+
167+
<div id="results" class="results"></div>
168+
</div>
169+
170+
<script type="module">
171+
const jsonInput = document.getElementById('jsonInput');
172+
const errorDiv = document.getElementById('error');
173+
const resultsDiv = document.getElementById('results');
174+
const loadExampleButton = document.getElementById('loadExample');
175+
176+
async function loadExampleJSON() {
177+
loadExampleButton.disabled = true;
178+
loadExampleButton.textContent = 'Loading...';
179+
180+
try {
181+
const response = await fetch('https://gist.githubusercontent.com/simonw/1d1013ba059af76461153722005a039d/raw/0835963f6cc3554fe49c50f273660a6d01e4a7e0/response.json');
182+
183+
if (!response.ok) {
184+
throw new Error(`HTTP error! status: ${response.status}`);
185+
}
186+
187+
const text = await response.text();
188+
jsonInput.value = text;
189+
processJSON();
190+
191+
loadExampleButton.textContent = 'Load example JSON';
192+
} catch (e) {
193+
showError(`Failed to load example JSON: ${e.message}`);
194+
loadExampleButton.textContent = 'Load example JSON';
195+
} finally {
196+
loadExampleButton.disabled = false;
197+
}
198+
}
199+
200+
loadExampleButton.addEventListener('click', loadExampleJSON);
201+
202+
function showError(message) {
203+
errorDiv.textContent = message;
204+
errorDiv.style.display = 'block';
205+
}
206+
207+
function hideError() {
208+
errorDiv.style.display = 'none';
209+
}
210+
211+
function shouldDisplay(str) {
212+
return str.includes('\n') || str.length > 20;
213+
}
214+
215+
function extractStrings(obj, path = '') {
216+
const results = [];
217+
218+
if (typeof obj === 'string') {
219+
if (shouldDisplay(obj)) {
220+
results.push({ path, value: obj });
221+
}
222+
} else if (Array.isArray(obj)) {
223+
obj.forEach((item, index) => {
224+
const newPath = path ? `${path}[${index}]` : `[${index}]`;
225+
results.push(...extractStrings(item, newPath));
226+
});
227+
} else if (obj !== null && typeof obj === 'object') {
228+
Object.keys(obj).forEach(key => {
229+
const newPath = path ? `${path}.${key}` : key;
230+
results.push(...extractStrings(obj[key], newPath));
231+
});
232+
}
233+
234+
return results;
235+
}
236+
237+
function resizeTextarea(textarea) {
238+
// Reset height to auto to get the correct scrollHeight
239+
textarea.style.height = 'auto';
240+
// Set height to scrollHeight to fit content
241+
textarea.style.height = textarea.scrollHeight + 'px';
242+
}
243+
244+
function resizeAllTextareas() {
245+
document.querySelectorAll('.string-textarea').forEach(textarea => {
246+
resizeTextarea(textarea);
247+
});
248+
}
249+
250+
function copyToClipboard(text, button) {
251+
navigator.clipboard.writeText(text).then(() => {
252+
const originalText = button.textContent;
253+
button.textContent = 'Copied!';
254+
button.classList.add('copied');
255+
256+
setTimeout(() => {
257+
button.textContent = originalText;
258+
button.classList.remove('copied');
259+
}, 2000);
260+
}).catch(err => {
261+
console.error('Failed to copy:', err);
262+
alert('Failed to copy to clipboard');
263+
});
264+
}
265+
266+
function renderResults(strings) {
267+
if (strings.length === 0) {
268+
resultsDiv.innerHTML = '<p class="count">No strings found that are longer than 20 characters or contain newlines.</p>';
269+
return;
270+
}
271+
272+
// Sort strings by length (longest first)
273+
const sortedStrings = [...strings].sort((a, b) => b.value.length - a.value.length);
274+
275+
let html = `<p class="count">Found ${strings.length} string${strings.length === 1 ? '' : 's'}:</p>`;
276+
277+
sortedStrings.forEach((item, index) => {
278+
const escapedValue = item.value
279+
.replace(/&/g, '&amp;')
280+
.replace(/</g, '&lt;')
281+
.replace(/>/g, '&gt;')
282+
.replace(/"/g, '&quot;');
283+
284+
html += `
285+
<div class="string-item">
286+
<div class="string-path">${item.path || '(root)'}</div>
287+
<div class="string-content">
288+
<textarea class="string-textarea" data-index="${index}">${escapedValue}</textarea>
289+
<button class="copy-button" data-index="${index}">Copy to clipboard</button>
290+
</div>
291+
</div>
292+
`;
293+
});
294+
295+
resultsDiv.innerHTML = html;
296+
297+
// Add event listeners to copy buttons
298+
document.querySelectorAll('.copy-button').forEach(button => {
299+
button.addEventListener('click', () => {
300+
const index = button.getAttribute('data-index');
301+
const textarea = document.querySelector(`.string-textarea[data-index="${index}"]`);
302+
copyToClipboard(textarea.value, button);
303+
});
304+
});
305+
306+
// Resize all textareas after rendering
307+
requestAnimationFrame(() => {
308+
resizeAllTextareas();
309+
});
310+
}
311+
312+
function processJSON() {
313+
const input = jsonInput.value.trim();
314+
315+
if (!input) {
316+
resultsDiv.innerHTML = '';
317+
hideError();
318+
return;
319+
}
320+
321+
try {
322+
const parsed = JSON.parse(input);
323+
hideError();
324+
325+
const strings = extractStrings(parsed);
326+
renderResults(strings);
327+
} catch (e) {
328+
showError(`Invalid JSON: ${e.message}`);
329+
resultsDiv.innerHTML = '';
330+
}
331+
}
332+
333+
jsonInput.addEventListener('input', processJSON);
334+
335+
// Resize textareas when window is resized
336+
let resizeTimeout;
337+
window.addEventListener('resize', () => {
338+
clearTimeout(resizeTimeout);
339+
resizeTimeout = setTimeout(() => {
340+
resizeAllTextareas();
341+
}, 100);
342+
});
343+
344+
// Process on load if there's any content
345+
processJSON();
346+
</script>
347+
</body>
348+
</html>

0 commit comments

Comments
 (0)