package yqlib import ( "encoding/xml" "fmt" "io" "strings" yaml "gopkg.in/yaml.v3" ) var XMLPreferences = xmlPreferences{AttributePrefix: "+", ContentName: "+content", StrictMode: false, UseRawToken: false} type xmlEncoder struct { attributePrefix string contentName string indentString string directiveName string procInstPrefix string writer io.Writer } func NewXMLEncoder(indent int, attributePrefix string, contentName string) Encoder { var indentString = "" for index := 0; index < indent; index++ { indentString = indentString + " " } return &xmlEncoder{attributePrefix, contentName, indentString, "_directive_", "_procInst_", nil} } func (e *xmlEncoder) CanHandleAliases() bool { return false } func (e *xmlEncoder) PrintDocumentSeparator(writer io.Writer) error { return nil } func (e *xmlEncoder) PrintLeadingContent(writer io.Writer, content string) error { return nil } func (e *xmlEncoder) Encode(writer io.Writer, node *yaml.Node) error { encoder := xml.NewEncoder(writer) // hack so we can manually add newlines to procInst and directives e.writer = writer encoder.Indent("", e.indentString) switch node.Kind { case yaml.MappingNode: err := e.encodeTopLevelMap(encoder, node) if err != nil { return err } case yaml.DocumentNode: err := e.encodeComment(encoder, headAndLineComment(node)) if err != nil { return err } err = e.encodeTopLevelMap(encoder, unwrapDoc(node)) if err != nil { return err } err = e.encodeComment(encoder, footComment(node)) if err != nil { return err } case yaml.ScalarNode: var charData xml.CharData = []byte(node.Value) err := encoder.EncodeToken(charData) if err != nil { return err } return encoder.Flush() default: return fmt.Errorf("unsupported type %v", node.Tag) } var charData xml.CharData = []byte("\n") return encoder.EncodeToken(charData) } func (e *xmlEncoder) encodeTopLevelMap(encoder *xml.Encoder, node *yaml.Node) error { err := e.encodeComment(encoder, headAndLineComment(node)) if err != nil { return err } for i := 0; i < len(node.Content); i += 2 { key := node.Content[i] value := node.Content[i+1] start := xml.StartElement{Name: xml.Name{Local: key.Value}} log.Debugf("comments of key %v", key.Value) err := e.encodeComment(encoder, headAndLineComment(key)) if err != nil { return err } if strings.HasPrefix(key.Value, e.procInstPrefix) { name := strings.Replace(key.Value, e.procInstPrefix, "", 1) procInst := xml.ProcInst{Target: name, Inst: []byte(value.Value)} if err := encoder.EncodeToken(procInst); err != nil { return err } if _, err := e.writer.Write([]byte("\n")); err != nil { log.Warning("Unable to write newline, skipping: %w", err) } } else if key.Value == e.directiveName { var directive xml.Directive = []byte(value.Value) if err := encoder.EncodeToken(directive); err != nil { return err } if _, err := e.writer.Write([]byte("\n")); err != nil { log.Warning("Unable to write newline, skipping: %w", err) } } else { log.Debugf("recursing") err = e.doEncode(encoder, value, start) if err != nil { return err } } err = e.encodeComment(encoder, footComment(key)) if err != nil { return err } } return e.encodeComment(encoder, footComment(node)) } func (e *xmlEncoder) encodeStart(encoder *xml.Encoder, node *yaml.Node, start xml.StartElement) error { err := encoder.EncodeToken(start) if err != nil { return err } return e.encodeComment(encoder, headComment(node)) } func (e *xmlEncoder) encodeEnd(encoder *xml.Encoder, node *yaml.Node, start xml.StartElement) error { err := encoder.EncodeToken(start.End()) if err != nil { return err } return e.encodeComment(encoder, footComment(node)) } func (e *xmlEncoder) doEncode(encoder *xml.Encoder, node *yaml.Node, start xml.StartElement) error { switch node.Kind { case yaml.MappingNode: return e.encodeMap(encoder, node, start) case yaml.SequenceNode: return e.encodeArray(encoder, node, start) case yaml.ScalarNode: err := e.encodeStart(encoder, node, start) if err != nil { return err } var charData xml.CharData = []byte(node.Value) err = encoder.EncodeToken(charData) if err != nil { return err } if err = e.encodeComment(encoder, lineComment(node)); err != nil { return err } return e.encodeEnd(encoder, node, start) } return fmt.Errorf("unsupported type %v", node.Tag) } func (e *xmlEncoder) encodeComment(encoder *xml.Encoder, commentStr string) error { if commentStr != "" { log.Debugf("encoding comment %v", commentStr) if !strings.HasSuffix(commentStr, " ") { commentStr = commentStr + " " } var comment xml.Comment = []byte(commentStr) err := encoder.EncodeToken(comment) if err != nil { return err } } return nil } func (e *xmlEncoder) encodeArray(encoder *xml.Encoder, node *yaml.Node, start xml.StartElement) error { if err := e.encodeComment(encoder, headAndLineComment(node)); err != nil { return err } for i := 0; i < len(node.Content); i++ { value := node.Content[i] if err := e.doEncode(encoder, value, start.Copy()); err != nil { return err } } return e.encodeComment(encoder, footComment(node)) } func (e *xmlEncoder) encodeMap(encoder *xml.Encoder, node *yaml.Node, start xml.StartElement) error { log.Debug("its a map") //first find all the attributes and put them on the start token for i := 0; i < len(node.Content); i += 2 { key := node.Content[i] value := node.Content[i+1] if strings.HasPrefix(key.Value, e.attributePrefix) && key.Value != e.contentName { if value.Kind == yaml.ScalarNode { attributeName := strings.Replace(key.Value, e.attributePrefix, "", 1) start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: attributeName}, Value: value.Value}) } else { return fmt.Errorf("cannot use %v as attribute, only scalars are supported", value.Tag) } } } err := e.encodeStart(encoder, node, start) if err != nil { return err } //now we encode non attribute tokens for i := 0; i < len(node.Content); i += 2 { key := node.Content[i] value := node.Content[i+1] err := e.encodeComment(encoder, headAndLineComment(key)) if err != nil { return err } if strings.HasPrefix(key.Value, e.procInstPrefix) { name := strings.Replace(key.Value, e.procInstPrefix, "", 1) procInst := xml.ProcInst{Target: name, Inst: []byte(value.Value)} if err := encoder.EncodeToken(procInst); err != nil { return err } } else if key.Value == e.directiveName { var directive xml.Directive = []byte(value.Value) if err := encoder.EncodeToken(directive); err != nil { return err } } else if !strings.HasPrefix(key.Value, e.attributePrefix) && key.Value != e.contentName { start := xml.StartElement{Name: xml.Name{Local: key.Value}} err := e.doEncode(encoder, value, start) if err != nil { return err } } else if key.Value == e.contentName { // directly encode the contents err = e.encodeComment(encoder, headAndLineComment(value)) if err != nil { return err } var charData xml.CharData = []byte(value.Value) err = encoder.EncodeToken(charData) if err != nil { return err } err = e.encodeComment(encoder, footComment(value)) if err != nil { return err } } err = e.encodeComment(encoder, footComment(key)) if err != nil { return err } } return e.encodeEnd(encoder, node, start) }