1
0
mirror of https://github.com/imgproxy/imgproxy.git synced 2025-12-23 22:11:10 +02:00
Files
imgproxy/processing/svg/parser/node.go
2025-10-15 17:28:13 +03:00

254 lines
5.1 KiB
Go

package svgparser
import (
"bufio"
"encoding/xml"
"errors"
"fmt"
"io"
"golang.org/x/net/html/charset"
)
type Attr = xml.Attr
type Node struct {
Parent *Node
Name xml.Name
Attrs []Attr
Children []any
}
func (n *Node) readFrom(r io.ReadSeeker) error {
if n.Parent != nil {
return errors.New("cannot read child node")
}
dec := xml.NewDecoder(r)
dec.Strict = false
dec.CharsetReader = charset.NewReaderLabel
curNode := n
for {
// Save the current position to know where to read raw CData from.
pos := dec.InputOffset()
// Read raw token so decoder doesn't mess with attributes and namespaces.
tok, err := dec.RawToken()
if err == io.EOF {
break
}
if err != nil {
return err
}
switch t := tok.(type) {
case xml.StartElement:
// An element is opened, create a node for it
el := &Node{
Parent: curNode,
Name: t.Name,
Attrs: t.Attr,
}
// Append the node to the current node's children and make it current
curNode.Children = append(curNode.Children, el)
curNode = el
case xml.EndElement:
// If the current node has no parent, then we are at the root,
// which can't be closed.
if curNode.Parent == nil {
return fmt.Errorf(
"malformed XML: unexpected closing tag </%s> while no elements are opened",
fullName(t.Name),
)
}
// Closing tag name should match opened node name (which is current)
if curNode.Name.Local != t.Name.Local || curNode.Name.Space != t.Name.Space {
return fmt.Errorf(
"malformed XML: unexpected closing tag </%s> for opened <%s> element",
fullName(t.Name),
fullName(curNode.Name),
)
}
// The node is closed, return to its parent
curNode = curNode.Parent
case xml.CharData:
// We want CData as is, so read it raw
cdata, err := readRawCData(r, pos, dec.InputOffset()-pos)
if err != nil {
return err
}
curNode.Children = append(curNode.Children, cdata)
case xml.Directive:
curNode.Children = append(curNode.Children, t.Copy())
case xml.Comment:
curNode.Children = append(curNode.Children, t.Copy())
case xml.ProcInst:
curNode.Children = append(curNode.Children, t.Copy())
}
}
return nil
}
func (n *Node) writeTo(w *bufio.Writer) error {
if err := w.WriteByte('<'); err != nil {
return err
}
if err := writeFullName(w, n.Name); err != nil {
return err
}
n.writeAttrsTo(w)
if len(n.Children) == 0 {
if _, err := w.WriteString("/>"); err != nil {
return err
}
return nil
}
if err := w.WriteByte('>'); err != nil {
return err
}
if err := n.writeChildrenTo(w); err != nil {
return err
}
if _, err := w.WriteString("</"); err != nil {
return err
}
if err := writeFullName(w, n.Name); err != nil {
return err
}
if err := w.WriteByte('>'); err != nil {
return err
}
return nil
}
func (n *Node) writeAttrsTo(w *bufio.Writer) error {
for _, attr := range n.Attrs {
if err := w.WriteByte(' '); err != nil {
return err
}
if err := writeFullName(w, attr.Name); err != nil {
return err
}
if _, err := w.WriteString(`="`); err != nil {
return err
}
if len(attr.Value) > 2 && attr.Value[0] == '&' && attr.Value[len(attr.Value)-1] == ';' {
// Attribute value is an entity, write it as is
if _, err := w.WriteString(attr.Value); err != nil {
return err
}
} else {
// Escape the attribute value
if err := escapeString(w, attr.Value); err != nil {
return err
}
}
if err := w.WriteByte('"'); err != nil {
return err
}
}
return nil
}
func (n *Node) writeChildrenTo(w *bufio.Writer) error {
for _, child := range n.Children {
switch c := child.(type) {
case *Node:
if err := c.writeTo(w); err != nil {
return err
}
case CData:
if _, err := w.Write([]byte(c)); err != nil {
return err
}
case Comment:
if _, err := w.WriteString("<!--"); err != nil {
return err
}
if _, err := w.Write([]byte(c)); err != nil {
return err
}
if _, err := w.WriteString("-->"); err != nil {
return err
}
case Directive:
if _, err := w.WriteString("<!"); err != nil {
return err
}
if _, err := w.Write([]byte(c)); err != nil {
return err
}
if err := w.WriteByte('>'); err != nil {
return err
}
case ProcInst:
if _, err := w.WriteString("<?"); err != nil {
return err
}
if _, err := w.WriteString(c.Target); err != nil {
return err
}
if len(c.Inst) > 0 {
if err := w.WriteByte(' '); err != nil {
return err
}
if _, err := w.Write([]byte(c.Inst)); err != nil {
return err
}
}
if _, err := w.WriteString("?>"); err != nil {
return err
}
default:
return fmt.Errorf("unknown child type: %T", c)
}
}
return nil
}
func fullName(name xml.Name) string {
if len(name.Space) == 0 {
return name.Local
}
return name.Space + ":" + name.Local
}
func writeFullName(w *bufio.Writer, name xml.Name) error {
if len(name.Space) > 0 {
if _, err := w.WriteString(name.Space); err != nil {
return err
}
if err := w.WriteByte(':'); err != nil {
return err
}
}
if _, err := w.WriteString(name.Local); err != nil {
return err
}
return nil
}