--- title: "Zipdown, specification version 0.5" zd-date: 2026-11-11T14:35:39.701128000Z zd-version: 1 zd-markdown-length-octets: 60632 zd-markdown-sha256: 3b641fcf6d6512c8bcd802e07bda5cd4e1807a4859566ccd3adcaafac890ec59 zd-boundary: 59cc43e990fd8cd65250cabf85010f72c265ea44f243ec42-3adcaafac890ec59 --- # Zipdown, specification version 0.5 ```text Independent Submission (TBD) Request for Comments: XXXX November 2026 Category: Experimental Zipdown: Markdown with Embedded Assets (specification version 0.5) ``` ## Status of This Memo Version 0.5 of this specification, written November 11th, 2026. It is not yet stable. The specification and the file format are numbered separately, and files carry only the format's number. Every version of this specification before 2 describes format version 1, so files say `zd-version: 1` until version 2 of the specification is published. Until then the format may still evolve: a file written to one version of this specification may not read the same under a later one. Anyone may implement this specification, in any software, free or paid, without asking and without paying. This is the site version, jokes included; the version submitted as an Internet-Draft removes the Acknowledgments jokes. Comments are, as tradition demands, requested: write to what this document hopes to become, plural, at treeflow.ai. This document is itself a Zipdown file. Its diagrams travel inside it as assets, and so does the worked example from Section 24, so a reader can extract `assets/example.zd` and check it byte for byte against the text. The ASCII diagrams in the text remain the reference; the images are illustrations. ## Abstract Zipdown is a single-file text format that carries a Markdown document together with the assets it needs. The header is valid frontmatter, the body is ordinary Markdown, and the assets follow as base64 blocks inside an HTML comment, one block per asset, with a visible index and a short two-language note at the end. A plain CommonMark renderer shows the document, one visible marker line, the index, and the note. Every plain Markdown file is a valid Zipdown file with zero assets, and reads as Zipdown Markdown unless it says otherwise. ## Table of Contents 1. Conventions and Terminology 2. Format Overview 3. Character Encoding and Line Endings 4. Header 5. Files With No Assets 6. Body 7. Zipdown Markdown (zd1) 8. Separator 9. Boundary Marker 10. Delimiters and the Comment 11. Payload 12. Index 13. Epilogue 14. Pointer 15. Terminator 16. Signatures 17. Reading a File 18. Appearance in Plain Renderers 19. Extracting an Asset by Hand 20. Security Considerations 21. Media Type, Extension, and Site 22. Implementations 23. Future Work 24. Example 25. Open Issues 26. References Appendix A. Notes for Viewers and Editors ## 1. Conventions and Terminology The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here. - **Octet**: an 8-bit byte. All lengths are in octets. - **Header**: the frontmatter block at the top of the file, fences included. - **Header end**: the position just after the LF of the header's closing fence line. - **Body**: everything from the header end to the separator. - **Separator**: the fixed end-of-document block between the body and the opening delimiter line. - **Preamble**: the header plus the body. - **Marker**: the 65-character string declared by `zd-boundary`. - **Asset**: one file carried in the payload. - **Asset block**: one asset's base64 text, in 64-character lines. - **Payload**: the asset blocks and the blank lines between them. - **Index**: the `zipdown-index` code block. - **Epilogue**: the two mandatory note lines. - **Pointer**: the line just before the terminator. - **Terminator**: the last line of the file, a single `.`. - **MLO**: the value of `zd-markdown-length-octets`. - **Reader**: code that parses and verifies a file and hands its document to something else, such as an agent, a tool, or a viewer. - **Viewer**: code that shows a file to a person. This memo defines the bytes and what they mean. What readers, servers, and editors do with a file, including files that break the rules, is their business, except where Section 20 says otherwise. ## 2. Format Overview ```text +-------------------------------------------------------+ | --- | header | zd-version: 1 | (valid frontmatter) | zd-markdown-length-octets: 4821 | | zd-markdown-sha256: <64 hex> | | zd-boundary: <65-char marker> | | --- | +-------------------------------------------------------+ <- header end | # Title | body | ... ![dot](assets/dot.png){width=10%} ... | (MLO octets) | | ends with a blank line +-------------------------------------------------------+ | *** | separator (visible) | -----END OF DOCUMENT----- | | *** | +-------------------------------------------------------+ | -- | opening delimiter (visible) |
| comment closes | ```zipdown-index | index (visible) | path size lines(1-based,inclusive) base64-sha256 sha256 | assets/dot.png 70 14-15 <64 hex> <64 hex> | | assets/chart.svg 812 17-30 <64 hex> <64 hex> | | ``` | | Psst, this is a Zipdown file! ... \ | epilogue (2 lines) | (the same, in Chinese) | | pointer (hidden) | . | terminator (visible) +-------------------------------------------------------+ ``` ![Format overview](assets/format-overview.svg) ## 3. Character Encoding and Line Endings - The whole file MUST be UTF-8 [RFC3629]. - Lines end with LF. - Writers SHOULD NOT emit CR (0x0D) anywhere. - If a file contains a CR anywhere, MLO, every hash, the index line ranges, and the pointer carry no guarantees. Whether to read such a file anyway is the reader's and the viewer's business, but a viewer MUST NOT show it as verified or signed. ## 4. Header ### 4.1. Must Be Valid Frontmatter The header MUST be valid frontmatter in exactly one of these forms: - **YAML** between lines consisting solely of `---`. - **TOML** between lines consisting solely of `+++`. - **JSON** in the style of Hugo: a line consisting solely of `{` opens it, and the first later line consisting solely of `}` closes it. Nested braces MUST be indented. The block is one JSON object. Readers MUST parse the header as plain data. In particular, a YAML reader MUST NOT honor tags that construct objects or run code; a "safe load" that yields only strings, numbers, booleans, lists, and maps is what is meant. ### 4.2. Zipdown Keys | Key | Type | Meaning | |---|---|---| | `zd-version` | integer | REQUIRED. Its presence marks the file as Zipdown. This memo defines version 1. | | `zd-markdown-length-octets` | integer | OPTIONAL hint. Length of the body in octets. | | `zd-markdown-sha256` | string | REQUIRED. SHA-256 of the body, 64 lowercase hex digits. | | `zd-boundary` | string | REQUIRED. The bare 65-character marker. | | `zd-payload-encoding` | string | OPTIONAL. Default `base64`. In version 1 the only legal value is `base64`. `binary` is reserved. | | `zd-markdown-flavor` | string | OPTIONAL. The body's Markdown: `zd1`, or an identifier from the IANA Markdown Variants registry. Default: `zd1` (Section 6.2). | | `zd-signature` | string | OPTIONAL. An Ed25519 signature over the header, the index, and the epilogue (Section 16). | | `zd-date` | string | OPTIONAL. When the file was written, in ISO 8601 with nanoseconds, such as `2026-11-11T13:04:00.123456789-08:00`. Conforming writers include the time zone; a date without one is local time. | (The table above is a GFM table. In a plain CommonMark renderer it shows as rows of text separated by vertical bars, which is still readable.) ### 4.3. Other Keys - The `zd-` prefix is reserved. A `zd-` key not defined here is invalid. It is ignored, and the rest of the header is read normally. (So `zd-markdown-flavour` is ignored, and the flavor falls back to `zd1`.) - A header in which any `zd-` key appears more than once is invalid. Parsers disagree about which duplicate wins, so two readers could otherwise see different markers or hashes in the same file. - Keys without the `zd-` prefix, such as `title` or `tags`, are allowed and are not Zipdown's business. - Key order carries no meaning. - This memo says nothing about files whose `zd-version` it does not define. ### 4.4. MLO MLO is the number of octets in the body, from the header end to the first octet of the separator: ```text offset of separator = header end + MLO offset of opening delimiter line = header end + MLO + 34 ``` 34 is the separator's fixed length in octets (Section 8). - If MLO is present, readers MUST confirm that the separator starts exactly at header end + MLO and that the opening delimiter line follows it. If either is not there, the file is invalid. - If MLO is missing, or zero or less, the reader finds the opening delimiter line by scanning, and the body ends 34 octets before it. - Either way, readers MUST confirm that the marker does not occur in the body. ### 4.5. The zd- Line Grammar Libraries for YAML, TOML, and JSON disagree in ways that matter: escaped key spellings, duplicate keys, and YAML 1.1 reading `0153` as an octal number. So every `zd-` entry has exactly one spelling in each header form, and readers find `zd-` entries by that spelling, never through a library. ```text YAML zd-: TOML zd- = "" or zd- = JSON "zd-": "" or "zd-": ``` - Each `zd-` entry sits on a line of its own. In JSON the line may be indented with spaces and ends with a comma where JSON needs one. - `zd-` entries sit at the top level only. In TOML they come before the first table header (a line starting with `[`); in JSON they are members of the outermost object, never of a nested one. A `zd-` entry anywhere else makes the file invalid. - `` is lowercase ASCII letters, digits, and hyphens. - `` is printable ASCII (0x20 to 0x7E) other than `"` and `\`, with no leading or trailing space. `` is decimal, with no leading zeros. - To catch disguised spellings, readers rename every key the grammar matched (for example, `zd-boundary` to `x-zd-boundary`) and parse the header with a library. If the library still finds any `zd-` key, the file is invalid. That catches escaped spellings, TOML dotted keys, and JSON `\u` escapes, including a disguised second copy of a key that is also spelled plainly. - YAML headers MUST NOT use anchors, aliases, tags, merge keys, or a second document. - The header is at most 64 KiB, has no byte order mark, and its fence lines are exactly `---`, `+++`, or `{` and `}`, with nothing else on them. - Keys without the `zd-` prefix are free-form, as before. ## 5. Files With No Assets A file with no assets needs no header, no separator, no delimiters, no comment, no index, no epilogue, no pointer, and no terminator. It is plain Markdown. It MAY be named `.md` or `.zd`. Either way, its body is `zd1` (Section 6.2). If it starts with an ordinary frontmatter block (one with no `zd-version`) that holds a `zd-markdown-flavor` line, that flavor applies instead. A frontmatter block is never part of the body, so viewers do not show it as text. Everything below applies to files with one or more assets. ## 6. Body ### 6.1. Rules - The body MUST NOT contain the marker. - The body MUST end with an empty line, so its last two octets are LF LF. - `zd-markdown-sha256` covers exactly the body's octets. The header is never hashed. ### 6.2. Which Markdown `zd-markdown-flavor` names the body's Markdown. A file does not have to use Zipdown's own Markdown to use the Zipdown container. - **`zd1`**, the default, is Zipdown Markdown, version 1: CommonMark 0.31.2 [CommonMark] plus the extensions in Section 7. In practice that is GFM [GFM] without raw HTML, plus Zipdown's own marks. It targets modern Markdown, the kind people type in messaging apps, and does not pin to the GFM spec, which has been at version 0.29 since 2019. Later versions will be `zd2`, `zd3`, and so on. - **The default applies to every file without a flavor,** whatever its extension. Viewers do not guess: a `.md` file with no header is `zd1`, the same as a `.zd` file. Plain GFM bodies read the same either way, apart from raw HTML (Section 7.12). - **Any other value** is an identifier from the IANA Markdown Variants registry, such as `GFM` or `CommonMark`. The body follows that flavor's rules, and nothing in Section 7 applies. - A renderer that does not know the named flavor renders the body as CommonMark. ## 7. Zipdown Markdown (zd1) This section applies only when the flavor is `zd1`. Renderers decide how everything here looks: the actual sizes, colors, grays, and indent widths. A renderer that does not support a feature shows its text as written. Nothing in this section applies inside code spans, code blocks, link destinations, or autolinks. ### 7.1. Inline Marks and the Edge Rule Size, highlight, and spoilers wrap text in a pair of marks. A mark counts only at an edge: - **Opening mark:** the character before it is whitespace, a punctuation character, or the start of a line, and the character after it is not whitespace. - **Closing mark:** the character before it is not whitespace, and the character after it is whitespace, a punctuation character, or the end of a line. - **Character** means a Unicode code point. **Punctuation character** means, as in CommonMark, any character in the Unicode general categories P or S, as of Unicode 16.0. The marks themselves count, so marks can sit against each other. - **Exact runs:** each mark is an exact run of characters: `+`, `++`, `-`, `--`, `—`, `==`, or `||`. A closer pairs only with an opener of the same run. A longer run, such as `---` or `+++`, is plain text. - **Escape:** a backslash before a mark makes it plain text, for example `\--` at the start of a line. - **Nesting:** marks nest, as in `++**very big and bold**++`. Nesting deeper than 16 levels is drawn as level 16. - **Pairing:** marks pair the way CommonMark pairs emphasis. A closing mark pairs with the nearest earlier opening mark of the same kind that is still unpaired. Marks left unpaired, or opened inside a pair that closes around them, are plain text. So `-a ++b- c++` reads as a small `a ++b` followed by a plain ` c++`. Ordinary text stays plain, because its dashes and pluses sit inside words or next to spaces: ```text self-evident 10-20 1+1+1 C++ y-axis this — that a == b ``` The rule is not perfect. `++i and C++` reads as XL text, because both ends sit at an edge. A backslash fixes it: `\++i and C++`. ### 7.2. Text Size Five steps. M is normal text and has no marks. | Step | Inline | Line | |---|---|---| | XL | `++text++` | `+## text` | | L | `+text+` | `+# text` | | M | (no marks) | (no marker) | | S | `-text-` | `-# text` | | XS | `--text--` or `—text—` | `-## text` | - **Inline** marks follow the edge rule (Section 7.1). An em dash pair is a second spelling of `--`. - **Line markers** sit at the start of a line and need a space after them. Like an ATX heading, a line marker sizes exactly its own line, and it may come directly after a paragraph line with no blank line between. Plain viewers show `-# text` as text, because a list item needs a space after its `-` or `+`. ### 7.3. Text Color and Grays A color name in parentheses, directly after the closing mark of a styled span, colors that span: ```markdown the build is **failing**(red), the docs are *passing*(green) ~~old price~~(g50), `exit 1`(red), ++big news++(blue) ``` - **Styled spans** are emphasis (`*` or `_`), strong emphasis (`**` or `__`), strikethrough (`~~`), code spans, and the size marks (Section 7.2). The span keeps its own style, and the color is added on top. - **Colors:** `red`, `orange`, `yellow`, `green`, `cyan`, `blue`, `magenta`, and `purple`. - **Grays:** `g10`, `g25`, `g33`, `g50`, `g67`, `g75`, and `g90`, plus `slg` (super-light), `lg` (light), `dg` (dark), and `sdg` (super-dark). - Names are case-insensitive. Only these names count: `**Note**(see below)` stays plain text. - The `)` follows the closing edge rule (Section 7.1). So `**x**(red)c` stays plain. - `\(red)` keeps the parentheses as plain text, as in `**x**\(red)`. - After a highlight or a spoiler, `(red)` is ordinary text. Viewers decide how highlights and spoilers look. - Viewers pick the actual shade for every name, in day and night themes alike. - Colored text always carries another style too, so color is never the only signal. A plain Markdown viewer shows the styled span followed by the name: the word "failing" in bold, then "(red)", which still says what was meant. ### 7.4. Highlight `==text==` highlights text, like a marker pen. The marks follow the edge rule (Section 7.1). A file cannot pick the highlight color. Each renderer picks its own, and a renderer may show no highlight at all. ### 7.5. Spoilers `||text||` hides text until the reader reveals it, as in Discord and Telegram. The marks follow the edge rule (Section 7.1). Inside a table cell, the pipes are escaped: `\|\|text\|\|`. ### 7.6. Mentions A mention is `@` followed by a name, and optionally `/` and a platform code, as in `@qrux` or `@qrux/gh`. - The `@` follows the opening edge rule (Section 7.1), so `q@example.com` is not a mention. - A name is letters, digits, `_`, `-`, and `.`, not ending in `.`. A platform code is letters and digits. - A mention only has to make sense to the conversation or document it is in. Zipdown implies no global identity. - The platform code is optional. The site keeps a list of common codes as a shared convention (Section 21). - A renderer that cannot resolve a mention shows it as written. ### 7.7. Emoji Shortcodes A name between colons, such as `:tada:`, stands for an emoji. There is no standard list: Discord, Slack, and GitHub each keep their own. Zipdown keeps its own list on the site (Section 21). An unknown name shows as written. ### 7.8. Indent `>` keeps its CommonMark and email meaning: a quote, with depth by repetition as in RFC 3676 [RFC3676], so `>>` is a quote inside a quote. Indent has its own markers. **Block indent.** `))` followed by a space indents one level, `)))` two, and so on: ```markdown )) This paragraph is indented one level. It continues here without a marker. ))) This one is indented two levels. ``` - It is a container, like `>`: lists, code, and quotes can sit inside it, and it nests with `>` both ways. - It has lazy continuation, like `>`: a paragraph needs the marker only on its first line. A blank line ends it. - A single `)` means nothing. **Region indent.** A line holding only `))>` indents everything after it, across blank lines, until a line holding only `<((`: ```markdown ))> First indented paragraph. Second indented paragraph. <(( ``` - Each `)` beyond the first in an opener adds one level: `))>` adds one, `)))>` adds two. Each `(` beyond the first in a closer removes one: `<((` removes one, `<(((` removes two. - Openers and closers need not pair up one to one, as long as the depth never goes below zero and is zero at the end of the body. A file that breaks either rule is invalid. - Neither line needs a blank line before or after it. - Block indents inside a region add to the region's depth. ### 7.9. Tables Tables are GFM pipe tables [GFM], with column alignment set by colons in the second row (`:---` left, `---:` right, `:---:` center), plus: - **No cell spans.** A cell never merges into another, so an empty cell written `||` is just an empty cell, as in GFM, and `^^` is just text. - **No line breaks in a cell.** `
` is a reserved tag and does nothing (Section 7.12). Cells hold no block content. - **Headerless tables:** a header row whose cells are all empty is not drawn. - **Captions:** a paragraph starting with `Table:`, directly above or below the table, separated from it by a blank line. - **Column widths** are the renderer's business. ```markdown | | | | |--------|-------|-------| | North | 10 | 12 | | South | 11 | 13 | Table: Sales by region ``` In a plain GFM viewer, the empty header shows as a blank row, and the caption shows as an ordinary line of text. ### 7.10. Strikethrough, Task Lists, Plain Links, and Footnotes As on GitHub [GFM]: - **Strikethrough:** `~~text~~`. - **Task lists:** `- [ ] to do` and `- [x] done`. - **Plain links:** bare `https://…` and `www.…` addresses become links. - **Footnotes:** `text[^1]`, with `[^1]: the note` anywhere in the body; notes collect at the end. GFM's tag filter is not part of `zd1`: `zd1` shows no raw HTML at all (Section 7.12). ### 7.11. Image Size and Crop An image may be followed directly by attributes in braces, in the style of Pandoc, Quarto, and kramdown: ```markdown ![chart](assets/chart.svg){width=50%} ![portrait](assets/face.jpg){width=100% height=20lh position=top} ![banner](assets/wide.pdf#page=2){width=100% height=8lh position="50% 0%"} ``` - **Syntax.** `{` immediately after the image's closing parenthesis, then `key=value` pairs separated by spaces, then `}`. A value that contains a space is quoted with double quotes. - **`width`** is a percentage of the column, such as `50%`. No other unit. - **`height`** is in `lh`, lines of body text, such as `20lh`. No other unit. - **Out:** pixels, which mean nothing across display densities; `em`; and viewport units, which WebKit handles inconsistently. - **One size:** the image keeps its own aspect ratio. Every image type has one: raster images, SVG through its viewBox, and PDF pages. - **Both sizes:** the image fills the box, and the overflow is cropped. This is the only behavior for both sizes; there is no fit or stretch keyword. - **`position`** sets where the crop happens, in CSS `object-position` syntax: the keywords `top`, `bottom`, `left`, `right`, and `center`, percentages, or a mix. The default is centered. - **Unknown attributes and bad values** are ignored, and the default applies, the same way an unknown `zd-` key is ignored in the header. - The attributes apply to every image asset, not only PDF. In a plain renderer the braces show as text next to the image. The image is broken there anyway, because the asset lives inside the payload, so the leftover costs little. ### 7.12. HTML `zd1` shows no HTML. Raw HTML in the body is never rendered, and there is no allowlist. - **Reserved tags:** `br`, `sup`, `sub`, `kbd`, `details`, `summary`, `table`, `thead`, `tbody`, `tr`, `th`, and `td`. They have no meaning in `zd1`; a later version may give them one. The tags themselves have no effect, and whatever sits between them is ordinary text: `x2` shows as `x2`. They raise no notice. - **Any other tag** is left out. Text between inline tags stays text: `plain` shows as `plain`. An HTML block, in CommonMark's sense, is left out whole, unless it holds only reserved tags, in which case its text is shown as a paragraph. - **Code is never shown,** not even as text: the contents of `script`, `style`, `iframe`, `object`, `embed`, `noscript`, `template`, `textarea`, `title`, `svg`, `math`, and similar tags are left out with the tags, and nothing a viewer offers puts them back. - **Viewers say so loudly.** When anything other than a reserved tag was left out, a viewer shows a prominent notice outside the document: the document contains HTML, and none of it is shown. Source mode shows the file as written. - **Comments** are not tags; they follow Section 17.1. Flavors other than `zd1` keep their own rules for raw HTML, narrowed by Section 20. ## 8. Separator Directly after the body come exactly these three lines, 34 octets in all: ```text *** -----END OF DOCUMENT----- *** ``` - The separator is mandatory and exact. Each line ends with LF. It is English only and has no translation. - It tells a person reading in a plain renderer that the document is over: everything below it, if shown at all, is there for tools. - The framing borrows from PEM [RFC7468], whose `-----END ...-----` lines mark where encoded content stops. - `***` is a thematic break, a horizontal rule. It is used instead of `---` because a `---` line directly under text turns that text into a heading; `***` never does. Because the body ends with an empty line, the first `***` always starts a new block. - The separator is not part of the body. MLO and `zd-markdown-sha256` do not cover it. - A Zipdown renderer does not show it, any more than it shows the marker line or the index. ## 9. Boundary Marker ```text marker = hex( R XOR H[0..191] ) "-" hex( H[192..255] ) 65 characters \______ 48 hex ______/ \____ 16 hex _____/ H = SHA-256 of the body (256 bits) R = 192 bits from a cryptographically secure random source ``` ![Marker construction](assets/marker.svg) The last 16 hex digits of the marker equal the last 16 hex digits of `zd-markdown-sha256`, so the two can be checked by eye. If the marker appears in the body, the writer replaces R with the first 192 bits of SHA-256(R) and tries again. The marker MUST NOT contain `-->`; one built this way never does. The marker fits MIME's 70-character boundary limit. ## 10. Delimiters and the Comment After the separator, in order: ```text --LF opening delimiter line (visible, on purpose)
LF comment closes ``` - The opening delimiter line is visible in plain renderers by design. It tells a reader this is not plain Markdown. - `` across blank lines. - Writers MUST NOT emit trailing whitespace on either delimiter line. Readers ignore trailing spaces and tabs on them. - Renderers SHOULD follow the comment rules of CommonMark 0.31 or later, under which the payload is hidden. Hiding is guaranteed only there. ## 11. Payload ### 11.1. Asset Blocks - Each asset is encoded as standard base64 with padding [RFC4648], wrapped at 64 characters per line. Every line but the last is exactly 64 characters. Every line ends with LF. - The base64 is canonical: decoding and re-encoding it gives the same characters. An asset of N octets takes exactly `4 * ceil(N / 3)` characters, and its decoded length MUST equal its size in the index. - Blocks appear in the same order as the index rows. - Consecutive blocks are separated by exactly one empty line. - Between the `LF ``` - Both fields give the octet offset, from the start of the file (byte 0 is the first byte of the header), of the index's opening fence line. - The decimal field has no leading zeros. - The hex field is always 32 digits. The top 8 hex digits (32 bits) are reserved and MUST be zero. The low 24 hex digits (96 bits) equal the decimal value. - Because the hex field is fixed width and the terminator is always the same two octets, the hex field sits at a fixed distance from the end of the file: its first digit is 39 octets before the end. - The two fields MUST agree, and the offset MUST be that of the one index, directly after the comment-close line. Anything else makes the file invalid. - Nonzero reserved bits make the file invalid. So does an offset at or past the end of the file. - A reader whose integers cannot hold the offset treats the file as too large to read. (The limit in Java and PHP is 2^63 - 1 octets, about 8 exabytes.) - The pointer is a hint. Readers SHOULD check that the fence is really there. - The line starts with ` plain Markdown, done | yes v MLO > 0, separator at --no--> scan line by line for header end + MLO, delimiter ----------> the opening delimiter; line right after it? body ends 34 octets before it | yes | v v body known: render it <----------------------+ | v last line is "."? read the pointer above it --> seek to the index | v check the index (ranges inside the payload, ascending, no overlaps, sizes match line counts) | v for each asset a link needs: hash the line range (base64-sha256), decode, hash again (sha256), hand the bytes to the renderer ``` ![Reader flow](assets/reader-flow.svg) A tool that never shows the file to a person may skip verification. A tool that shows it MUST verify what it shows (Section 20). A valid signature from a known key lets it skip some cross-checks (Section 16). ### 17.1. Verify, Then Strip Some parts of a file are invisible in every renderer: HTML comments in the body, assets nothing links to, and invisible characters. People cannot see them, but software that reads the file can, which makes them a channel for hiding instructions aimed at AI agents. So a conforming reader, after verifying the original bytes, strips them before handing the document on: - **Body comments** (``) are removed. They are legal in the file; they just never get past the reader. - **Unlinked assets** are dropped. - **Image metadata** is pulled out of every image asset the body shows: EXIF, XMP, IPTC, comments, PNG text chunks, the time stamp, application data, and any bytes after the end of the image; in SVG, comments, `metadata`, and `desc`. Two things stay, because the picture needs them: - **Color.** An ICC profile keeps its color data. Its four text tags are replaced by fixed placeholders: `desc` by `Color profile`, `cprt` by `Unspecified`, and `dmnd` and `dmdd` by `Unspecified`. The profile ID, which no longer matches, is set to zero, meaning "not computed". - **Orientation.** The EXIF orientation is kept, alone, so the picture still shows upright. (A reader MAY instead rotate the pixels, if it keeps the color profile.) This works on the verified bytes the reader hands on. The file itself, and its hashes, never change. - **Invisible characters** are removed: Unicode tag characters (U+E0000 to U+E007F), zero width space (U+200B), word joiner (U+2060), byte order marks (U+FEFF), and the bidirectional controls (U+202A to U+202E, U+2066 to U+2069). Zero width joiner (U+200D) and zero width non-joiner (U+200C) stay: emoji and several scripts need them. Everything downstream of the reader, agents and tools included, sees only the stripped document. A reader that hands a document to an agent SHOULD pass images as pixels only, never as the original file bytes. A viewer shows what was stripped in a **sidebar** beside the document: each comment anchored where it sat, the unlinked assets, a mark wherever invisible characters were removed, and each image's metadata, original profile text included. HTML is not in the sidebar; it gets the notice in Section 7.12. Source mode, the raw file, shows everything. People can see all of a file; agents cannot be steered by what people cannot see. ## 18. Appearance in Plain Renderers Informative. A renderer that knows nothing about Zipdown shows: ```text +--------------------------------------------------+ | Title | body, as usual; | ... [broken image: dot]{width=10%} ... | asset images break | ------------------------------------------------ | rule | -----END OF DOCUMENT----- | separator | ------------------------------------------------ | rule | --3e9c81245fe5...dbd9ba-af2fb38cc34248a2 | marker line, visible | | on purpose | path size lines(1-based,inclusive) ... | index, monospace | assets/dot.png 70 14-15 ... | | | | Psst, this is a Zipdown file! ... | epilogue, one | (the same, in Chinese) | language per line | | | . | terminator +--------------------------------------------------+ (payload and pointer: hidden inside HTML comments) ``` ![Plain renderer view](assets/plain-view.svg) Renderers vary. Some escape raw HTML, in which case the payload shows as text. Renderers that follow comment rules older than CommonMark 0.31, which forbid `--` inside a comment, may end the comment early at the closing delimiter line. ## 19. Extracting an Asset by Hand Informative. Given a line range `L1-L2` from the index: ```bash sed -n 'L1,L2p' file.zd | base64 -d > out ``` ```bash awk 'NR>=L1 && NR<=L2' file.zd | base64 -d > out ``` ```bash head -n L2 file.zd | tail -n +L1 | base64 -d > out ``` In `vi`, from command mode: ```text :L1,L2w !base64 -d > out ``` Older macOS `base64` spells decode `-D`. ![Extracting by line range](assets/extract.svg) ## 20. Security Considerations A Zipdown file is untrusted input. Most risks of Markdown itself, including raw HTML in the body, are already described in [RFC7763], and renderers apply their usual sanitization. What Zipdown adds: - **Hints can lie.** MLO, index sizes and ranges, and the pointer are all hints. Readers SHOULD check them (Sections 4.4, 12, and 14) and MUST NOT read past the end of the file on their say-so. - **Parser differentials.** Duplicate `zd-` keys (Section 4.3), duplicate or case-colliding paths (Section 11.2), and an inconsistent index (Section 12) make a file invalid, because two readers could otherwise disagree about what the file contains. - **Header parsing.** The header is parsed as plain data only (Section 4.1). Some YAML libraries can construct objects or run code from tags. - **Unsafe paths.** Absolute paths, `..` segments, and the other path forms in Section 11.2 are invalid. A tool that extracts assets MUST NOT write outside its target directory, and on a case-insensitive file system it MUST NOT let one asset overwrite another. - **The reader serves the assets.** Plain Markdown links to assets hosted somewhere else, and the browser's origin rules apply. A Zipdown reader hands embedded assets to its renderer itself, so it effectively becomes their server. It SHOULD treat every asset like content from an unknown website, never as its own. - **Active content.** Any kind of file may be an asset. SVG can carry scripts, event handlers, and embedded HTML; HTML assets are pages. A renderer MUST display SVG only through an image element, never as inline markup, and MUST NOT open any asset as a document in its own origin. Assets that are not images are offered as files to save. Assets are served from an isolated origin with a content policy that allows no scripts. - **Raw HTML in the body.** In `zd1` none is rendered (Section 7.12). Under any other flavor it MUST be sanitized to this allowlist, with every attribute removed: `br`, `sup`, `sub`, `kbd`, `details`, `summary`, `table`, `thead`, `tbody`, `tr`, `th`, and `td`. Some Markdown libraries pass raw HTML through by default. - **Metadata in images** is text people do not see, so it is a channel too; readers pull it out (Section 17.1). - **Links.** An image source resolves only to an asset path. Links that start with `//` (scheme-relative) or `file:` are never followed: on Windows, `//host/share` reaches out to a network share and leaks the user's credentials. - **`position` values** are checked against a strict grammar: one or two items, each one of `top`, `bottom`, `left`, `right`, `center`, or a percentage from 0% to 100%. Anything else is ignored, so a value can never inject CSS. - **Depth limits.** Indent (block and region together) and nested inline marks are drawn at most 16 levels deep. Region markers still count fully for the balance rule in Section 7.8. - **Sanitizing is a separate step.** A reader that verifies hashes and a sanitizer that rewrites content do not mix: sanitizing changes the bytes. The natural order is read, verify, sanitize, render. - **No alternates.** One figure, one asset (Section 11.4), so a file cannot show different pictures to different readers by design. - **Attributes carry no style.** Image attributes are limited to the keys in Section 7.11. They are not CSS and cannot reach anything outside the image's box. - **Showing means verifying.** A renderer that shows a file to a person MUST check the body's hash before showing the body, and an asset's hashes before showing that asset. Otherwise two apps could show the same file differently: one trusting a stale MLO, one catching the mismatch. Tools that never show content, such as indexers, MAY skip verification. - **Lookalikes in the body.** The body is free text, so it can contain lines that look like a separator, an index, an epilogue, or a terminator. Readers find the real ones by position (the header, the marker, and the pointer), never by appearance, so a lookalike is just body text. A person reading the raw file can still be fooled. - **No remote loads.** A renderer MUST NOT load anything from outside the file, such as remote images, fonts, or styles, unless the person viewing it turns that on. A file carries everything it needs; that is the point of the format. A remote load would also tell a third party when the file was opened. - **Integrity and authenticity.** The hashes catch damage and careless edits; anyone who can change an unsigned file can recompute them. A signature from a known key adds authenticity (Section 16). It does not make the content safe, and it does not cover the unsigned parts listed there. ## 21. Media Type, Extension, and Site - Extension: `.zd`. - Media type: `text/markdown; charset=UTF-8; variant=zipdown` [RFC7763]. - Site: `zd.treeflow.ai` is the address written into every file. It permanently redirects to `treeflow.ai/zd`, where the site is served. If the site ever moves, only the redirect changes, and every file already written still points to the right place. - The site keeps two living lists that are not part of this memo: - **Emoji shortcodes** (Section 7.7), at `treeflow.ai/zd/emoji` and `treeflow.ai/zd/emoji.json`. The first version is GitHub's `gemoji` list [gemoji]. - **Mention platform codes** (Section 7.6), at `treeflow.ai/zd/platforms` and `treeflow.ai/zd/platforms.json`. The first version: `gh` GitHub, `li` LinkedIn, `yt` YouTube, `tw` Twitter, `bs` Bluesky, and `su` Substack. - Viewers bundle a versioned copy of each list. They never fetch a list while showing a file, because that would be a remote load. - A future binary encoding would not be valid text and would get its own type and its own RFC. ## 22. Implementations - **Reference:** C, PHP, and Java. - **Also provided:** Python and JavaScript. - The C reference is dependency-free: SHA-256 from the reference code in RFC 6234, base64 from BIND's `b64_ntop` and `b64_pton`, with license notices kept. - Every other language listed uses only its standard library. - A conformance test suite, with files that must be accepted, files that must be rejected, and the exact rendering of every `zd1` feature, accompanies the reference implementations. ## 23. Future Work - Spans for combined turns: syntax letting a combined Zipdown file give back each original turn with a matching hash. ## 24. Example A complete, verified Zipdown file carrying a 1x1 PNG and a short text file. For reproducibility, R was derived from a fixed seed instead of a random source; a real writer MUST use a cryptographically secure random source. The same bytes travel inside this document as `assets/example.zd`. ````text --- zd-version: 1 zd-markdown-length-octets: 107 zd-markdown-sha256: 701b9eb2ff1a4cd68184c120c84f68d715fe0bceab30cfbd41703a59dcbf40ce zd-boundary: bd0805a4e5e77c086265536f5094c682f8dc0d06a8c26e62-41703a59dcbf40ce --- # Hello, Zipdown Here is a dot: ![dot](assets/dot.png){width=10%} And a note: [notes](assets/notes.txt) *** -----END OF DOCUMENT----- *** --bd0805a4e5e77c086265536f5094c682f8dc0d06a8c26e62-41703a59dcbf40ce
```zipdown-index path size lines(1-based,inclusive) base64-sha256 sha256 assets/dot.png 70 18-19 a666a27bb8de48cd9fa7e147d451a1d5b7ed400fd7f8e3a3944607c68ff1e6d4 497790947d4666760ce38f3c00e852c71fdb66cae849bae8e9ede352719e1581 assets/notes.txt 34 21-21 4eb8c6623242149abd72ed676f95c77a595766610c2e8b34dcf33634b8537843 2733e8b5c00a8b570f9fd273463a75600f35ec69d6533877568ebf5b809b1c76 ``` Psst, this is a Zipdown file! There are images and files tucked inside that a plain Markdown viewer can't show you. Grab a Zipdown reader at zd.treeflow.ai to see everything. \ 嘿,这是一个 Zipdown 文件!里面藏着普通 Markdown 查看器显示不了的图片和文件。到 zd.treeflow.ai 下载 Zipdown 阅读器,就能看到全部内容。 . ```` Checks performed on this example: it contains no CR; its header ends at octet 217, and the separator begins exactly at that plus MLO (107), with the opening delimiter line right after it; the marker's tail equals the last 16 hex digits of `zd-markdown-sha256`; the index rows (`assets/dot.png`, 70 octets, lines 18-19; `assets/notes.txt`, 34 octets, lines 21-21) lie inside the payload, ascend, and span the line counts their sizes imply; both hashes on each row match; the epilogue starts with the exact English line and has one translation; the pointer (658) lands on the index fence; and the last line is the terminator. A signed copy travels as `assets/example-signed.zd`. It is signed with a test key whose 32-octet private seed is the SHA-256 of the text `Zipdown example key: published, never trust`. Anyone can recompute that key, so it MUST NOT be trusted for anything real. Its public key, in hex: ```text 3cec666df17aaa5394160c49bf387f23b91356fd8917c3b6febf495d9d493cd0 ``` Its header: ```text --- zd-date: 2026-11-11T21:04:00.000000000Z zd-version: 1 zd-markdown-length-octets: 107 zd-markdown-sha256: 701b9eb2ff1a4cd68184c120c84f68d715fe0bceab30cfbd41703a59dcbf40ce zd-boundary: bd0805a4e5e77c086265536f5094c682f8dc0d06a8c26e62-41703a59dcbf40ce zd-signature: ed25519 ad7117d6ed57a87b ENM7uyF9j23ETthigh+g4iDvJRqRGaV2Mq7CdLzH/EcnZ2/B/qIAMIp0CKuvqJ7CyBpPrf7dYpd3H0greJbqAA== --- ``` Checks performed on the signed copy: every check above passes for it too; its key id matches the public key; and the Ed25519 signature over the context string, the header with the signature removed, the index, and the epilogue verifies. ## 25. Open Issues - **Metadata in other formats.** Section 17.1 names what is pulled out of JPEG, PNG, WebP, GIF, and SVG. AVIF, HEIC, TIFF, and PDF carry metadata too, and are not covered yet; until they are, readers pass them to agents as pixels only. - **The conformance test suite** (Section 22) is still to be written. ## 26. References ### 26.1. Normative References - **[RFC2119]** Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, March 1997. - **[RFC8174]** Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, May 2017. - **[RFC3629]** Yergeau, F., "UTF-8, a transformation format of ISO 10646", STD 63, RFC 3629, November 2003. - **[RFC4648]** Josefsson, S., "The Base16, Base32, and Base64 Data Encodings", RFC 4648, October 2006. - **[RFC7763]** Leonard, S., "The text/markdown Media Type", RFC 7763, March 2016. - **[CommonMark]** MacFarlane, J., "CommonMark Spec", version 0.31.2, https://spec.commonmark.org/0.31.2/. - **[GFM]** GitHub, "GitHub Flavored Markdown Spec", version 0.29-gfm, https://github.github.com/gfm/. - **[RFC8032]** Josefsson, S. and I. Liusvaara, "Edwards-Curve Digital Signature Algorithm (EdDSA)", RFC 8032, January 2017. - **[FIPS180-4]** NIST, "Secure Hash Standard (SHS)", FIPS PUB 180-4. ### 26.2. Informative References - **[RFC7468]** Josefsson, S. and S. Leonard, "Textual Encodings of PKIX, PKCS, and CMS Structures", RFC 7468, April 2015. - **[gemoji]** GitHub, "gemoji", https://github.com/github/gemoji. - **[RFC3676]** Gellens, R., "The Text/Plain Format and DelSp Parameters", RFC 3676, February 2004. - **[age]** Valsorda, F. and B. Cartwright-Cox, "age-encryption.org/v1", https://age-encryption.org/v1. - **[RFC5321]** Klensin, J., "Simple Mail Transfer Protocol", RFC 5321, October 2008. - **[RFC2046]** Freed, N. and N. Borenstein, "Multipurpose Internet Mail Extensions (MIME) Part Two: Media Types", RFC 2046, November 1996. - **[RFC6234]** Eastlake, D. and T. Hansen, "US Secure Hash Algorithms (SHA and SHA-based HMAC and HKDF)", RFC 6234, May 2011. - **[RFC8118]** Hardy, M., Masinter, L., Markovic, D., Johnson, D., and M. Bailey, "The application/pdf Media Type", RFC 8118, March 2017. - **[Pandoc]** "Pandoc User's Guide: Extension: attributes", https://pandoc.org/MANUAL.html. ## Acknowledgments Site version only. The author thanks TurboAsshole™ for several proposals: - That a single CR anywhere invalidate the file. Rejected on review, although its spirit lives on in the rule that a CR voids every guarantee. - That `zd-markdown-flavour` be punished. Adopted: the key is quietly ignored, as if it had never been written. - That the epilogue include French. Reversed; Japanese took its place, then left with five others to wait in the lobby. - That PowerShell be offered an extraction snippet. Declined. - That Rust receive an implementation. Declined. - That the pointer hold a 128-bit offset, enough to number every atom in a small lake. The committee settled on 96 bits, enough for a hot tub full of people, and reserved the rest. Ever since we dropped ZIP, the file's really opened up. ## Appendix A. Notes for Viewers and Editors Informative. What the reference viewer and editor do; other tools are free to differ. - **Four modes.** **Clean** shows the rendered document with no marks and cannot be edited. **Marked** shows every mark, dimmed. **Live** shows marks only in the element being edited, in the style of Obsidian. **Source** shows the raw file. Any mode can sit beside any other. - **One parser.** The editor and the renderer use the same parser, so they always agree on where a span starts and ends. - **No speculative styling.** An opening mark with no closer styles nothing until the closer exists. A style ends exactly at its closer, and typing past the closer never extends it. - **No sticky state.** Leaving a span by moving the caret, deleting its closer, pasting, undoing, or redoing leaves no style behind; the text's style always follows from the text. - **Flavor follows the file,** not a setting (Section 6.2). *** -----END OF DOCUMENT----- *** --59cc43e990fd8cd65250cabf85010f72c265ea44f243ec42-3adcaafac890ec59
```zipdown-index path size lines(1-based,inclusive) base64-sha256 sha256 assets/format-overview.svg 13816 1406-1693 08c31ff873d3af2557ebdb9b2f4bcc383caecf765de408f9c5e44e1d82281149 82537e45af407d1b36b402020b5bea89fe2ee21dab767cc9229ce96fc432155d assets/marker.svg 5883 1695-1817 f364cdd017801ec236312d00ff8b09f2e40b502b57971b25249e59273d62a506 c9184059696437f6773da162ba4c5278938adf327bfd2dbccb76ac6b3a007305 assets/hash-ranges.svg 11477 1819-2058 5d14e7ce1f0a8c85a08890b58089d659436397844d039d28f3b16d27d0253bf0 b93c1fc74a59cf8391063b4b0b969492ae027e96842a6076e4db670d9d6cb773 assets/extract.svg 3303 2060-2128 d53631870dfc2bf84d641f620aa792ec0c819aa2c1cd467ff522fb3dd580f457 90403a87d3f8b9d83bd127640c12fe30525f0974b940e8809bb6e9f5bb3d4f09 assets/reader-flow.svg 8296 2130-2302 23e19e229753b140435f0fca3f57cec5931774f0916180ed9af8f6a82e0bb3a2 3cf6c388cecf304640c1d1619128dc3e170e1484613e86223dd3632e4f1bc8f7 assets/plain-view.svg 12444 2304-2563 f4cc5fbc7f1e549071cb4b36508226e2bbbdec2913f8a4a5adc90bfaadc31b0c e0985f217f3985dbe79203a78b676a4d8d8d14bb9ab6be3445389edd80ce530f assets/example.zd 1473 2565-2595 28b54db88b1f11957dac86d7c6cc37398013acc211e504687db6d7ec883fe89d 92625cc455c35e4ed60db48d0fa65d8704b5311c78eb7b21b9739176212f5e79 assets/example-signed.zd 1641 2597-2631 2f81c5bfba13314cf5f10202bbb7eef3bb5fdb1dd0bfd431570240b9f2e4f312 6d8061fcb5fab2f64ad6e9d37477c12a8e2a14a19dd1af9fc57c224846fbcb44 ``` Psst, this is a Zipdown file! There are images and files tucked inside that a plain Markdown viewer can't show you. Grab a Zipdown reader at zd.treeflow.ai to see everything. \ 嘿,这是一个 Zipdown 文件!里面藏着普通 Markdown 查看器显示不了的图片和文件。到 zd.treeflow.ai 下载 Zipdown 阅读器,就能看到全部内容。 .