silly-stuff/odin-templating/html_builder.odin

238 lines
5.4 KiB
Odin

package html_builder
import "core:fmt"
import "core:slice"
import "core:strings"
Element :: struct {
tag_name: string,
id: string,
children: [dynamic]Node,
attributes: map[string]string,
class_list: [dynamic]string,
}
Node :: union {
Element,
string,
}
empty_tags: map[string]bool = {
"img" = true,
"br" = true,
"hr" = true,
}
indent :: proc(sb: ^strings.Builder, depth: int) {
for i in 0 ..< depth {
fmt.sbprintf(sb, "\t")
}
}
element_to_string :: proc(sb: ^strings.Builder, e: Element, depth: int = 0) -> string {
indent(sb, depth)
fmt.sbprintf(sb, "<%s", e.tag_name)
for name, attr in e.attributes {
fmt.sbprintf(sb, " %s=\"%s\"", name, attr)
}
fmt.sbprintf(sb, ">")
children_len := len(e.children)
if children_len == 1 {
switch ch in e.children[0] {
case string:
fmt.sbprintf(sb, "%s", ch)
case Element:
fmt.sbprintf(sb, "\n")
element_to_string(sb, ch, depth + 1)
fmt.sbprintf(sb, "\n")
indent(sb, depth)
}
}
if children_len > 1 {
for child, idx in e.children {
fmt.sbprintf(sb, "\n")
switch ch in child {
case string:
indent(sb, depth + 1)
fmt.sbprintf(sb, "%s", ch)
case Element:
element_to_string(sb, ch, depth + 1)
}
}
fmt.sbprintf(sb, "\n")
indent(sb, depth)
}
if !empty_tags[e.tag_name] do fmt.sbprintf(sb, "</%s>", e.tag_name)
return strings.to_string(sb^)
}
parent_element: ^Element
elements_by_id: map[string]^Element
elements_by_classes: map[string][dynamic]^Element
_element_scope_start :: proc(tag_name: string, id: string = "") -> (previous_parent: ^Element) {
children := &parent_element.children
new_idx := len(children^)
new_element := Element {
tag_name = tag_name,
children = make([dynamic]Node),
}
append(children, new_element)
previous_parent = parent_element
parent_element = &children[new_idx].(Element)
return previous_parent
}
_element_scope_end :: proc(previous_parent: ^Element) {
parent_element = previous_parent
}
// @(deferred_out = free_html)
html :: proc() -> ^Element {
parent_element = new(Element)
parent_element.tag_name = "html"
parent_element.children = make([dynamic]Node)
return parent_element
}
text :: proc(txt: string) {
append(&parent_element.children, txt)
}
attr :: proc(name: string, value: string) {
if name == "class" {
class(value)
return
}
map_insert(&parent_element.attributes, name, value)
}
id :: proc(id: string) {
if id != "" {
parent_element.id = id
parent_element.attributes["id"] = id
elements_by_id[id] = parent_element
}
}
class :: proc(class_names: string) {
classes: [dynamic]string
for class in strings.split(class_names, " ") {
_, found := slice.linear_search(parent_element.class_list[:], class)
if elements_by_classes[class] == nil do elements_by_classes[class] = make([dynamic]^Element)
if !found {
append(&classes, class)
append(&elements_by_classes[class], parent_element)
}
}
append(&parent_element.class_list, ..classes[:])
map_insert(
&parent_element.attributes,
"class",
strings.join(parent_element.class_list[:], " "),
)
}
free_html :: proc(el: ^Element) {
defer parent_element = nil
handle_children :: proc(children: [dynamic]Node) {
for e in children {
_e, ok := e.(Element)
if ok do handle_children(_e.children)
}
delete(children)
}
handle_children(el.children)
free(el)
}
@(deferred_out = _element_scope_end)
body :: proc(id: string = "") -> (previous_parent: ^Element) {
return _element_scope_start("body", id)
}
@(deferred_out = _element_scope_end)
head :: proc(id: string = "") -> (previous_parent: ^Element) {
return _element_scope_start("head", id)
}
@(deferred_out = _element_scope_end)
title :: proc(txt: string = "") -> (previous_parent: ^Element) {
el := _element_scope_start("title")
text(txt)
return el
}
@(deferred_out = _element_scope_end)
h1 :: proc(txt: string = "") -> (previous_parent: ^Element) {
el := _element_scope_start("h1")
text(txt)
return el
}
@(deferred_out = _element_scope_end)
h2 :: proc(txt: string = "") -> (previous_parent: ^Element) {
el := _element_scope_start("h2")
text(txt)
return el
}
@(deferred_out = _element_scope_end)
h3 :: proc(txt: string = "") -> (previous_parent: ^Element) {
el := _element_scope_start("h3")
text(txt)
return el
}
@(deferred_out = _element_scope_end)
h4 :: proc(txt: string = "") -> (previous_parent: ^Element) {
el := _element_scope_start("h4")
text(txt)
return el
}
@(deferred_out = _element_scope_end)
h5 :: proc(txt: string = "") -> (previous_parent: ^Element) {
el := _element_scope_start("h5")
text(txt)
return el
}
@(deferred_out = _element_scope_end)
h6 :: proc(txt: string = "") -> (previous_parent: ^Element) {
el := _element_scope_start("h6")
text(txt)
return el
}
@(deferred_out = _element_scope_end)
div :: proc(id: string = "") -> (previous_parent: ^Element) {
previous_parent = _element_scope_start("div", id)
return previous_parent
}
@(deferred_out = _element_scope_end)
img :: proc(src: string = "", alt: string = "") -> (previous_parent: ^Element) {
previous_parent = _element_scope_start("img")
parent_element.children = nil
parent_element.attributes["src"] = src
if alt != "" do parent_element.attributes["alt"] = alt
return previous_parent
}
@(deferred_out = _element_scope_end)
a :: proc(txt: string, href: string = "") -> (previous_parent: ^Element) {
previous_parent = _element_scope_start("a")
append(&parent_element.children, txt)
parent_element.attributes["href"] = href
return previous_parent
}