Encodes Typst values to XML. This package also provides built-in support for generating Atom feeds.
Encoding XML
You should structure your Typst values as follows:
#import "@preview/exemel:0.1.0": to-xml
// An XML node is either:
// - a dictionary describing an element, or
// - a plain string, which becomes a text node
#let value = (
tag: "bookstore",
// `namespace` is optional and defaults to `none`
namespace: none,
// `attrs` is optional and defaults to an empty dictionary
attrs: (:),
// `children` is optional and defaults to no children
children: (
(
tag: "book",
attrs: (id: "bk101", category: "programming"),
children: (
(tag: "title", attrs: (lang: "en"), children: ("XML Developer's Guide",)),
(tag: "author", children: ("Gambardella, Matthew",)),
(tag: "year", children: ("2000",)),
(tag: "price", children: ("44.95",)),
),
),
),
)
#to-xml(value, pretty: true)
Notes on the shape:
tagis required on every element;attrsandchildrenmay be omitted and default to empty.attrsis a dictionary of string keys to string values.childrenis an array whose entries are either nested element dictionaries or plain strings (text content). An element can’t currently have both mixed markup and be namespace-prefixed. Typst’s ownxml()reader doesn’t preserve namespace prefixes either, so round-tripping a parsed document throughxml()and back throughto-xmlwill lose anyfoo:bar-style prefixes.- Passing
pretty: trueindents the output with two spaces per level. the default,pretty: false, produces compact output with no added whitespace. to-xmlalso accepts an array of top-level values, in which case it returns one XML string per value.
Generating Atom Feed
#import "@preview/exemel:0.1.0": atom-encode, atom-post, atom-author, atom-site-id
#let me = atom-author(
"Hachimi",
email: "Namerudo@example.com",
uri: "https://example.com/ashiga",
)
#let posts = (
atom-post(
"Hayaku",
link: "https://example.com/blog/hayaku-naro",
id: atom-site-id(
authority: "example.com",
time: datetime(year: 2026, month: 7, day: 1),
identifier: "blog/typst-xml-plugin",
),
updated: datetime(year: 2026, month: 7, day: 3, hour: 14, minute: 30, second: 0),
published: datetime(year: 2026, month: 7, day: 1, hour: 9, minute: 0, second: 0),
summary: "Hachimi hachimi hachimi hachimi hachimi hachimi o namerudo",
content: "Ashiga ashiga ashiga ashiga ashiga hayaku naro",
authors: (me,),
categories: ("umamusume", "clannad", "baka futari"),
),
)
#atom-encode(
title: "Honey Drink",
subtitle: "Hachimi, hachimi, and hachimi",
site-link: "https://example.com/",
id: atom-site-id(
authority: "example.com",
time: datetime(year: 2026, month: 1, day: 1),
),
updated: datetime(year: 2026, month: 7, day: 3, hour: 14, minute: 30, second: 0),
authors: (me,),
posts: posts,
)
atom-encode builds the <feed> root, and accepts:
title(required),subtitle(optional)site-link(required): the human-readable site URLfeed-link: the URL of the feed itself; defaults tosite-link + "feed.xml"id(required): a stable, unique feed identifier;atom-site-idis a helper for building spec-complianttag:URIs (RFC 4151) from a domain you controlupdated(requireddatetime): when the feed was last modifiedauthors: an array built fromatom-author(...)callsposts: an array built fromatom-post(...)calls
atom-post builds a single <entry>, and accepts:
- the entry title (positional, required)
link(required): URL of the human-readable postid(required): unique entry identifier, same convention as the feedidupdated(requireddatetime),published(optionaldatetime)summary,content: both optional;content-type(default"html") is written as thecontentelement’stypeattributeauthors: optional per-entry authors; if omitted, readers fall back to the feed-level authors per the Atom speccategories: an array of plain category strings, each rendered as<category term="...">
atom-author builds an <author> element from a name plus optional email and uri, and is used for both the feed-level and per-entry authors arrays.