mirror of
https://github.com/sbrow/thor.git
synced 2026-08-26 19:33:32 -04:00
256 lines
5.3 KiB
Odin
256 lines
5.3 KiB
Odin
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 :: proc(source: string) -> string {
|
|
gc := ts.ensure_parser("html")
|
|
if gc == nil {
|
|
return source
|
|
}
|
|
|
|
source_c := strings.clone_to_cstring(source)
|
|
defer delete(source_c)
|
|
|
|
tree := ts.parser_parse_string(gc.parser, nil, source_c, u32(len(source)))
|
|
if tree == nil {
|
|
return source
|
|
}
|
|
defer ts.tree_delete(tree)
|
|
|
|
root := ts.tree_root_node(tree)
|
|
|
|
if ts.node_has_error(root) {
|
|
log.warnf("minify: HTML parse errors, skipping minification")
|
|
return source
|
|
}
|
|
|
|
comments: [dynamic]Range
|
|
defer delete(comments)
|
|
preserves: [dynamic]Range
|
|
defer delete(preserves)
|
|
|
|
collect_html_ranges(root, source, &comments, &preserves)
|
|
|
|
sb := strings.builder_make()
|
|
|
|
ci := 0
|
|
pi := 0
|
|
i := 0
|
|
last_written: u8 = 0
|
|
|
|
for i < len(source) {
|
|
if pi < len(preserves) && u32(i) >= preserves[pi].start {
|
|
p := preserves[pi]
|
|
segment := source[i:p.end]
|
|
strings.write_string(&sb, segment)
|
|
if len(segment) > 0 {
|
|
last_written = segment[len(segment)-1]
|
|
}
|
|
i = int(p.end)
|
|
pi += 1
|
|
continue
|
|
}
|
|
|
|
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]
|
|
}
|
|
if last_written != '>' || next != '<' {
|
|
strings.write_byte(&sb, ' ')
|
|
last_written = ' '
|
|
}
|
|
i = j
|
|
} else {
|
|
strings.write_byte(&sb, c)
|
|
last_written = c
|
|
i += 1
|
|
}
|
|
}
|
|
|
|
return strings.to_string(sb)
|
|
}
|
|
|
|
collect_html_ranges :: proc(
|
|
node: ts.Node,
|
|
source: string,
|
|
comments: ^[dynamic]Range,
|
|
preserves: ^[dynamic]Range,
|
|
) {
|
|
child_count := ts.node_named_child_count(node)
|
|
for i in 0..<child_count {
|
|
child := ts.node_named_child(node, u32(i))
|
|
type_str := string(ts.node_type(child))
|
|
|
|
if type_str == "comment" {
|
|
append(comments, Range{
|
|
start = ts.node_start_byte(child),
|
|
end = ts.node_end_byte(child),
|
|
})
|
|
} else if type_str == "script_element" || type_str == "style_element" {
|
|
append(preserves, Range{
|
|
start = ts.node_start_byte(child),
|
|
end = ts.node_end_byte(child),
|
|
})
|
|
} else if type_str == "element" {
|
|
tag := html_tag_name(child, source)
|
|
if is_preserve_tag(tag) {
|
|
append(preserves, Range{
|
|
start = ts.node_start_byte(child),
|
|
end = ts.node_end_byte(child),
|
|
})
|
|
} else {
|
|
collect_html_ranges(child, source, comments, preserves)
|
|
}
|
|
} else {
|
|
collect_html_ranges(child, source, comments, preserves)
|
|
}
|
|
}
|
|
}
|
|
|
|
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 :: proc(source: string) -> string {
|
|
gc := ts.ensure_parser("css")
|
|
if gc == nil {
|
|
return source
|
|
}
|
|
|
|
source_c := strings.clone_to_cstring(source)
|
|
defer delete(source_c)
|
|
|
|
tree := ts.parser_parse_string(gc.parser, nil, source_c, u32(len(source)))
|
|
if tree == nil {
|
|
return source
|
|
}
|
|
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")
|
|
return source
|
|
}
|
|
|
|
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]
|
|
}
|
|
|
|
if !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
|
|
}
|
|
}
|
|
|
|
return strings.to_string(sb)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|