use "raw"
use "files"
use "debug"
/*
* Unfortunately, we have to use direct FFI calls in this case instead
* of calls to LibXML2 et al, because we use addressof, and that is
* only valid in direct FFI calls.
*/
use @xmlMemGet[I32](
freeFunc: Pointer[XmlFreeFunc] tag,
mallocFunc: Pointer[Pointer[None]] tag, // Unused, so not defining
reallocFunc: Pointer[Pointer[None]] tag, // Unused, so not defining
strdupFunc: Pointer[Pointer[None]] tag) // Unused, so not defining
use @xmlDocDumpFormatMemoryEnc[None](
outdoc: NullablePointer[XmlDoc] tag,
doctxtptr: Pointer[Pointer[U8]] tag,
doctxtlen: Pointer[I32] tag,
txtencoding: Pointer[U8] tag,
format: I32)
type XmlFreeFunc is @{(Pointer[None] tag): None}
class Xml2Doc
"""
Wrapper around a libxml2 `xmlDoc` pointer, providing convenient parsing and
XPath evaluation helpers.
"""
let ptr': NullablePointer[XmlDoc]
new parseFile(auth: FileAuth, pfilename: String val) ? =>
"""
Parse an XML document from the given file path using libxml2.
- `auth`: Capability proving the caller has permission to read files.
- `pfilename`: Path to the XML file to parse.
On success, stores the underlying `xmlDoc*` in `ptr'` and its non-null
value in `ptr`. Raises an error if parsing fails or returns a null
document pointer.
"""
let ptrx: NullablePointer[XmlDoc] = LibXML2.xmlParseFile(pfilename)
if ptrx.is_none() then error end
ptr' = ptrx
new parseDoc(pcur: String val) ? =>
"""
Parse an XML document from an in-memory string using libxml2.
- `pcur`: String containing the complete XML document.
On success, stores the underlying `xmlDoc*` in `ptr'` and its non-null
value in `ptr`. Raises an error if parsing fails or returns a null
document pointer.
"""
let ptrx: NullablePointer[XmlDoc] = LibXML2.xmlParseDoc(pcur)
if ptrx.is_none() then error end
ptr' = ptrx
new create(version: String = "1.0") ? =>
"""
Create a new empty XML document with the specified version.
- `version`: XML version string (default: "1.0")
Creates an empty document with no root element. Use setRootElement()
or createElement() to build the document tree.
Example:
```pony
let doc = Xml2Doc.create()?
let root = doc.createElement("root")?
doc.setRootElement(root)?
```
"""
let ptrx: NullablePointer[XmlDoc] = LibXML2.xmlNewDoc(version)
if ptrx.is_none() then error end
ptr' = ptrx
new createWithRoot(root_name: String, version: String = "1.0") ? =>
"""
Create a new XML document with a root element.
- `root_name`: Name of the root element
- `version`: XML version string (default: "1.0")
Convenience constructor that creates a document and sets the root
element in one step.
Example:
```pony
let doc = Xml2Doc.createWithRoot("root")?
let root = doc.getRootElement()?
let child = root.addChild("item")?
```
"""
let ptrx: NullablePointer[XmlDoc] = LibXML2.xmlNewDoc(version)
if ptrx.is_none() then error end
ptr' = ptrx
let root_ptr = LibXML2.xmlNewDocNode(ptr', NullablePointer[XmlNs].none(),
root_name, "")
if root_ptr.is_none() then error end
LibXML2.xmlDocSetRootElement(ptr', root_ptr)
fun xpathEval(
xpath: String val,
namespaces: Array[(String val, String val)] = [])
: Xml2XPathResult
=>
"""
Evaluate an XPath expression against this document and return the result.
- `xpath`: The XPath expression to evaluate.
- `namespaces`: Optional list of `(prefix, uri)` pairs to register on a
temporary XPath context before evaluation, for namespace-aware queries.
Internally, creates a new `xmlXPathContext` for the document, registers
the provided namespaces, calls `xmlXPathEval`, then frees the context.
Returns an `Xml2XPathResult` wrapper around the resulting
`xmlXPathObject*`.
"""
let tmpctx: NullablePointer[XmlXPathContext] =
LibXML2.xmlXPathNewContext(ptr')
for (n, url) in namespaces.values() do
LibXML2.xmlXPathRegisterNs(tmpctx, n, url)
end
let xptr: NullablePointer[XmlXPathObject] =
LibXML2.xmlXPathEval(xpath, tmpctx)
let xpo: Xml2XPathResult = Xml2XPathObject(recover tag this end, xptr)
LibXML2.xmlXPathFreeContext(tmpctx)
xpo
fun xpathEvalNodes(
xpath: String val,
namespaces: Array[(String val, String val)] = [])
: Array[Xml2Node] ?
=>
"""
A convenience method that calls xpathEval and returns an Array[Xml2Node].
"""
(xpathEval(xpath, namespaces) as Array[Xml2Node])
fun xpathEvalString(
xpath: String val,
namespaces: Array[(String val, String val)] = [])
: String val ?
=>
"""
A convenience method that calls xpathEval and returns a String val.
"""
(xpathEval(xpath, namespaces) as String val)
fun xpathEvalF64(
xpath: String val,
namespaces: Array[(String val, String val)] = [])
: F64 ?
=>
"""
A convenience method that calls xpathEval and returns an F64 (XML's
default number type in libxml2).
"""
(xpathEval(xpath, namespaces) as F64)
fun xpathEvalBool(
xpath: String val,
namespaces: Array[(String val, String val)] = [])
: Bool ?
=>
"""
A convenience method that calls xpathEval and returns a Bool.
"""
(xpathEval(xpath, namespaces) as Bool)
fun ref getRootElement(): Xml2Node ? =>
"""
Return the root element node of this document as an `Xml2Node`.
Calls `xmlDocGetRootElement` on the underlying `xmlDoc*`. Raises an error
if the document has no root element or the returned pointer is null.
"""
let ptrx: NullablePointer[XmlNode] = LibXML2.xmlDocGetRootElement(ptr')
Xml2Node.fromPTR(recover tag this end, ptrx)?
fun ref createElement(name: String, content: String = ""): Xml2Node ? =>
"""
Create a new element node belonging to this document.
- `name`: Element name (tag name)
- `content`: Optional text content
Returns an Xml2Node wrapper. The node is created but not yet attached
to the document tree. Use setRootElement() or appendChild() to add it.
Example:
```pony
let doc = Xml2Doc.create()?
let elem = doc.createElement("item", "Hello")?
elem.setProp("id", "1")
```
"""
let node_ptr = LibXML2.xmlNewDocNode(ptr', NullablePointer[XmlNs].none(),
name, content)
if node_ptr.is_none() then error end
Xml2Node.fromPTR(recover tag this end, node_ptr)?
fun ref setRootElement(root: Xml2Node): Xml2Node ? =>
"""
Set the root element of this document.
- `root`: The node to set as root element
Returns the old root element if one existed, otherwise returns the new root.
Raises error if the operation fails.
Example:
```pony
let doc = Xml2Doc.create()?
let root = doc.createElement("root")?
doc.setRootElement(root)?
```
"""
let old_root = LibXML2.xmlDocSetRootElement(ptr', root.ptr')
if old_root.is_none() then
root // Return the new root if no previous root existed
else
Xml2Node.fromPTR(recover tag this end, old_root)?
end
fun ref createTextNode(content: String): Xml2Node ? =>
"""
Create a text node belonging to this document.
- `content`: Text content
Text nodes are typically added as children of element nodes to create
mixed content (text and elements combined).
Example:
```pony
let doc = Xml2Doc.createWithRoot("para")?
let para = doc.getRootElement()?
para.appendChild(doc.createTextNode("Some text "))?
let bold = doc.createElement("b", "bold")?
para.appendChild(bold)?
para.appendChild(doc.createTextNode(" more text"))?
```
"""
let node_ptr = LibXML2.xmlNewDocText(ptr', content)
if node_ptr.is_none() then error end
Xml2Node.fromPTR(recover tag this end, node_ptr)?
fun ref createComment(content: String): Xml2Node ? =>
"""
Create a comment node belonging to this document.
- `content`: Comment text (without <!-- --> delimiters)
Comment nodes can be added to the document tree using appendChild().
Example:
```pony
let doc = Xml2Doc.createWithRoot("root")?
let root = doc.getRootElement()?
root.appendChild(doc.createComment("This is a comment"))?
```
"""
let node_ptr = LibXML2.xmlNewDocComment(ptr', content)
if node_ptr.is_none() then error end
Xml2Node.fromPTR(recover tag this end, node_ptr)?
fun serialize(
format: Bool = true,
encoding: String = "UTF-8")
: String ?
=>
"""
Serialize this document to a String with optional formatting.
- `format`: If true, enables pretty-printing (indentation, newlines).
If false, produces compact output.
- `encoding`: Character encoding for the output (default: "UTF-8").
Common values: "UTF-8", "ISO-8859-1", "UTF-16".
Returns the serialized XML as a String val. Raises an error if
serialization fails or returns null memory.
Example:
```pony
let doc = Xml2Doc.parseDoc("<root><child>text</child></root>")?
let xml_string = doc.serialize()? // Pretty-printed UTF-8
let compact = doc.serialize(false)? // Compact output
```
"""
// Allocate a pony variable to hold our Pointer[U8]
// Allocate a pony variable to hold the size
var c_str: Pointer[U8] ref = Pointer[U8]
var size: I32 = 0
// The function to free is available as a function pointer from
// the xmlMemGet function. We allocate a variable for it and
// pass the address of that to the function:
var freeFunc: XmlFreeFunc = @{(p: Pointer[None] tag) => None}
var mallocFunc: Pointer[None] = Pointer[None]
var reallocFunc: Pointer[None] = Pointer[None]
var strdupFunc: Pointer[None] = Pointer[None]
var rc: I32 = @xmlMemGet(addressof freeFunc, addressof mallocFunc, addressof reallocFunc, addressof strdupFunc)
// Call xmlDocDumpFormatMemoryEnc
// format parameter: 1 for formatted, 0 for compact
let format_val: I32 = if format then I32(1) else I32(0) end
@xmlDocDumpFormatMemoryEnc(
ptr', // our xmlDoc pointer
addressof c_str, // output: pointer to allocated memory
addressof size, // output: size of allocated memory
encoding.cstring(), // encoding string
format_val) // format flag
// Check if memory was allocated
if c_str.is_null() then error end
// Convert to Pony String (String.from_cstring makes a copy)
let result: String iso = String.from_cpointer(c_str, size.usize()).clone()
// FREE THE MEMORY (critical!)
// Call the function pointer we retrieved earlier.
freeFunc(c_str)
// Return the cloned string
consume result
fun saveToFile(
auth: FileAuth,
filename: String,
format: Bool = true,
encoding: String = "UTF-8")
: None ?
=>
"""
Save this document to a file with optional formatting and encoding.
- `auth`: Capability proving the caller has permission to write files.
- `filename`: Path to the file where the document should be saved.
- `format`: If true, enables pretty-printing (indentation, newlines).
If false, produces compact output.
- `encoding`: Character encoding for the output (default: "UTF-8").
Common values: "UTF-8", "ISO-8859-1", "UTF-16".
Returns None on success. Raises an error if the file cannot be written
or if libxml2 returns an error code (negative return value).
Example:
```pony
let doc = Xml2Doc.parseDoc("<root><child>text</child></root>")?
doc.saveToFile(auth, "output.xml")? // Pretty-printed UTF-8
doc.saveToFile(auth, "compact.xml", false, "ISO-8859-1")?
```
"""
// Call the C function
// Returns number of bytes written, or -1 on error
let format_val: I32 = if format then I32(1) else I32(0) end
let bytes_written: I32 = LibXML2.xmlSaveFormatFileEnc(
filename,
ptr',
encoding,
format_val)
// Check for error (negative return indicates failure)
if bytes_written < 0 then error end
fun _final() =>
LibXML2.xmlFreeDoc(ptr')