odin-html

HTML Parsing library in Odin.
Log | Files | Refs | README | LICENSE

html.odin (5101B)


      1 package html
      2 
      3 // Node_Ref is an index into Document.nodes.
      4 // -1 means "none" (no parent, no child, no sibling).
      5 Node_Ref :: int
      6 
      7 // Attribute_Ref is an index into Document.attrs.
      8 Attribute_Ref :: int
      9 
     10 // Node_Kind classifies a node in the document tree.
     11 Node_Kind :: enum u8 {
     12 	Element, // an HTML tag: <div>, <p>, ...
     13 	Text, // raw text content
     14 	Comment, // <!-- ... -->
     15 	Doctype, // <!DOCTYPE ...>
     16 }
     17 
     18 // Node is a single node in the document tree.
     19 //
     20 // Nodes are stored in a #soa[] slice on the Document (see Document.nodes).
     21 // All cross-references use integer indices, never pointers, so the arena
     22 // can be frozen (immutable) after parse and traversed cache-efficiently.
     23 //
     24 // Index conventions: -1 means "none".
     25 Node :: struct {
     26 	kind:         Node_Kind,
     27 	parent:       Node_Ref, // index of parent node; -1 for root
     28 	first_child:  Node_Ref, // index of first child; -1 if leaf
     29 	last_child:   Node_Ref, // index of last child; -1 if leaf
     30 	next_sibling: Node_Ref, // index of next sibling; -1 if last
     31 	prev_sibling: Node_Ref, // index of prev sibling; -1 if first
     32 	child_count:  u16,
     33 	attrs_offset: Attribute_Ref, // index into Document.attrs; -1 if none
     34 	attrs_count:  u16,
     35 	name:         string, // tag name for Element; "" otherwise (slices source)
     36 	text:         string, // text/comment body for Text/Comment (slices source)
     37 }
     38 
     39 // Attribute is a single name="value" pair on an Element node.
     40 // Stored in a flat []Attribute on the Document; a node references a
     41 // contiguous run [attrs_offset, attrs_offset+attrs_count).
     42 Attribute :: struct {
     43 	name:  string, // slices source
     44 	value: string, // slices source; "" for boolean attributes
     45 }
     46 
     47 // Document is the immutable, fully-parsed HTML document.
     48 //
     49 // All strings on nodes and attributes are slices into `source`, which the
     50 // Document owns. Delete with document_delete when done.
     51 Document :: struct {
     52 	source:   string, // owned copy of the input
     53 	nodes:    #soa[]Node, // frozen arena; index 0 is the root
     54 	attrs:    []Attribute,
     55 	preamble: string, // anything before the first tag (e.g. doctype), slices source
     56 }
     57 
     58 // node_name returns the tag name of an Element, or "" for other kinds.
     59 node_name :: proc(doc: ^Document, ref: Node_Ref) -> string {
     60 	return doc.nodes[ref].name
     61 }
     62 
     63 // node_kind returns the Node_Kind of the node at ref.
     64 node_kind :: proc(doc: ^Document, ref: Node_Ref) -> Node_Kind {
     65 	return doc.nodes[ref].kind
     66 }
     67 
     68 // node_text returns the text/comment body for Text and Comment nodes.
     69 node_text :: proc(doc: ^Document, ref: Node_Ref) -> string {
     70 	return doc.nodes[ref].text
     71 }
     72 
     73 // node_parent returns the parent node ref, or -1 for the root.
     74 node_parent :: proc(doc: ^Document, ref: Node_Ref) -> Node_Ref {
     75 	return doc.nodes[ref].parent
     76 }
     77 
     78 // node_first_child returns the first child ref, or -1 if leaf.
     79 node_first_child :: proc(doc: ^Document, ref: Node_Ref) -> Node_Ref {
     80 	return doc.nodes[ref].first_child
     81 }
     82 
     83 // node_last_child returns the last child ref, or -1 if leaf.
     84 node_last_child :: proc(doc: ^Document, ref: Node_Ref) -> Node_Ref {
     85 	return doc.nodes[ref].last_child
     86 }
     87 
     88 // node_next_sibling returns the next sibling ref, or -1 if last.
     89 node_next_sibling :: proc(doc: ^Document, ref: Node_Ref) -> Node_Ref {
     90 	return doc.nodes[ref].next_sibling
     91 }
     92 
     93 // node_prev_sibling returns the previous sibling ref, or -1 if first.
     94 node_prev_sibling :: proc(doc: ^Document, ref: Node_Ref) -> Node_Ref {
     95 	return doc.nodes[ref].prev_sibling
     96 }
     97 
     98 // node_child_count returns the number of children.
     99 node_child_count :: proc(doc: ^Document, ref: Node_Ref) -> int {
    100 	return int(doc.nodes[ref].child_count)
    101 }
    102 
    103 // Node_Children is a zero-allocation iterator over a node's children.
    104 // It lives on the caller's stack and walks the sibling chain.
    105 Node_Children :: struct {
    106 	doc:     ^Document,
    107 	current: Node_Ref,
    108 }
    109 
    110 // node_children returns an iterator over the children of a node.
    111 // The iterator walks the sibling chain starting from first_child.
    112 node_children :: proc(doc: ^Document, ref: Node_Ref) -> Node_Children {
    113 	return {doc = doc, current = doc.nodes[ref].first_child}
    114 }
    115 
    116 // next yields the next child ref, or false when exhausted.
    117 next :: proc(it: ^Node_Children) -> (ref: Node_Ref, ok: bool) {
    118 	if it.current < 0 {
    119 		return
    120 	}
    121 	ref = it.current
    122 	it.current = it.doc.nodes[ref].next_sibling
    123 	return ref, true
    124 }
    125 
    126 // node_attr returns the value of the named attribute and whether it exists.
    127 node_attr :: proc(doc: ^Document, ref: Node_Ref, name: string) -> (string, bool) {
    128 	offset := doc.nodes[ref].attrs_offset
    129 	count := int(doc.nodes[ref].attrs_count)
    130 	for k in 0 ..< count {
    131 		attr := &doc.attrs[offset + k]
    132 		if attr.name == name {
    133 			return attr.value, true
    134 		}
    135 	}
    136 	return "", false
    137 }
    138 
    139 // node_has_attr reports whether the node has the named attribute.
    140 node_has_attr :: proc(doc: ^Document, ref: Node_Ref, name: string) -> bool {
    141 	_, ok := node_attr(doc, ref, name)
    142 	return ok
    143 }
    144 
    145 // document_delete frees the arena, attribute array, and owned source.
    146 document_delete :: proc(doc: ^Document) {
    147 	delete(doc.nodes)
    148 	delete(doc.attrs)
    149 	delete(doc.source)
    150 	doc.nodes = nil
    151 	doc.attrs = nil
    152 	doc.source = ""
    153 	doc.preamble = ""
    154 }
    155