@@ -15,6 +15,7 @@ const Element = @import("Element.zig");
1515const elements = Element .all ;
1616const kinds = Element .elements ;
1717const Attribute = @import ("Attribute.zig" );
18+ const JsTokenizer = @import ("../js/Tokenizer.zig" );
1819
1920const log = std .log .scoped (.@"html/ast" );
2021const 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