Merge epic/34-save-parity: XML declaration save-option parity
This commit is contained in:
commit
8027010e9e
@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- Serialization option parity for the XML declaration (the libxml2 save-option
|
||||
model). `PureXML.Emitting.Options` gains `includeXMLDeclaration`, `xmlVersion`,
|
||||
`encodingName`, and `standalone`; when requested, both the tree serializer and
|
||||
the incremental writer prepend an `<?xml ...?>` declaration from a single shared
|
||||
computation. `Writer.writeStartDocument()` emits it. Defaults stay off so
|
||||
fragment output is unchanged.
|
||||
- Spec-exact XML character classification (`PureXML.Parsing.XMLCharacter`, the
|
||||
libxml2 `chvalid.h` parity): the XML 1.0 (Fifth Edition) `Char`,
|
||||
`NameStartChar`, and `NameChar` productions as public predicates over Unicode
|
||||
|
||||
@ -64,7 +64,7 @@ flowchart TB
|
||||
TreeApi["#31 Tree manipulation API"]:::epic
|
||||
Writer["#32 xmlTextWriter"]:::done
|
||||
Reader["#33 xmlTextReader with validation"]:::epic
|
||||
Save["#34 Serialization option parity"]:::epic
|
||||
Save["#34 Serialization option parity"]:::done
|
||||
Chars["#35 Character and name validity"]:::done
|
||||
Core --> Push
|
||||
Core --> Sax
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
public extension PureXML.Emitting {
|
||||
/// Explicit emitter options. The default is pretty-printed output with
|
||||
/// two-space indentation and a self-closing tag for empty elements.
|
||||
/// two-space indentation and a self-closing tag for empty elements, and no
|
||||
/// XML declaration.
|
||||
struct Options: Equatable, Hashable, Sendable {
|
||||
/// Whether to indent nested elements onto their own lines.
|
||||
public var prettyPrint: Bool
|
||||
@ -8,15 +9,32 @@ public extension PureXML.Emitting {
|
||||
public var indent: String
|
||||
/// Whether childless elements collapse to `<name/>`.
|
||||
public var selfCloseEmptyElements: Bool
|
||||
/// Whether to emit an `<?xml ...?>` declaration (libxml2 emits one by
|
||||
/// default when saving a document; PureXML defaults to off for fragments).
|
||||
public var includeXMLDeclaration: Bool
|
||||
/// The version written in the declaration.
|
||||
public var xmlVersion: String
|
||||
/// The encoding name written in the declaration, or nil to omit it.
|
||||
public var encodingName: String?
|
||||
/// The `standalone` flag written in the declaration, or nil to omit it.
|
||||
public var standalone: Bool?
|
||||
|
||||
public init(
|
||||
prettyPrint: Bool = true,
|
||||
indent: String = " ",
|
||||
selfCloseEmptyElements: Bool = true,
|
||||
includeXMLDeclaration: Bool = false,
|
||||
xmlVersion: String = "1.0",
|
||||
encodingName: String? = "UTF-8",
|
||||
standalone: Bool? = nil,
|
||||
) {
|
||||
self.prettyPrint = prettyPrint
|
||||
self.indent = indent
|
||||
self.selfCloseEmptyElements = selfCloseEmptyElements
|
||||
self.includeXMLDeclaration = includeXMLDeclaration
|
||||
self.xmlVersion = xmlVersion
|
||||
self.encodingName = encodingName
|
||||
self.standalone = standalone
|
||||
}
|
||||
|
||||
/// Pretty-printed output with two-space indentation.
|
||||
@ -24,5 +42,18 @@ public extension PureXML.Emitting {
|
||||
|
||||
/// Single-line output with no inserted whitespace.
|
||||
public static let compact = Options(prettyPrint: false)
|
||||
|
||||
/// The `<?xml ...?>` declaration string, or nil when not requested.
|
||||
var xmlDeclaration: String? {
|
||||
guard includeXMLDeclaration else { return nil }
|
||||
var declaration = "<?xml version=\"\(xmlVersion)\""
|
||||
if let encodingName {
|
||||
declaration += " encoding=\"\(encodingName)\""
|
||||
}
|
||||
if let standalone {
|
||||
declaration += " standalone=\"\(standalone ? "yes" : "no")\""
|
||||
}
|
||||
return declaration + "?>"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,6 +31,9 @@ public extension PureXML.Emitting {
|
||||
/// Serializes a node into an XML string.
|
||||
public func serialize(_ node: PureXML.Model.Node) -> String {
|
||||
var output = ""
|
||||
if let declaration = options.xmlDeclaration {
|
||||
output += declaration + "\n"
|
||||
}
|
||||
var stack: [SerializeStep] = [.node(node, 0, options.prettyPrint)]
|
||||
while let step = stack.popLast() {
|
||||
switch step {
|
||||
|
||||
@ -29,6 +29,14 @@ public extension PureXML.Emitting {
|
||||
self.options = options
|
||||
}
|
||||
|
||||
/// Emits the XML declaration if the options request one. Call before the
|
||||
/// root element; a no-op when `includeXMLDeclaration` is off.
|
||||
public mutating func writeStartDocument() {
|
||||
if let declaration = options.xmlDeclaration {
|
||||
output += declaration + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens an element. Attributes may follow until any content or close.
|
||||
public mutating func writeStartElement(_ name: String) {
|
||||
closeStartTag()
|
||||
|
||||
38
Tests/SaveOptionsTests.swift
Normal file
38
Tests/SaveOptionsTests.swift
Normal file
@ -0,0 +1,38 @@
|
||||
@testable import PureXML
|
||||
import Testing
|
||||
|
||||
@Suite("Save options")
|
||||
struct SaveOptionsTests {
|
||||
@Test("The serializer emits the XML declaration when requested")
|
||||
func test_declaration() {
|
||||
let options = PureXML.Emitting.Options(prettyPrint: false, includeXMLDeclaration: true)
|
||||
let xml = PureXML.serialize(.element(.init("r")), options: options)
|
||||
#expect(xml == "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<r/>")
|
||||
}
|
||||
|
||||
@Test("No declaration is emitted by default")
|
||||
func test_noDeclarationByDefault() {
|
||||
#expect(PureXML.serialize(.element(.init("r")), options: .compact) == "<r/>")
|
||||
}
|
||||
|
||||
@Test("Standalone and an omitted encoding are honored")
|
||||
func test_declarationVariants() {
|
||||
let options = PureXML.Emitting.Options(
|
||||
prettyPrint: false,
|
||||
includeXMLDeclaration: true,
|
||||
encodingName: nil,
|
||||
standalone: true,
|
||||
)
|
||||
let xml = PureXML.serialize(.element(.init("r")), options: options)
|
||||
#expect(xml == "<?xml version=\"1.0\" standalone=\"yes\"?>\n<r/>")
|
||||
}
|
||||
|
||||
@Test("The writer emits the declaration via writeStartDocument")
|
||||
func test_writerDeclaration() {
|
||||
var writer = PureXML.Emitting.Writer(options: .init(prettyPrint: false, includeXMLDeclaration: true))
|
||||
writer.writeStartDocument()
|
||||
writer.writeStartElement("r")
|
||||
writer.writeEndElement()
|
||||
#expect(writer.output == "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<r/>")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user