package main import ts "treesitter" import "core:log" import "core:strings" PRESERVE_TAGS :: [?]string{"pre", "code", "textarea"} Range :: struct { start: u32, end: u32, } // minify_html collapses insignificant whitespace, strips comments, and minifies // inline , which has no raw_text child. style_raw_text :: proc(style_element: ts.Node) -> (raw: ts.Node, ok: bool) { child_count := ts.node_named_child_count(style_element) for i in 0 ..< child_count { child := ts.node_named_child(style_element, u32(i)) if string(ts.node_type(child)) == "raw_text" { return child, true } } return {}, false } html_tag_name :: proc(element: ts.Node, source: string) -> string { child_count := ts.node_named_child_count(element) for i in 0 ..< child_count { child := ts.node_named_child(element, u32(i)) if string(ts.node_type(child)) == "start_tag" { tag_child_count := ts.node_named_child_count(child) for j in 0 ..< tag_child_count { tag_child := ts.node_named_child(child, u32(j)) if string(ts.node_type(tag_child)) == "tag_name" { start := ts.node_start_byte(tag_child) end := ts.node_end_byte(tag_child) return source[start:end] } } } } return "" } is_preserve_tag :: proc(tag: string) -> bool { for t in PRESERVE_TAGS { if tag == t do return true } return false } CSS_DELIMS :: [?]u8{'{', '}', ':', ';', ','} is_css_delim :: proc(c: u8) -> bool { for d in CSS_DELIMS { if c == d do return true } return false } // minify_css collapses insignificant whitespace and strips comments. The caller // owns `parser` and passes it in; it may be reused across calls. On success it // returns (owned string, true) and the caller must free the string. On failure // (nil parser, parse failure, or syntax errors) it returns ("", false) and the // caller should keep the original source unchanged. minify_css :: proc(parser: ts.Parser, source: string) -> (minified: string, ok: bool) { if parser == nil { return minified, ok } source_c := strings.clone_to_cstring(source) defer delete(source_c) tree := ts.parser_parse_string(parser, nil, source_c, u32(len(source))) if tree == nil { ok = false return minified, ok } defer ts.tree_delete(tree) root := ts.tree_root_node(tree) if ts.node_has_error(root) { log.warnf("minify: CSS parse errors, skipping minification") ok = false return minified, ok } comments: [dynamic]Range defer delete(comments) collect_css_comments(root, &comments) sb := strings.builder_make() ci := 0 i := 0 last_written: u8 = 0 for i < len(source) { if ci < len(comments) && u32(i) >= comments[ci].start { i = int(comments[ci].end) ci += 1 continue } c := source[i] if c == ' ' || c == '\t' || c == '\n' || c == '\r' { j := i + 1 for j < len(source) { c2 := source[j] if c2 != ' ' && c2 != '\t' && c2 != '\n' && c2 != '\r' { break } j += 1 } next: u8 = 0 if j < len(source) { next = source[j] } // Emit a single separating space, but never a leading space // (last_written == 0), a doubled space (last_written == ' ', // which happens when a stripped comment sits between two // whitespace runs), or one adjacent to a delimiter. if last_written != 0 && last_written != ' ' && !is_css_delim(last_written) && !is_css_delim(next) { strings.write_byte(&sb, ' ') last_written = ' ' } i = j } else { strings.write_byte(&sb, c) last_written = c i += 1 } } minified, ok = strings.to_string(sb), true return minified, ok } collect_css_comments :: proc(node: ts.Node, comments: ^[dynamic]Range) { child_count := ts.node_named_child_count(node) for i in 0 ..< child_count { child := ts.node_named_child(node, u32(i)) if string(ts.node_type(child)) == "comment" { append( comments, Range{start = ts.node_start_byte(child), end = ts.node_end_byte(child)}, ) } else { collect_css_comments(child, comments) } } }