Skip to content

Commit 9b1a188

Browse files
committed
Add YAML Sorter tool
New tool that sorts YAML keys alphabetically with options for deep sorting nested keys and preserving line width. Uses js-yaml library for parsing. https://claude.ai/code/session_01P4nYNMs3xBReSMSF35TG1g
1 parent 543e43e commit 9b1a188

3 files changed

Lines changed: 363 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ A collection of web-based utility tools.
1111
- **[Link to Markdown Table](https://tools.kzuraw.com/html/link-to-markdown-table.html)** - convert links to markdown table format
1212
- **[Markdown to Rich Text](https://tools.kzuraw.com/html/markdown-to-rich-text.html)** - convert markdown to rich text
1313
- **[SVG to React](https://tools.kzuraw.com/html/svg-to-react.html)** - convert SVG to React components with camelCased props
14+
- **[YAML Sorter](https://tools.kzuraw.com/html/yaml-sorter.html)** - sort YAML keys alphabetically with deep sort option
1415

1516
## Deployment
1617

html/yaml-sorter.html

Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
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>YAML Sorter</title>
7+
<link
8+
rel="icon"
9+
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🛠️</text></svg>"
10+
/>
11+
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"></script>
12+
<style>
13+
* {
14+
box-sizing: border-box;
15+
margin: 0;
16+
padding: 0;
17+
}
18+
19+
body {
20+
font-family:
21+
-apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
22+
background: #f5f5f7;
23+
color: #1d1d1f;
24+
min-height: 100vh;
25+
display: flex;
26+
flex-direction: column;
27+
}
28+
29+
header {
30+
padding: 20px 32px 16px;
31+
display: flex;
32+
align-items: baseline;
33+
gap: 12px;
34+
}
35+
36+
header h1 {
37+
font-size: 20px;
38+
font-weight: 600;
39+
letter-spacing: -0.3px;
40+
color: #1d1d1f;
41+
}
42+
43+
header span {
44+
font-size: 13px;
45+
color: #86868b;
46+
}
47+
48+
.main {
49+
flex: 1;
50+
display: grid;
51+
grid-template-columns: 1fr 1fr;
52+
gap: 0;
53+
padding: 0 24px 24px;
54+
min-height: 0;
55+
}
56+
57+
.pane {
58+
display: flex;
59+
flex-direction: column;
60+
gap: 8px;
61+
}
62+
63+
.pane-label {
64+
font-size: 11px;
65+
font-weight: 600;
66+
letter-spacing: 0.5px;
67+
text-transform: uppercase;
68+
color: #86868b;
69+
padding: 0 4px;
70+
}
71+
72+
.pane:first-child {
73+
padding-right: 12px;
74+
}
75+
76+
.pane:last-child {
77+
padding-left: 12px;
78+
}
79+
80+
textarea {
81+
flex: 1;
82+
resize: none;
83+
border: 1px solid #d2d2d7;
84+
border-radius: 12px;
85+
padding: 16px;
86+
font-family: "SF Mono", "Menlo", "Monaco", "Courier New", monospace;
87+
font-size: 13px;
88+
line-height: 1.6;
89+
color: #1d1d1f;
90+
background: #ffffff;
91+
outline: none;
92+
transition: border-color 0.15s ease;
93+
min-height: calc(100vh - 160px);
94+
}
95+
96+
textarea:focus {
97+
border-color: #0071e3;
98+
box-shadow: 0 0 0 3px rgba(0, 113, 227, 0.15);
99+
}
100+
101+
textarea[readonly] {
102+
background: #fafafa;
103+
color: #1d1d1f;
104+
cursor: default;
105+
}
106+
107+
textarea[readonly]:focus {
108+
border-color: #d2d2d7;
109+
box-shadow: none;
110+
}
111+
112+
.pane-header {
113+
display: flex;
114+
align-items: center;
115+
justify-content: space-between;
116+
padding: 0 4px;
117+
}
118+
119+
.status {
120+
font-size: 11px;
121+
font-weight: 500;
122+
padding: 2px 8px;
123+
border-radius: 20px;
124+
}
125+
126+
.status.ok {
127+
color: #1d7e4a;
128+
background: #d4f0e2;
129+
}
130+
131+
.status.error {
132+
color: #c0392b;
133+
background: #fde8e8;
134+
}
135+
136+
.status.empty {
137+
color: #86868b;
138+
}
139+
140+
.copy-btn {
141+
font-size: 12px;
142+
font-weight: 500;
143+
color: #0071e3;
144+
background: none;
145+
border: none;
146+
cursor: pointer;
147+
padding: 2px 6px;
148+
border-radius: 6px;
149+
transition: background 0.12s;
150+
font-family: inherit;
151+
}
152+
153+
.copy-btn:hover {
154+
background: rgba(0, 113, 227, 0.1);
155+
}
156+
157+
.copy-btn:active {
158+
background: rgba(0, 113, 227, 0.2);
159+
}
160+
161+
.copy-btn.copied {
162+
color: #1d7e4a;
163+
}
164+
165+
.divider {
166+
width: 1px;
167+
background: #d2d2d7;
168+
margin: 0;
169+
align-self: stretch;
170+
}
171+
172+
.options {
173+
display: flex;
174+
align-items: center;
175+
gap: 16px;
176+
padding: 0 28px 12px;
177+
}
178+
179+
.options label {
180+
display: flex;
181+
align-items: center;
182+
gap: 6px;
183+
font-size: 13px;
184+
color: #3d3d3f;
185+
cursor: pointer;
186+
user-select: none;
187+
}
188+
189+
.options input[type="checkbox"] {
190+
width: 14px;
191+
height: 14px;
192+
accent-color: #0071e3;
193+
cursor: pointer;
194+
}
195+
196+
.options-label {
197+
font-size: 11px;
198+
font-weight: 600;
199+
letter-spacing: 0.5px;
200+
text-transform: uppercase;
201+
color: #86868b;
202+
margin-right: 4px;
203+
}
204+
205+
footer {
206+
text-align: center;
207+
padding: 16px;
208+
font-size: 14px;
209+
color: #86868b;
210+
}
211+
212+
footer a {
213+
color: #0071e3;
214+
text-decoration: none;
215+
margin: 0 10px;
216+
}
217+
218+
footer a:hover {
219+
text-decoration: underline;
220+
}
221+
</style>
222+
</head>
223+
<body>
224+
<header>
225+
<h1>YAML Sorter</h1>
226+
<span>Sorts top-level keys alphabetically</span>
227+
</header>
228+
229+
<div class="options">
230+
<span class="options-label">Options</span>
231+
<label>
232+
<input type="checkbox" id="deepSort" /> Deep sort (nested keys)
233+
</label>
234+
<label>
235+
<input type="checkbox" id="lineWidth" /> Preserve line width
236+
</label>
237+
</div>
238+
239+
<div class="main">
240+
<div class="pane">
241+
<div class="pane-header">
242+
<span class="pane-label">Input</span>
243+
<span class="status empty" id="inputStatus"></span>
244+
</div>
245+
<textarea
246+
id="input"
247+
placeholder="Paste your YAML here…"
248+
spellcheck="false"
249+
></textarea>
250+
</div>
251+
252+
<div class="pane">
253+
<div class="pane-header">
254+
<span class="pane-label">Sorted</span>
255+
<div style="display: flex; align-items: center; gap: 10px">
256+
<span class="status empty" id="outputStatus"></span>
257+
<button class="copy-btn" id="copyBtn">Copy</button>
258+
</div>
259+
</div>
260+
<textarea
261+
id="output"
262+
readonly
263+
placeholder="Sorted YAML will appear here…"
264+
spellcheck="false"
265+
></textarea>
266+
</div>
267+
</div>
268+
269+
<script type="module">
270+
const input = document.getElementById("input");
271+
const output = document.getElementById("output");
272+
const inputStatus = document.getElementById("inputStatus");
273+
const outputStatus = document.getElementById("outputStatus");
274+
const copyBtn = document.getElementById("copyBtn");
275+
const deepSortCb = document.getElementById("deepSort");
276+
const lineWidthCb = document.getElementById("lineWidth");
277+
278+
function sortKeys(obj, deep) {
279+
if (typeof obj !== "object" || obj === null || Array.isArray(obj))
280+
return obj;
281+
const sorted = {};
282+
Object.keys(obj)
283+
.sort((a, b) =>
284+
a.localeCompare(b, undefined, { sensitivity: "base" }),
285+
)
286+
.forEach((key) => {
287+
sorted[key] = deep ? sortKeys(obj[key], deep) : obj[key];
288+
});
289+
return sorted;
290+
}
291+
292+
function process() {
293+
const raw = input.value.trim();
294+
if (!raw) {
295+
inputStatus.textContent = "—";
296+
inputStatus.className = "status empty";
297+
outputStatus.textContent = "—";
298+
outputStatus.className = "status empty";
299+
output.value = "";
300+
return;
301+
}
302+
303+
try {
304+
const parsed = jsyaml.load(raw);
305+
const keyCount = Object.keys(parsed).length;
306+
inputStatus.textContent = `${keyCount} key${keyCount !== 1 ? "s" : ""}`;
307+
inputStatus.className = "status ok";
308+
309+
const deep = deepSortCb.checked;
310+
const sorted = sortKeys(parsed, deep);
311+
312+
const dumpOpts = {
313+
lineWidth: lineWidthCb.checked ? 80 : -1,
314+
noRefs: true,
315+
sortKeys: false,
316+
};
317+
318+
output.value = jsyaml.dump(sorted, dumpOpts);
319+
outputStatus.textContent = "Sorted";
320+
outputStatus.className = "status ok";
321+
} catch (e) {
322+
inputStatus.textContent = "Invalid YAML";
323+
inputStatus.className = "status error";
324+
outputStatus.textContent = "—";
325+
outputStatus.className = "status empty";
326+
output.value = "";
327+
}
328+
}
329+
330+
input.addEventListener("input", process);
331+
deepSortCb.addEventListener("change", process);
332+
lineWidthCb.addEventListener("change", process);
333+
334+
copyBtn.addEventListener("click", () => {
335+
if (!output.value) return;
336+
navigator.clipboard.writeText(output.value).then(() => {
337+
copyBtn.textContent = "Copied!";
338+
copyBtn.classList.add("copied");
339+
setTimeout(() => {
340+
copyBtn.textContent = "Copy";
341+
copyBtn.classList.remove("copied");
342+
}, 1500);
343+
});
344+
});
345+
</script>
346+
347+
<footer>
348+
<a href="../index.html">Home</a> |
349+
<a href="https://github.com/kzuraw/tools/blob/main/html/yaml-sorter.html"
350+
>Source Code</a
351+
>
352+
|
353+
<a href="https://kzuraw.com">kzuraw.com</a>
354+
</footer>
355+
</body>
356+
</html>

index.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ <h1>kzuraw tools</h1>
9797
>convert SVG to React components with camelCased props</span
9898
>
9999
</li>
100+
<li>
101+
<a href="html/yaml-sorter.html">YAML Sorter</a>
102+
<span class="description"
103+
>sort YAML keys alphabetically with deep sort option</span
104+
>
105+
</li>
100106
</ul>
101107
<footer>
102108
<a href="https://github.com/kzuraw/tools">Source Code</a> |

0 commit comments

Comments
 (0)