Skip to content

Commit 1faf4ed

Browse files
Add better formatting for JS inside script tags (#126)
* Add better formatting for JS inside script tags Fixes #115 * More cases for JS formatting `case` and `default` code blocks within `switch` blocks are now properly formatted. Single-line bodies for `if`, `else`, `while`, and `for` statements are now properly formatted. * Clean up the code a bit --------- Co-authored-by: Loris Cro <kappaloris@gmail.com>
1 parent 21d5f85 commit 1faf4ed

2 files changed

Lines changed: 1337 additions & 3 deletions

File tree

src/html/Ast.zig

Lines changed: 350 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const Element = @import("Element.zig");
1515
const elements = Element.all;
1616
const kinds = Element.elements;
1717
const Attribute = @import("Attribute.zig");
18+
const JsTokenizer = @import("../js/Tokenizer.zig");
1819

1920
const log = std.log.scoped(.@"html/ast");
2021
const fmtlog = std.log.scoped(.@"html/ast/fmt");
@@ -32,9 +33,9 @@ pub const Kind = enum {
3233

3334
// superhtml
3435
extend, super, ctx,
35-
36+
3637
___, // invalid or web component (or superhtml if not in shtml mode)
37-
38+
3839
// Begin of html tags
3940
a, abbr, address, area, article, aside, audio, b, base, bdi, bdo,
4041
blockquote, body, br, button, canvas, caption, cite, code, col, colgroup,
@@ -1167,7 +1168,7 @@ pub fn render(ast: Ast, src: []const u8, w: *Writer) !void {
11671168
first = false;
11681169
}
11691170
},
1170-
.style, .script => {
1171+
.style => {
11711172
var css_indent = indentation;
11721173
var it = std.mem.splitScalar(u8, txt, '\n');
11731174
var first = true;
@@ -1200,6 +1201,105 @@ pub fn render(ast: Ast, src: []const u8, w: *Writer) !void {
12001201

12011202
if (it.peek() != null) try w.print("\n", .{});
12021203

1204+
first = false;
1205+
}
1206+
},
1207+
.script => {
1208+
// Use JS tokenizer for proper brace/bracket/paren tracking
1209+
// that correctly ignores braces in strings, comments, and regex.
1210+
// Indent changes are clamped to ±1 per line to match common
1211+
// formatting styles where multiple braces on one line don't
1212+
// cause excessive indentation.
1213+
var js_indent = indentation;
1214+
// Track if we're inside a case block (between case/default: and break/return/etc)
1215+
var in_case_block = false;
1216+
// Track if previous line expects a single-line body (if/else/for/while without brace)
1217+
var pending_body = false;
1218+
var it = std.mem.splitScalar(u8, txt, '\n');
1219+
var first = true;
1220+
var empty_line = false;
1221+
while (it.next()) |raw_line| {
1222+
const line = std.mem.trim(
1223+
u8,
1224+
raw_line,
1225+
&std.ascii.whitespace,
1226+
);
1227+
if (line.len == 0) {
1228+
if (empty_line) continue;
1229+
empty_line = true;
1230+
try w.print("\n", .{});
1231+
continue;
1232+
} else empty_line = false;
1233+
1234+
const info = JsTokenizer.lineIndentInfo(line);
1235+
1236+
// Handle outdenting for this line
1237+
if (info.starts_with_close) {
1238+
// If closing brace while in case block, outdent twice
1239+
// (once for case body, once for the brace)
1240+
if (in_case_block) {
1241+
js_indent -|= 2;
1242+
in_case_block = false;
1243+
} else {
1244+
js_indent -|= 1;
1245+
}
1246+
pending_body = false;
1247+
} else if (info.is_case_label) {
1248+
// Case labels outdent from case body level
1249+
if (in_case_block) {
1250+
js_indent -|= 1;
1251+
}
1252+
pending_body = false;
1253+
} else if (info.starts_with_else) {
1254+
// else/else if should be at the same level as if
1255+
// If we were in a pending body state, outdent first
1256+
if (pending_body) {
1257+
js_indent -|= 1;
1258+
pending_body = false;
1259+
}
1260+
} else if (pending_body) {
1261+
// This is the single-line body, already indented from previous line
1262+
// Will outdent after this line
1263+
}
1264+
1265+
if (!first) for (0..js_indent) |_| try w.print("\t", .{});
1266+
try w.print("{s}", .{line});
1267+
1268+
// Apply indent change for next line
1269+
if (info.is_case_label) {
1270+
// After a case label, indent for the case body
1271+
js_indent += 1;
1272+
in_case_block = true;
1273+
} else if (info.ends_case_block and in_case_block) {
1274+
// After break/return/etc, outdent from case body
1275+
js_indent -|= 1;
1276+
in_case_block = false;
1277+
} else if (info.expects_body) {
1278+
// Line ends with control flow expecting a body
1279+
// Indent for the next line (the body)
1280+
js_indent += 1;
1281+
pending_body = true;
1282+
} else if (pending_body) {
1283+
// Just finished a single-line body, outdent
1284+
js_indent -|= 1;
1285+
pending_body = false;
1286+
} else {
1287+
// Normal brace-based indentation, clamped to ±1.
1288+
// Account for the outdent we already applied.
1289+
const effective_delta = if (info.starts_with_close)
1290+
info.delta + 1
1291+
else
1292+
info.delta;
1293+
1294+
if (effective_delta > 0) {
1295+
js_indent += 1;
1296+
} else if (effective_delta < 0) {
1297+
js_indent -|= 1;
1298+
}
1299+
}
1300+
1301+
if (it.peek() != null) try w.print("\n", .{});
1302+
12031303
first = false;
12041304
}
12051305
},
@@ -2151,6 +2251,252 @@ pub const Cursor = struct {
21512251
}
21522252
};
21532253

2254+
test "script formatting - basic" {
2255+
const case =
2256+
\\<script>
2257+
\\function hello() {
2258+
\\console.log("world");
2259+
\\}
2260+
\\</script>
2261+
;
2262+
const expected = comptime std.fmt.comptimePrint(
2263+
\\<script>
2264+
\\{0c}function hello() {{
2265+
\\{0c}{0c}console.log("world");
2266+
\\{0c}}}
2267+
\\</script>
2268+
\\
2269+
, .{'\t'});
2270+
const ast = try Ast.init(std.testing.allocator, case, .html, false);
2271+
defer ast.deinit(std.testing.allocator);
2272+
2273+
try std.testing.expectFmt(expected, "{f}", .{ast.formatter(case)});
2274+
}
2275+
2276+
test "script formatting - braces in strings" {
2277+
const case =
2278+
\\<script>
2279+
\\const x = "{ not a brace }";
2280+
\\const y = '{ also not }';
2281+
\\</script>
2282+
;
2283+
const expected = comptime std.fmt.comptimePrint(
2284+
\\<script>
2285+
\\{0c}const x = "{{ not a brace }}";
2286+
\\{0c}const y = '{{ also not }}';
2287+
\\</script>
2288+
\\
2289+
, .{'\t'});
2290+
const ast = try Ast.init(std.testing.allocator, case, .html, false);
2291+
defer ast.deinit(std.testing.allocator);
2292+
2293+
try std.testing.expectFmt(expected, "{f}", .{ast.formatter(case)});
2294+
}
2295+
2296+
test "script formatting - braces in comments" {
2297+
const case =
2298+
\\<script>
2299+
\\// { comment brace }
2300+
\\/* { block } */
2301+
\\const x = 1;
2302+
\\</script>
2303+
;
2304+
const expected = comptime std.fmt.comptimePrint(
2305+
\\<script>
2306+
\\{0c}// {{ comment brace }}
2307+
\\{0c}/* {{ block }} */
2308+
\\{0c}const x = 1;
2309+
\\</script>
2310+
\\
2311+
, .{'\t'});
2312+
const ast = try Ast.init(std.testing.allocator, case, .html, false);
2313+
defer ast.deinit(std.testing.allocator);
2314+
2315+
try std.testing.expectFmt(expected, "{f}", .{ast.formatter(case)});
2316+
}
2317+
2318+
test "script formatting - braces in regex" {
2319+
const case =
2320+
\\<script>
2321+
\\const re = /{.*}/g;
2322+
\\const x = 1;
2323+
\\</script>
2324+
;
2325+
const expected = comptime std.fmt.comptimePrint(
2326+
\\<script>
2327+
\\{0c}const re = /{{.*}}/g;
2328+
\\{0c}const x = 1;
2329+
\\</script>
2330+
\\
2331+
, .{'\t'});
2332+
const ast = try Ast.init(std.testing.allocator, case, .html, false);
2333+
defer ast.deinit(std.testing.allocator);
2334+
2335+
try std.testing.expectFmt(expected, "{f}", .{ast.formatter(case)});
2336+
}
2337+
2338+
test "script formatting - nested structures" {
2339+
const case =
2340+
\\<script>
2341+
\\const obj = {
2342+
\\users: [
2343+
\\{name: "Alice"},
2344+
\\{name: "Bob"}
2345+
\\]
2346+
\\};
2347+
\\</script>
2348+
;
2349+
const expected = comptime std.fmt.comptimePrint(
2350+
\\<script>
2351+
\\{0c}const obj = {{
2352+
\\{0c}{0c}users: [
2353+
\\{0c}{0c}{0c}{{name: "Alice"}},
2354+
\\{0c}{0c}{0c}{{name: "Bob"}}
2355+
\\{0c}{0c}]
2356+
\\{0c}}};
2357+
\\</script>
2358+
\\
2359+
, .{'\t'});
2360+
const ast = try Ast.init(std.testing.allocator, case, .html, false);
2361+
defer ast.deinit(std.testing.allocator);
2362+
2363+
try std.testing.expectFmt(expected, "{f}", .{ast.formatter(case)});
2364+
}
2365+
2366+
test "script formatting - else if chain" {
2367+
const case =
2368+
\\<script>
2369+
\\if (a) {
2370+
\\doA();
2371+
\\} else if (b) {
2372+
\\doB();
2373+
\\} else {
2374+
\\doC();
2375+
\\}
2376+
\\</script>
2377+
;
2378+
const expected = comptime std.fmt.comptimePrint(
2379+
\\<script>
2380+
\\{0c}if (a) {{
2381+
\\{0c}{0c}doA();
2382+
\\{0c}}} else if (b) {{
2383+
\\{0c}{0c}doB();
2384+
\\{0c}}} else {{
2385+
\\{0c}{0c}doC();
2386+
\\{0c}}}
2387+
\\</script>
2388+
\\
2389+
, .{'\t'});
2390+
const ast = try Ast.init(std.testing.allocator, case, .html, false);
2391+
defer ast.deinit(std.testing.allocator);
2392+
2393+
try std.testing.expectFmt(expected, "{f}", .{ast.formatter(case)});
2394+
}
2395+
2396+
test "script formatting - switch case" {
2397+
const case =
2398+
\\<script>
2399+
\\switch (x) {
2400+
\\case 1:
2401+
\\doOne();
2402+
\\break;
2403+
\\case 'foo':
2404+
\\doFoo();
2405+
\\break;
2406+
\\default:
2407+
\\doDefault();
2408+
\\}
2409+
\\</script>
2410+
;
2411+
const expected = comptime std.fmt.comptimePrint(
2412+
\\<script>
2413+
\\{0c}switch (x) {{
2414+
\\{0c}{0c}case 1:
2415+
\\{0c}{0c}{0c}doOne();
2416+
\\{0c}{0c}{0c}break;
2417+
\\{0c}{0c}case 'foo':
2418+
\\{0c}{0c}{0c}doFoo();
2419+
\\{0c}{0c}{0c}break;
2420+
\\{0c}{0c}default:
2421+
\\{0c}{0c}{0c}doDefault();
2422+
\\{0c}}}
2423+
\\</script>
2424+
\\
2425+
, .{'\t'});
2426+
const ast = try Ast.init(std.testing.allocator, case, .html, false);
2427+
defer ast.deinit(std.testing.allocator);
2428+
2429+
try std.testing.expectFmt(expected, "{f}", .{ast.formatter(case)});
2430+
}
2431+
2432+
test "script formatting - single-line if/else bodies" {
2433+
const case =
2434+
\\<script>
2435+
\\if (foo)
2436+
\\doSomething()
2437+
\\else if (bar)
2438+
\\doSomethingElse()
2439+
\\else
2440+
\\doDefault()
2441+
\\</script>
2442+
;
2443+
const expected = comptime std.fmt.comptimePrint(
2444+
\\<script>
2445+
\\{0c}if (foo)
2446+
\\{0c}{0c}doSomething()
2447+
\\{0c}else if (bar)
2448+
\\{0c}{0c}doSomethingElse()
2449+
\\{0c}else
2450+
\\{0c}{0c}doDefault()
2451+
\\</script>
2452+
\\
2453+
, .{'\t'});
2454+
const ast = try Ast.init(std.testing.allocator, case, .html, false);
2455+
defer ast.deinit(std.testing.allocator);
2456+
2457+
try std.testing.expectFmt(expected, "{f}", .{ast.formatter(case)});
2458+
}
2459+
2460+
test "script formatting - single-line for loop" {
2461+
const case =
2462+
\\<script>
2463+
\\for (const e of elements)
2464+
\\doSomething(e)
2465+
\\</script>
2466+
;
2467+
const expected = comptime std.fmt.comptimePrint(
2468+
\\<script>
2469+
\\{0c}for (const e of elements)
2470+
\\{0c}{0c}doSomething(e)
2471+
\\</script>
2472+
\\
2473+
, .{'\t'});
2474+
const ast = try Ast.init(std.testing.allocator, case, .html, false);
2475+
defer ast.deinit(std.testing.allocator);
2476+
2477+
try std.testing.expectFmt(expected, "{f}", .{ast.formatter(case)});
2478+
}
2479+
2480+
test "script formatting - single-line while loop" {
2481+
const case =
2482+
\\<script>
2483+
\\while (condition)
2484+
\\doSomething()
2485+
\\</script>
2486+
;
2487+
const expected = comptime std.fmt.comptimePrint(
2488+
\\<script>
2489+
\\{0c}while (condition)
2490+
\\{0c}{0c}doSomething()
2491+
\\</script>
2492+
\\
2493+
, .{'\t'});
2494+
const ast = try Ast.init(std.testing.allocator, case, .html, false);
2495+
defer ast.deinit(std.testing.allocator);
2496+
2497+
try std.testing.expectFmt(expected, "{f}", .{ast.formatter(case)});
2498+
}
2499+
21542500
// test "fuzz" {
21552501
// const Reader = std.Io.Reader;
21562502
// const generator = @import("../generator/html.zig");
@@ -2194,3 +2540,4 @@ pub const Cursor = struct {
21942540
// .out = &out.interface,
21952541
// }, Context.testOne, .{});
21962542
// }
2543+

0 commit comments

Comments
 (0)