Repository navigation
Expand file tree
/
Copy pathdocument.go
More file actions
1378 lines (1250 loc) · 46.7 KB
/
Copy pathdocument.go
File metadata and controls
1378 lines (1250 loc) · 46.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package helium
import (
"bytes"
"errors"
"fmt"
"strings"
"unsafe"
"github.com/lestrrat-go/helium/enum"
"github.com/lestrrat-go/helium/internal/lexicon"
"github.com/lestrrat-go/helium/internal/pool"
)
// DocumentStandaloneType records the state of a document's standalone
// declaration, mirroring libxml2's xmlDoc.standalone. It is one of the
// Standalone* constants; any other value is out of the defined space and is
// treated the same as StandaloneImplicitNo — serialization emits no standalone
// attribute for it and DTD validation treats it as not-standalone. The full
// value space:
//
// - StandaloneExplicitYes: the XML declaration carried standalone="yes".
// - StandaloneExplicitNo: the XML declaration carried standalone="no".
// - StandaloneImplicitNo: no explicit standalone="yes"/"no" was seen. This is
// the parser's default, and the value a parsed document carries both when the
// XML declaration is absent entirely and when it is present without a
// standalone pseudo-attribute. It is also the value to pass to NewDocument
// for an ordinary API-built document. Serialization emits no standalone
// attribute.
// - StandaloneNoXMLDecl: a libxml2-compatible caller-selected sentinel for a
// constructed document with no XML declaration (used by NewHTMLDocument and
// the xslt3 serializer). The parser never produces it; serialization treats
// it as not-standalone and emits no standalone attribute.
// - StandaloneInvalidValue: an internal parser sentinel for a standalone
// value that has not been resolved yet; not meaningful on a finished
// document.
type DocumentStandaloneType int
const (
StandaloneInvalidValue DocumentStandaloneType = -99
StandaloneExplicitYes DocumentStandaloneType = 1
StandaloneExplicitNo DocumentStandaloneType = 0
StandaloneNoXMLDecl DocumentStandaloneType = -1
StandaloneImplicitNo DocumentStandaloneType = -2
)
// DocProperties is a bitmask of document properties, mirroring
// libxml2's xmlDocProperties. Properties are set by the parser
// or by user code to record how the document was produced and
// what validations it passed.
type DocProperties int
const (
DocWellFormed DocProperties = 1 << iota // document is XML well-formed
DocNSValid // document is namespace-valid
DocOld10 // parsed with XML 1.0 (4th edition or earlier)
DocDTDValid // DTD validation was successful
DocXInclude // XInclude substitution was done
DocUserBuilt // built via API, not by parsing
DocInternal // built for internal processing
DocHTML // parsed or built as HTML
)
// Document represents an XML document (libxml2: xmlDoc).
type Document struct {
docnode
version string
encoding string
standalone DocumentStandaloneType
url string // document URI for base URI resolution (mirrors libxml2's xmlDoc.URL)
properties DocProperties
intSubset *DTD
extSubset *DTD
ids map[string]*Element
idsSkip bool
// standaloneNormAttrs records specified attributes whose value was changed by
// tokenized-type normalization driven by an external-subset ATTLIST
// declaration. Populated by the parser (which alone holds the pre-normalization
// value) and consulted by DTD validation for the VC: Standalone Document
// Declaration in a standalone="yes" document. See valid.go.
standaloneNormAttrs []standaloneNormAttr
// Slab allocators for high-frequency node types.
// These reduce per-node heap allocation overhead by allocating
// nodes in chunks and handing them out one at a time.
// Pooled chunks are obtained from global pools and returned on Free().
// A document the parser did not build takes small heap chunks first (see
// slabGrowthSizes), so a small built tree does not hold a full pooled chunk.
elemSlab []Element
textSlab []Text
nsSlab []Namespace
attrSlab []Attribute
// Track allocated chunks for pool return.
elemChunks []*[slabSize]Element
textChunks []*[slabSize]Text
nsChunks []*[slabSize]Namespace
attrChunks []*[slabSize]Attribute
// Slab allocator for parsed text content bytes.
textContentSlab []byte
textContentChunks []*[textContentSlabSize]byte
// slabEscaped records that at least one slab-backed node owned by this
// document was linked into another document (an XInclude/XSLT-style
// cross-document move). Once set, Free must NOT return this document's chunks
// to the pool: a moved node still points into one of them, and recycling it
// would let a later parse overwrite the live node. Set by the tree-insertion
// paths (node.go noteCrossDocumentEscape).
slabEscaped bool
// pooledSlabs makes every slab chunk come from the pools from the first
// node on. The parser sets it on the document it builds: a parsed document
// is commonly released with Free and its chunks reused by the next parse,
// which the small heap chunks of the growth schedule would defeat.
pooledSlabs bool
// Number of small heap chunks each slab allocator has taken from its
// growth schedule (slabGrowthSizes, textContentGrowthSizes). Once a counter
// reaches the end of its schedule, that allocator draws pooled chunks.
elemGrowth uint8
textGrowth uint8
nsGrowth uint8
attrGrowth uint8
textContentGrowth uint8
// offChainChildClaim records that a node has been given this document as its
// parent WITHOUT being linked into the document's child list. The DTD-subset
// copy paths do it when copying an external subset: the copied subset
// claims the destination document and is then reachable only through
// ExtSubset. An append through such a subset records its own result as the
// document's lastChild, which moves that record off the child list, so the
// O(1) append-point resolution must stop trusting that record.
//
// The record is per-PARENT, and this document IS the parent that was handed
// the claimant: it declines only for appends onto the document node itself,
// never for the elements the document owns, whose own lastChild records the
// claimant says nothing about. (CreateInternalSubset also gives a DTD this
// document as its parent, but it splices that DTD into the child list, so it
// creates no claim.) Set by copy_dtd.go; read through
// node.go holdsOffChainChildClaim by tailJumpTarget and resolveOwnedTail.
offChainChildClaim bool
}
// NewDefaultDocument creates a minimal user-built document with version "1.0",
// no encoding, and implicit-no standalone (libxml2: xmlNewDoc).
func NewDefaultDocument() *Document {
doc := NewDocument("1.0", "", StandaloneImplicitNo)
doc.properties |= DocUserBuilt
return doc
}
// NewHTMLDocument creates a new HTML document (HTMLDocumentNode type). The
// HTML parser builds its documents through it, so like a document the XML
// parser builds, it draws its slab chunks from the pools from the first node
// on.
func NewHTMLDocument() *Document {
doc := &Document{
standalone: StandaloneNoXMLDecl,
properties: DocHTML,
pooledSlabs: true,
}
doc.etype = HTMLDocumentNode
doc.name = "(document)"
return doc
}
// NewDocument allocates an empty XML Document with the given XML
// declaration version, character encoding, and standalone status. The
// standalone argument is one of the Standalone* constants (see
// DocumentStandaloneType for the full value space); pass StandaloneImplicitNo
// for an ordinary document with no explicit standalone declaration. The
// returned document owns no children yet; use CreateElement and the other
// Create* methods to build a tree, then attach the root via AddChild.
func NewDocument(version, encoding string, standalone DocumentStandaloneType) *Document {
doc := &Document{
encoding: encoding,
standalone: standalone,
version: version,
}
doc.etype = DocumentNode
doc.name = "(document)"
return doc
}
// Free returns pooled slab chunks for reuse by future parse calls.
// This is optional — if not called, GC handles cleanup normally.
// Calling Free on a document that is still in use causes undefined behavior.
//
// If any of this document's slab-backed nodes was moved into another document
// (via AddChild/AddSibling/Replace), Free does NOT recycle its chunks: a moved
// node still references one of them, and Free zeroes every chunk it returns
// to the pool, so recycling it would wipe the live node and let a later parse
// overwrite it. In that case Free is a no-op and GC
// reclaims the chunks once they are no longer referenced.
func (d *Document) Free() {
if d.slabEscaped {
return
}
recycleNodeChunks(elemChunkPool, d.elemChunks, len(d.elemSlab))
recycleNodeChunks(textChunkPool, d.textChunks, len(d.textSlab))
recycleNodeChunks(nsChunkPool, d.nsChunks, len(d.nsSlab))
recycleNodeChunks(attrChunkPool, d.attrChunks, len(d.attrSlab))
for _, c := range d.textContentChunks {
textContentChunkPool.Put(c)
}
d.elemChunks = nil
d.textChunks = nil
d.nsChunks = nil
d.attrChunks = nil
d.textContentChunks = nil
d.elemSlab = nil
d.textSlab = nil
d.nsSlab = nil
d.attrSlab = nil
d.textContentSlab = nil
}
// Content returns the document's content: the content of its Element, Text,
// CDATA and EntityRef children, with every entity reference expanded and
// comments and PIs left out, the same text XPath string(/) reads. The DTD is
// not document content and adds nothing, so neither does any entity
// declaration it holds. libxml2 2.9's xmlNodeGetContent read a document the
// same way, adding only its element, text and CDATA children. A document with
// no such content returns nil.
func (d *Document) Content() []byte {
if d.firstChild == nil {
return nil
}
var b bytes.Buffer
appendDocumentContent(&b, &d.docnode)
return b.Bytes()
}
// AddChild appends cur as the last child of the document, detaching it from any
// previous parent first. It returns an error if cur is nil or if the insertion
// would create a cycle.
func (d *Document) AddChild(cur Node) error {
return addChild(d, cur)
}
// AppendText appends b as a Text child of the document, merging into a trailing
// Text node when possible.
func (d *Document) AppendText(b []byte) error {
return appendText(d, b)
}
// AddSibling returns an error. It exists to satisfy the Node interface.
func (d *Document) AddSibling(_ Node) error {
return errors.New("can't add sibling to a document")
}
// SetTreeDoc sets doc as this node's owning document, propagating to
// descendants only when this node's owner changes. It is a no-op when this node
// already owns doc.
func (d *Document) SetTreeDoc(doc *Document) {
setTreeDoc(d, doc)
}
// Encoding returns the document's recorded character encoding, or "utf8" if none
// is recorded. Use RawEncoding to obtain the recorded value without that
// default.
func (d *Document) Encoding() string {
// In order to differentiate between a document with explicit
// encoding in the XML declaration and one without, the XML dump
// routine must check for d.encoding == "", and not Encoding()
if enc := d.encoding; enc != "" {
return d.encoding
}
return "utf8"
}
// SetEncoding sets the document's recorded encoding; an empty string clears it.
func (d *Document) SetEncoding(enc string) {
d.encoding = enc
}
// RawEncoding returns the document's encoding exactly as recorded, WITHOUT the
// "utf8" default that Encoding() synthesizes for documents whose XML
// declaration omitted an encoding. An empty result means the source had no
// encoding declaration. This is the value the XML serializer consults (it emits
// encoding="..." only when the raw encoding is non-empty), so a faithful copy
// of a document — e.g. an xsl:strip-space copy — must propagate this rather
// than Encoding(), which would synthesize a spurious encoding="utf8".
func (d *Document) RawEncoding() string {
return d.encoding
}
// Standalone returns the value of the document's standalone declaration.
func (d *Document) Standalone() DocumentStandaloneType {
return d.standalone
}
// SkipIDs reports whether ID attribute interning was skipped when this
// document was produced (mirrors the parser's SkipIDs option). When true,
// GetElementByID and fn:id always return no match without an O(n) tree walk.
func (d *Document) SkipIDs() bool {
return d.idsSkip
}
// SetSkipIDs sets the document's ID-skip state. This is used when producing a
// derived document (e.g. an xsl:strip-space copy) that must mirror the source's
// ID semantics.
//
// The ID-skip state is authoritative: while it is true, GetElementByID (and
// therefore fn:id) resolves NO ids and returns nil — even if the document
// already has a populated ID table from a normal parse. Setting it back to
// false restores normal resolution against the existing table (or the lazy
// tree walk for API-built documents).
func (d *Document) SetSkipIDs(v bool) {
d.idsSkip = v
}
// Version returns the document's recorded XML version (for example "1.0" or
// "1.1"), or an empty string if none is recorded.
func (d *Document) Version() string {
return d.version
}
// SetVersion sets the document's XML declaration version (e.g. "1.0" or "1.1").
// Serialization consults this to decide whether XML 1.1 restricted control
// characters are emitted as character references instead of being rejected.
func (d *Document) SetVersion(v string) {
d.version = v
}
// URL returns the document URI, used as the base for relative URI resolution.
// This mirrors libxml2's xmlDoc.URL field. An empty URL is valid for in-memory
// documents; callers that need a non-empty diagnostic label must provide their
// own fallback.
func (d *Document) URL() string {
return d.url
}
// SetURL sets the document URI.
func (d *Document) SetURL(url string) {
d.url = url
}
// Properties returns the document's property flags.
func (d *Document) Properties() DocProperties {
return d.properties
}
// SetProperties replaces the document's property flags.
func (d *Document) SetProperties(p DocProperties) {
d.properties = p
}
// HasProperty reports whether all bits in p are set.
func (d *Document) HasProperty(p DocProperties) bool {
return d.properties&p == p
}
// IntSubset returns the document's internal DTD subset, or nil when none is
// present. The abbreviated name mirrors libxml2's xmlGetIntSubset and the
// intSubset document field. InternalSubset is the equivalent accessor that
// reports absence as an error instead of nil.
func (d *Document) IntSubset() *DTD {
return d.intSubset
}
// RemoveInternalSubset detaches the document's internal-subset DTD (if any) from
// the child list and clears the association, so a subsequent
// CreateInternalSubset can install a fresh one in its place. It is a no-op when
// no internal subset is present.
func (d *Document) RemoveInternalSubset() {
if d.intSubset == nil {
return
}
unlinkNode(d.intSubset)
d.intSubset = nil
}
// ExtSubset returns the document's external DTD subset, or nil when none is
// present. The abbreviated name mirrors libxml2's extSubset document field,
// matching the IntSubset accessor.
func (d *Document) ExtSubset() *DTD {
return d.extSubset
}
// Replace returns ErrInvalidOperation. It exists to satisfy the Node interface.
func (d *Document) Replace(_ ...Node) error {
return ErrInvalidOperation
}
// DocumentElement returns the root element of the document, or nil if none exists.
//
// The child scan goes through Children, so it applies the owned-boundary rule
// and a per-list seen guard: a corrupt document child list (a cyclic sibling
// pointer before the root) terminates the scan instead of hanging, which would
// otherwise stall DTD/XSD validation before the traversal cycle guards run.
func (d *Document) DocumentElement() *Element {
for n := range Children(d) {
if elem, ok := AsNode[*Element](n); ok {
return elem
}
}
return nil
}
// SetDocumentElement installs root as the document's root element, replacing an
// existing root element if one is present. It returns ErrNilNode if root is nil,
// and an error wrapping ErrInvalidOperation if root is not an *Element.
func (d *Document) SetDocumentElement(root MutableNode) error {
if d == nil {
return ErrNilNode
}
// Reject a nil or typed-nil operand BEFORE any root.Type() dereference so the
// call returns ErrNilNode instead of panicking.
if isNilNode(root) {
return ErrNilNode
}
// The document element must be a concrete *Element. Accepting any node kind
// here let a Text/Comment/DTD/NamespaceDecl node become the root, producing a
// document that is not well formed. Checking the concrete type (not root.Type())
// also rejects a non-element node that merely REPORTS ElementNode — e.g. an
// XIncludeMarker constructed with ElementNode — which DocumentElement() would
// never return, leaving the document element effectively nil.
if _, ok := AsNode[*Element](root); !ok {
return fmt.Errorf("%w: document element must be an element node, got %s", ErrInvalidOperation, root.Type())
}
// Do NOT link root to d here. Let AddChild/Replace perform the linking AFTER
// their cycle/self preflight so a rejected insertion leaves the candidate and
// the tree untouched. The scan goes through Children so a corrupt document
// child list (a cyclic sibling pointer before the root) terminates instead of
// hanging.
var old Node
for n := range Children(d) {
if n.Type() == ElementNode {
old = n
break
}
}
if old == nil {
if err := d.AddChild(root); err != nil {
return err
}
} else {
if err := old.(MutableNode).Replace(root); err != nil { //nolint:forcetypeassert
return err
}
}
return nil
}
// CreateReference builds an entity-reference node and, when a matching entity
// is declared in the document, attaches its replacement content — mirroring
// libxml2's xmlNewReference. Like CreateCharRef (and libxml2's xmlNewCharRef)
// the node is an EntityRefNode that serializes as "&" + Name() + ";"; the only
// difference is the declared-entity lookup performed here.
func (d *Document) CreateReference(name string) (*EntityRef, error) {
n, err := d.CreateCharRef(name)
if err != nil {
return nil, err
}
ent, ok := d.GetEntity(n.name)
if ok {
bindEntityReference(n, ent)
}
return n, nil
}
func bindEntityReference(ref *EntityRef, ent *Entity) {
ref.content = []byte(ent.content)
// Original code says:
// The parent pointer in entity is a DTD pointer and thus is NOT
// updated. Not sure if this is 100% correct.
setFirstChild(ref, ent)
setLastChild(ref, ent)
}
// CreateAttribute builds an attribute node named name, with value parsed into
// its child node list, and optional namespace ns. A colon in name is rejected;
// supply a namespaced name through ns instead. name is not otherwise checked
// against the XML Name/NCName grammar. When d is non-nil the attribute is
// drawn from the document's slab allocator and records d as its owner
// document, so it must not outlive a call to Document.Free. A nil receiver
// allocates a standalone attribute with no owning document.
func (d *Document) CreateAttribute(name, value string, ns *Namespace) (attr *Attribute, err error) {
if strings.ContainsRune(name, ':') {
return nil, fmt.Errorf("attribute name %q contains a colon: use CreateAttribute with a local name and Namespace parameter", name)
}
var n Node
if d != nil {
attr = d.allocAttribute(name, ns)
} else {
attr = newAttribute(name, ns)
}
if value != "" {
n, err = d.stringToNodeList(value)
if err != nil {
attr = nil
return
}
setFirstChild(attr, n)
for n != nil {
n.baseDocNode().parent = attr
x := n.NextSibling()
if x == nil {
setLastChild(attr, n)
}
n = x
}
}
return attr, nil
}
// CreateNamespace builds a namespace node binding prefix to uri. An empty prefix
// denotes the default namespace.
func (d *Document) CreateNamespace(prefix, uri string) (*Namespace, error) {
var ns *Namespace
if d != nil {
ns = d.allocNamespace()
} else {
ns = &Namespace{}
}
ns.prefix = prefix
ns.href = uri
ns.etype = NamespaceNode
ns.context = d
return ns, nil
}
func (d *Document) allocNamespace() *Namespace {
if len(d.nsSlab) == 0 {
d.nsSlab = growthChunk[Namespace](d.pooledSlabs, &d.nsGrowth)
}
if len(d.nsSlab) == 0 {
chunk := nsChunkPool.Get()
d.nsChunks = append(d.nsChunks, chunk)
d.nsSlab = chunk[:]
}
ns := &d.nsSlab[0]
d.nsSlab = d.nsSlab[1:]
return ns
}
func (d *Document) allocAttribute(name string, ns *Namespace) *Attribute {
if len(d.attrSlab) == 0 {
d.attrSlab = growthChunk[Attribute](d.pooledSlabs, &d.attrGrowth)
}
if len(d.attrSlab) == 0 {
chunk := attrChunkPool.Get()
d.attrChunks = append(d.attrChunks, chunk)
d.attrSlab = chunk[:]
}
attr := &d.attrSlab[0]
d.attrSlab = d.attrSlab[1:]
attr.etype = AttributeNode
attr.name = name
attr.ns = ns
// Record the owning document, mirroring CreateElement/CreateText: a
// slab-backed attribute must report its owner so a cross-document move can
// mark the source document escaped (noteCrossDocumentEscape) and keep Free
// from recycling the chunk out from under the moved attribute.
attr.doc = d
return attr
}
// createLiteralAttribute builds an attribute whose value holds no entity
// reference: a single Text child with value as its content, or no child for an
// empty value. The Text shares value's bytes (see below), which is safe for any
// string; the parser's values come from its valueArena.
func (d *Document) createLiteralAttribute(name, value string, ns *Namespace) *Attribute {
var attr *Attribute
if d != nil {
attr = d.allocAttribute(name, ns)
} else {
attr = newAttribute(name, ns)
}
if value == "" {
return attr
}
var t *Text
if d != nil {
t = d.allocText()
} else {
t = &Text{}
}
t.etype = TextNode
t.name = textNodeName
t.doc = d
// The text reads value's bytes in place instead of copying them. Nothing
// writes a Text's content bytes in place: the slice's capacity equals its
// length, so AppendText moves the content to new memory before it appends,
// and every reader (rawContent) treats the bytes as read-only.
t.content = unsafe.Slice(unsafe.StringData(value), len(value))
setFirstChild(attr, t)
setLastChild(attr, t)
t.parent = attr
return attr
}
// CreatePI builds a processing-instruction node with the given target and data.
func (d *Document) CreatePI(target, data string) *ProcessingInstruction {
pi := &ProcessingInstruction{
target: target,
data: data,
}
pi.doc = d
return pi
}
// CreateDTD builds a detached DTD node. It is not linked into the tree; use
// CreateInternalSubset to install an internal subset.
func (d *Document) CreateDTD() (*DTD, error) {
dtd := newDTD()
dtd.doc = d
return dtd, nil
}
// InternalSubset returns the document's internal DTD subset, reporting absence
// as an error. IntSubset is the equivalent accessor that returns nil instead.
func (d *Document) InternalSubset() (*DTD, error) {
// equiv: xmlGetIntSubset (tree.c)
if d.intSubset == nil {
return nil, ErrNoInternalSubset
}
return d.intSubset, nil
}
// CreateInternalSubset creates the document's internal DTD subset with the given
// root element name and external identifiers, and installs it on the document.
// It returns an error if the document already has an internal subset.
func (d *Document) CreateInternalSubset(name, externalID, systemID string) (*DTD, error) {
// equiv: xmlCreateIntSubset (tree.c)
if _, err := d.InternalSubset(); err == nil {
return nil, errors.New("document " + d.name + " already has an internal subset")
}
cur, err := d.CreateDTD()
if err != nil {
return nil, err
}
cur.name = name
cur.externalID = externalID
cur.systemID = systemID
if d == nil {
return cur, nil
}
d.intSubset = cur
cur.parent = d
cur.doc = d
// Insert before the root element (matching libxml2's xmlCreateIntSubset).
// If no children exist yet, just append. The scan goes through Children so a
// corrupt document child list terminates instead of hanging.
var root Node
for c := range Children(d) {
if c.Type() == ElementNode {
root = c
break
}
}
if root == nil {
if err := d.AddChild(cur); err != nil {
return nil, err
}
} else {
// Insert cur before root.
cur.next = root
if prev := root.PrevSibling(); prev != nil {
prev.baseDocNode().next = cur
cur.prev = prev
} else {
setFirstChild(d, cur)
}
root.baseDocNode().prev = cur
}
return cur, nil
}
const slabSize = 256
const textContentSlabSize = 64 * 1024
// slabGrowthSizes is the growth schedule of the node slab allocators for a
// document the parser did not build: its first chunks are small heap slices of
// these sizes, and only once they are used up does it draw pooled chunks of
// slabSize nodes. A small tree built through the Create* methods, such as an
// XSLT temporary tree, then holds a few hundred bytes per node type in place of
// a pooled chunk of tens of kilobytes. textContentGrowthSizes is the same
// schedule, in bytes, for text content; a request larger than the next size
// takes a chunk of its own size. Heap chunks are left to the GC and never go to
// the pools.
var (
slabGrowthSizes = [...]int{8, 32}
textContentGrowthSizes = [...]int{256, 2048}
)
// growthChunk returns the next small heap chunk from slabGrowthSizes and advances
// *taken, or nil when pooled is set or the schedule is used up, in which case
// the caller draws a pooled chunk.
func growthChunk[T any](pooled bool, taken *uint8) []T {
if pooled || int(*taken) >= len(slabGrowthSizes) {
return nil
}
n := slabGrowthSizes[*taken]
*taken++
return make([]T, n)
}
// recycleNodeChunks zeroes the used nodes of each chunk and returns the chunk
// to p. Node allocation does not clear a node: a chunk is zero when it comes
// from p, either fresh from the runtime or cleared here, and a growth-schedule
// chunk is a fresh heap slice. Clearing here also keeps a pooled chunk from
// holding pointers into the freed tree.
//
// chunks are in allocation order and only the last one can be partly used. A
// document draws a pooled chunk only once its growth schedule is used up, so
// while it holds any pooled chunk its slab is the unused tail of the last one:
// unused (the slab's length) counts the nodes in that tail, which are still
// zero, so only the used prefix is cleared.
//
// Free is the only path that returns a node chunk to a pool, and it goes
// through here.
func recycleNodeChunks[T any](p *pool.Pool[*[slabSize]T], chunks []*[slabSize]T, unused int) {
last := len(chunks) - 1
for i, c := range chunks {
used := slabSize
if i == last {
used -= unused
}
clear(c[:used])
p.Put(c)
}
}
var (
elemChunkPool = pool.New(func() *[slabSize]Element { return new([slabSize]Element) }, nil)
textChunkPool = pool.New(func() *[slabSize]Text { return new([slabSize]Text) }, nil)
nsChunkPool = pool.New(func() *[slabSize]Namespace { return new([slabSize]Namespace) }, nil)
attrChunkPool = pool.New(func() *[slabSize]Attribute { return new([slabSize]Attribute) }, nil)
textContentChunkPool = pool.New(func() *[textContentSlabSize]byte { return new([textContentSlabSize]byte) }, nil)
)
// CreateElement allocates a new Element named name and owned by this
// document, but does not attach it to the tree; the caller must insert it
// via AddChild or a sibling/parent method. A colon in name is rejected;
// supply a namespaced name through CreateElementNS instead. name is not
// otherwise checked against the XML Name/NCName grammar. When d is non-nil the
// element is drawn from the document's slab allocator, so the element must not
// outlive a call to Document.Free. A nil receiver allocates a standalone
// element with no owning document.
func (d *Document) CreateElement(name string) (*Element, error) {
if strings.ContainsRune(name, ':') {
return nil, fmt.Errorf("element name %q contains a colon: use CreateElementNS with a local name and Namespace parameter", name)
}
var e *Element
if d != nil {
e = d.allocElement()
} else {
e = newElement(name)
}
e.name = name
e.etype = ElementNode
e.doc = d
return e, nil
}
// CreateElementNS allocates a new Element with local name localname and active
// namespace ns, owned by this document, without attaching it to the tree. The
// local name must not contain a colon; supply the namespace through ns. This is
// the namespaced analogue of CreateElement (mirroring the SetAttribute /
// SetAttributeNS pairing). A nil ns leaves the element unqualified. A nil
// receiver allocates a standalone element with no owning document. When ns is
// slab-backed by a different document, the installing SetNamespace marks that source
// document escaped so its Free will not recycle the namespace slab.
func (d *Document) CreateElementNS(localname string, ns *Namespace) (*Element, error) {
e, err := d.CreateElement(localname)
if err != nil {
return nil, err
}
e.SetNamespace(ns)
return e, nil
}
func (d *Document) allocElement() *Element {
if len(d.elemSlab) == 0 {
d.elemSlab = growthChunk[Element](d.pooledSlabs, &d.elemGrowth)
}
if len(d.elemSlab) == 0 {
chunk := elemChunkPool.Get()
d.elemChunks = append(d.elemChunks, chunk)
d.elemSlab = chunk[:]
}
e := &d.elemSlab[0]
d.elemSlab = d.elemSlab[1:]
return e
}
// CreateText allocates a new Text node holding a copy of value, owned by this
// document, without attaching it to the tree. When d is non-nil both the node
// and its content bytes come from the document's slab allocators and must not
// outlive a call to Document.Free. A nil receiver allocates a standalone text
// node.
func (d *Document) CreateText(value []byte) *Text {
var e *Text
if d != nil {
e = d.allocText()
} else {
e = &Text{}
}
e.etype = TextNode
if d != nil {
e.content = d.allocTextContent(len(value))
copy(e.content, value)
} else {
e.content = make([]byte, len(value))
copy(e.content, value)
}
e.name = textNodeName
e.doc = d
return e
}
func (d *Document) allocTextContent(size int) []byte {
if size <= 0 {
return nil
}
if size > textContentSlabSize {
return make([]byte, size)
}
if len(d.textContentSlab) < size && !d.pooledSlabs && int(d.textContentGrowth) < len(textContentGrowthSizes) {
d.textContentSlab = make([]byte, max(textContentGrowthSizes[d.textContentGrowth], size))
d.textContentGrowth++
}
if len(d.textContentSlab) < size {
chunk := textContentChunkPool.Get()
d.textContentChunks = append(d.textContentChunks, chunk)
d.textContentSlab = chunk[:]
}
buf := d.textContentSlab[:size:size]
d.textContentSlab = d.textContentSlab[size:]
return buf
}
func (d *Document) growOwnedTextContent(cur []byte, extra int) []byte {
if extra <= 0 {
return cur
}
need := len(cur) + extra
if cap(cur) >= need {
return cur
}
newCap := max(cap(cur)*2, need, 64)
next := d.allocTextContent(newCap)
next = next[:len(cur)]
copy(next, cur)
return next
}
func (d *Document) allocText() *Text {
if len(d.textSlab) == 0 {
d.textSlab = growthChunk[Text](d.pooledSlabs, &d.textGrowth)
}
if len(d.textSlab) == 0 {
chunk := textChunkPool.Get()
d.textChunks = append(d.textChunks, chunk)
d.textSlab = chunk[:]
}
t := &d.textSlab[0]
d.textSlab = d.textSlab[1:]
return t
}
// CreateComment allocates a new Comment node holding value, owned by this
// document, without attaching it to the tree.
func (d *Document) CreateComment(value []byte) *Comment {
e := newComment(value)
e.doc = d
return e
}
// CreateCDATASection mirrors xmlNewCDataBlock in libxml2's tree.c.
func (d *Document) CreateCDATASection(value []byte) *CDATASection {
e := newCDATASection(value)
e.doc = d
return e
}
// CreateElementContent builds an ElementContent leaf node of the given type for
// use in a DTD content model: an element-reference leaf ([ElementContentElement],
// which requires a name) or a #PCDATA leaf ([ElementContentPCDATA], which
// requires an empty name). To build a sequence (,) or choice (|) node use
// [Document.CreateElementContentSeq] / [Document.CreateElementContentChoice],
// which attach and validate the two children; a bare sequence/choice leaf built
// here has nil children and cannot be serialized. The node defaults to the
// "once" occurrence; use [ElementContent.SetOccurrence] to change it.
func (d *Document) CreateElementContent(name string, etype ElementContentType) (*ElementContent, error) {
e, err := newElementContent(name, etype)
if err != nil {
return nil, err
}
return e, nil
}
// CreateElementContentSeq builds a sequence (,) content node with the two given
// children and occurrence indicator. Both children must be non-nil and
// structurally complete; otherwise it returns an error. This is the safe way to
// compose a multi-particle content model — the resulting node can be serialized
// and matched without a nil dereference.
func (d *Document) CreateElementContentSeq(c1, c2 *ElementContent, occur ElementContentOccur) (*ElementContent, error) {
return newElementContentBinary(ElementContentSeq, c1, c2, occur)
}
// CreateElementContentChoice builds a choice (|) content node with the two given
// children and occurrence indicator. Both children must be non-nil and
// structurally complete; otherwise it returns an error. This is the safe way to
// compose a multi-particle content model — the resulting node can be serialized
// and matched without a nil dereference.
func (d *Document) CreateElementContentChoice(c1, c2 *ElementContent, occur ElementContentOccur) (*ElementContent, error) {
return newElementContentBinary(ElementContentOr, c1, c2, occur)
}
// GetEntity looks up a general entity declaration by name, searching the
// internal subset first and then the external subset (the latter is skipped
// for standalone="yes" documents). found reports whether a declaration was
// located.
func (d *Document) GetEntity(name string) (ent *Entity, found bool) {
if ints := d.intSubset; ints != nil {
ent, found = ints.LookupEntity(name)
if found {
return
}
}
if d.standalone != StandaloneExplicitYes {
if exts := d.extSubset; exts != nil {
ent, found = exts.LookupEntity(name)
return
}
}
return
}
// GetParameterEntity looks up a parameter entity declaration by name,
// searching the internal subset first and then the external subset (the
// latter is skipped for standalone="yes" documents). The boolean result
// reports whether a declaration was located.
func (d *Document) GetParameterEntity(name string) (*Entity, bool) {
if ints := d.intSubset; ints != nil {
if ent, ok := ints.LookupParameterEntity(name); ok {
return ent, true
}
}
if d.standalone != StandaloneExplicitYes {
if exts := d.extSubset; exts != nil {
return exts.LookupParameterEntity(name)
}
}
return nil, false
}
// IsMixedElement reports whether the element named name is declared with mixed
// (or EMPTY/ANY) content. It returns ErrElementDeclNotFound (match with
// errors.Is) if no element declaration is found for name.
//
// It faithfully preserves the content-type classification of libxml2's
// xmlIsMixedElement: EMPTY and ANY are collapsed with MIXED into a single
// "mixed" true so validity-constraint callers get an error on, e.g.,
// "<empty> </empty>"; only an element-only content model returns false.
// It is therefore NOT a pure "is this a #PCDATA mixed model" predicate; callers
// needing the raw content-model type should inspect the element declaration
// directly (see elementDeclType, which whitespace classification uses instead).
//
// Mirroring libxml2's xmlIsMixedElement, lookup requires an internal subset
// (a document with none reports ErrElementDeclNotFound) and then searches the
// internal subset first, falling back to the external subset when the internal
// subset has no declaration for name.
func (d *Document) IsMixedElement(name string) (bool, error) {
if d.intSubset == nil {
return false, ErrElementDeclNotFound
}
edecl, ok := d.intSubset.GetElementDesc(name)
if !ok && d.extSubset != nil {
edecl, ok = d.extSubset.GetElementDesc(name)