# XM
Beautiful Elixir DSL for building XML documents, backed by Saxy for escaping and encoding.
```elixir
import XM
document do
urlset xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9" do
for page <- pages do
url do
loc site_url <> page.path
lastmod page.date
end
end
end
end
```
XM is intentionally tiny: local calls become XML elements, keyword arguments become attributes, and normal Elixir expressions still work.
## Features
- Nested XML elements with Elixir `do/end` syntax.
- Attributes via keyword lists or maps.
- Dynamic/namespaced tag names with `tag/2` and `qname/2`.
- Namespace declarations with `xmlns/1`, `xmlns/2`, and declarative `schema do ... end` metadata.
- Dotted namespace calls such as `image.image do ... end` for declared prefixes.
- Optional XSD validation through `XM.validate!/2` or compile-time global config.
- Idiomatic `%XM.Error{}` exceptions for invalid documents, names, attributes, text, or schema validation.
- `for`, `if`, `unless`, and `case` inside XML blocks.
- Explicit `text/1`, `comment/1`, and `cdata/1` nodes.
- Binary rendering with `render/2` and iodata rendering with `render_iodata/2`.
- Iodata-first pipelines with `tree do ... end |> render_iodata()`.
- Saxy-backed escaping and XML encoding.
## Installation
```elixir
def deps do
[
{:xm, "~> 0.1.0"}
]
end
```
## Examples
### Sitemap
```elixir
import XM
pages = [
%{path: "/", date: ~D[2026-06-25]},
%{path: "/about/", date: ~D[2026-06-25]}
]
xml =
document do
urlset xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9" do
for page <- pages do
url do
loc "https://example.com" <> page.path
lastmod page.date
end
end
end
end
```
### Atom entry with CDATA
```elixir
import XM
document do
entry do
title "Hello"
content type: "html" do
cdata "<p>Hello from XML</p>"
end
end
end
```
### Namespaces and schema declarations
`schema do ... end` is document metadata, not an XML element. XM injects namespace declarations into the document root and renders XSD locations as `xsi:schemaLocation`.
```elixir
import XM
xml =
document do
schema do
default "http://www.sitemaps.org/schemas/sitemap/0.9",
location: "priv/schemas/sitemap.xsd"
ns :image, "http://www.google.com/schemas/sitemap-image/1.1",
location: "priv/schemas/sitemap-image.xsd"
end
urlset do
url do
loc "https://example.com/"
image.image do
image.loc "https://example.com/image.jpg"
end
end
end
end
```
### Namespaced or dynamic tags
```elixir
import XM
tree do
tag qname(:media, :thumbnail), [xmlns(:media, "https://example.com/media"), url: "https://example.com/image.png"]
end
```
### Iodata rendering
`document do ... end` is the convenience API for producing a binary XML document.
For iodata, build nodes with `tree do ... end` and render explicitly:
```elixir
import XM
iodata =
tree do
feed do
title "Hello"
end
end
|> XM.render_iodata()
IO.iodata_to_binary(iodata)
```
This mirrors common Elixir conventions: keep binary and iodata rendering as separate functions instead of overloading a single `render/2` option.
### XSD validation
Use `XM.validate!/2` explicitly:
```elixir
XM.validate!(xml)
XM.validate!(xml, schema: "priv/schemas/sitemap.xsd")
XM.validate!(xml, schemas: ["priv/schemas/sitemap.xsd", "priv/schemas/sitemap-image.xsd"])
```
Without explicit `:schema`/`:schemas`, XM reads schema locations from the parsed root element's `xsi:schemaLocation` or `xsi:noNamespaceSchemaLocation` attributes.
To validate every `document do ... end`, enable XM's global compile-time configuration before modules using `document/2` are compiled:
```elixir
config :xm, validate: true
```
The option is captured when the `document do ... end` macro expands. It is intentionally global; there is no per-document `validate:` option. If validation is enabled and the document does not declare schema locations, XM raises `%XM.Error{reason: :missing_schema}`.
## License
MIT © 2026 Danila Poyarkov