krz/orgo

Lightning fast org-mode static site generator. fast go org-mode static-site-generator

Commit 2f14a20767

2f14a2076788dd8a1d9e2c4ee571df3b19198dcf

parent: 2ff6947870

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-11 21:47 UTC

Rename to orgo

org-ssg described the project and named nothing. `orgo` is four letters, sits in
the same sound-space as hugo and zola without borrowing from either, and is free
on crates.io — which almost nothing short is.

The rename reaches further than the crate: the config file is `orgo.toml` and
the cache is `.orgo-cache.json`. Both are better done now, while the only site
using them is one I can rename in the same afternoon.

Layout: unified · split

.github/workflows/ci.yml +2 −2
@@ -1,6 +1,6 @@
11name: CI
22
3# Build, test and lint on both platforms org-ssg is used from, plus a compiler-floor job.
3# Build, test and lint on both platforms orgo is used from, plus a compiler-floor job.
44#
55# WHAT THIS CATCHES THAT LOCAL WORK DOES NOT:
66#
@@ -96,7 +96,7 @@ jobs:
9696 # Build only. The tests pull in dev-dependencies whose own floors move
9797 # independently, and chasing those would make this job about someone else's MSRV.
9898 #
99 # The floor is set by dependencies rather than by org-ssg — its own code compiles on
99 # The floor is set by dependencies rather than by orgo — its own code compiles on
100100 # 1.82 — which is precisely why it is checked here instead of reasoned about: a
101101 # dependency raising its floor is invisible until someone on an older compiler
102102 # tries to build.
.github/workflows/release.yml +3 −3
@@ -2,7 +2,7 @@ name: Release
22
33# Build binaries for a tag and attach them to a GitHub release.
44#
5# WHY BINARIES AT ALL, given `cargo install org-ssg` exists: installing from source needs
5# WHY BINARIES AT ALL, given `cargo install orgo` exists: installing from source needs
66# a Rust toolchain and about a minute of compiling syntect. Someone evaluating a site
77# generator should be able to download one file and point it at their notes.
88#
@@ -74,9 +74,9 @@ jobs:
7474 - name: Package
7575 shell: bash
7676 run: |
77 staging="org-ssg-${{ github.event.inputs.tag || github.ref_name }}-${{ matrix.target }}"
77 staging="orgo-${{ github.event.inputs.tag || github.ref_name }}-${{ matrix.target }}"
7878 mkdir "$staging"
79 cp "target/${{ matrix.target }}/release/org-ssg" "$staging/"
79 cp "target/${{ matrix.target }}/release/orgo" "$staging/"
8080 cp README.md LICENSE CHANGELOG.md "$staging/"
8181 tar czf "$staging.tar.gz" "$staging"
8282 shasum -a 256 "$staging.tar.gz" > "$staging.tar.gz.sha256"
CHANGELOG.md +4 −4
@@ -7,7 +7,7 @@ Two conventions worth knowing before reading:
77
88- **A cache-format bump is not a change you need to act on.** The incremental cache is
99 versioned and discards itself; a bump means the next build re-renders everything once.
10- **Output changes are called out.** org-ssg aims at what Emacs exports from the same
10- **Output changes are called out.** orgo aims at what Emacs exports from the same
1111 file, so an entry that says "now renders X" means your pages will change. That is the
1212 product, not a regression — but it belongs in a changelog rather than a diff you find
1313 later.
@@ -47,7 +47,7 @@ Release engineering, so that a version number is worth reading.
4747 built with `--strict`. Emacs is installed on both, so the oracle suite runs for real
4848 instead of skipping.
4949- **A checked MSRV**, 1.88 — which is how it came to be 1.88 rather than the 1.82
50 org-ssg's own code needs. The floor comes from dependencies, and nobody finds that out
50 orgo's own code needs. The floor comes from dependencies, and nobody finds that out
5151 by reasoning about it.
5252- **Release binaries** for macOS (arm64, x86_64) and Linux (gnu, musl), built on tag into
5353 a draft release. The tag is checked against `Cargo.toml` before anything is built.
@@ -117,7 +117,7 @@ publishes from the same sources. **All of these change output.**
117117## 0.12.0
118118
119119- `serve`: a development server with live reload, bound to loopback.
120- A documentation site under `docs/`, built by org-ssg itself.
120- A documentation site under `docs/`, built by orgo itself.
121121
122122## 0.11.0
123123
@@ -153,7 +153,7 @@ publishes from the same sources. **All of these change output.**
153153## 0.5.0
154154
155155- Parse diagnostics carry `file:line`, and pages render in parallel.
156- The corpus audit (`org-ssg audit`) and the `emacs --batch` oracle.
156- The corpus audit (`orgo audit`) and the `emacs --batch` oracle.
157157- `#+SLUG:` decides a page's output filename — found by auditing a real corpus, where it
158158 affected 169 of 182 URLs.
159159
Cargo.lock +1 −1
@@ -674,7 +674,7 @@ dependencies = [
674674]
675675
676676[[package]]
677name = "org-ssg"
677name = "orgo"
678678version = "0.19.1"
679679dependencies = [
680680 "anyhow",
Cargo.toml +7 −8
@@ -1,17 +1,16 @@
11[package]
2name = "org-ssg"
2name = "orgo"
33version = "0.19.1"
44edition = "2021"
55description = "Org-mode static site generator that renders the org element tree straight to HTML"
6license = "MIT"
6license = "0BSD"
77readme = "README.md"
88keywords = ["org-mode", "static-site-generator", "emacs", "html", "blog"]
99categories = ["command-line-utilities", "text-processing"]
10# Set before the first `cargo publish`: crates.io shows it on the crate page, and a
11# missing link is the first thing anyone evaluating a generator looks for.
12# repository = "https://git.krz.sh/cmc/org-ssg.git/"
10repository = "https://github.com/ccleberg/orgo"
11homepage = "https://ccleberg.github.io/orgo/"
1312
14# The compiler floor, checked in CI rather than assumed. org-ssg's own code needs 1.82
13# The compiler floor, checked in CI rather than assumed. orgo's own code needs 1.82
1514# (`Option::is_none_or`); the floor is 1.88 because dependencies in Cargo.lock declare it
1615# — `plist` and `time`, both by way of syntect. Raising this is a minor-version change,
1716# never a patch.
@@ -23,11 +22,11 @@ rust-version = "1.88"
2322exclude = ["docs/", "target/", "/.github/"]
2423
2524[lib]
26name = "org_ssg"
25name = "orgo"
2726path = "src/lib.rs"
2827
2928[[bin]]
30name = "org-ssg"
29name = "orgo"
3130path = "src/main.rs"
3231
3332[dependencies]
README.md +18 −18
@@ -1,4 +1,4 @@
1# org-ssg
1# orgo
22
33An org-mode static site generator, in Rust. Org is treated as the *source language*,
44not an inconvenient input to be normalized into markdown. The org element tree —
@@ -14,7 +14,7 @@ it imposes on the data model — pure, hashable, dependency-tracked units — is
1414deliverable, even while the corpus is small enough that a full rebuild is instant.
1515
1616**Full documentation is in [`docs/`](docs/)** — a site written in org and built by
17org-ssg itself. Build and read it with:
17orgo itself. Build and read it with:
1818
1919```bash
2020cargo run -- serve docs -o docs/_site
@@ -33,23 +33,23 @@ Or skip the scaffolding entirely — point it at any directory of `.org` files:
3333cargo run -- build ~/notes -o _site
3434```
3535
36**Zero configuration is a supported path, not a demo.** With no `org-ssg.toml`, no
37templates and no org-ssg-specific markup in your files, you get a complete site: pages,
36**Zero configuration is a supported path, not a demo.** With no `orgo.toml`, no
37templates and no orgo-specific markup in your files, you get a complete site: pages,
3838navigation, syntax-highlighted code and the stylesheet to colour it. Configuration
3939changes what you get; it is never what makes it work.
4040
4141Discovery skips what should not be published — dot-directories such as `.git`, the config
4242file, the templates directory, and the output directory when it sits inside the source, so
43`org-ssg build . -o _site` does the obvious thing.
43`orgo build . -o _site` does the obvious thing.
4444
4545## Configuration
4646
47Everything is optional. `org-ssg init` writes a fully commented `org-ssg.toml`; every
47Everything is optional. `orgo init` writes a fully commented `orgo.toml`; every
4848value below is the default.
4949
5050```toml
5151[site]
52title = "org-ssg site"
52title = "orgo site"
5353base_url = "" # absolute URL, no trailing slash; needed for feeds/canonical links
5454description = ""
5555language = "en"
@@ -224,7 +224,7 @@ group.
224224
225225**A feed is a listing page with an XML template**, not a separate feature — templates are
226226loaded by full filename and any extension, so `output = "feed.xml"` with
227`template = "feed.xml"` is all it takes. `org-ssg init` writes a working RSS template.
227`template = "feed.xml"` is all it takes. `orgo init` writes a working RSS template.
228228
229229A feed is read away from the site that served it, so relative links in one are simply
230230broken. Set `site.base_url` and use the `absolute` filter:
@@ -319,7 +319,7 @@ is the only inherently global stage — it is where the link dependency graph is
319319
320320| Stage | Module | Notes |
321321|---|---|---|
322| config | `src/config.rs` | `org-ssg.toml`: site metadata, nav mode, templates, theme. A hash input. |
322| config | `src/config.rs` | `orgo.toml`: site metadata, nav mode, templates, theme. A hash input. |
323323| PARSE | `src/parser.rs` | Hand-written recursive descent: line lexer → element builder → inline tokenizer. |
324324| audit | `src/audit.rs` | Phase 0 corpus audit: construct frequencies against the IN/OUT line. |
325325| model | `src/model.rs` | The org element tree — Elements (block) vs Objects (inline). |
@@ -424,7 +424,7 @@ feature):**
424424 The config component folds in a **site-structure hash** (every page's `(path, title)`),
425425 because the shared nav bar is global chrome — a title change or a page add/remove alters
426426 the nav on every page and so must re-render them all (otherwise byte-equivalence breaks).
427- **Persisted cache manifest** (`<out>/.org-ssg-cache.json`, JSON), carrying per-page
427- **Persisted cache manifest** (`<out>/.orgo-cache.json`, JSON), carrying per-page
428428 records, the config/template hashes, and the serialized dependency graph, tagged with
429429 `CACHE_FORMAT_VERSION`. A version mismatch, a missing file, or a corrupt file all fall
430430 back to a clean full rebuild — the cache is an optimization, never a correctness
@@ -523,7 +523,7 @@ it over the original and touches the directory, which is one edit and several ev
523523Two rules decide what counts as a change, and they are not the same rules the build uses
524524to find content:
525525
526- **A build input is a change.** Editing `org-ssg.toml` or a template rebuilds, even
526- **A build input is a change.** Editing `orgo.toml` or a template rebuilds, even
527527 though discovery skips both as non-content. The question is "would this change the
528528 site?", not "is this a page?".
529529- **Our own output is not.** `watch . -o _site` puts the output inside the source, so a
@@ -544,7 +544,7 @@ Emacs does.
544544The audit runs against any corpus — point it at your own notes before trusting this tool
545545with them. The numbers below come from a 179-file site published today by weblorg, a
546546wrapper around org's own HTML exporter, which makes it both a realistic workload and a
547directly comparable incumbent. With collections configured, org-ssg now reproduces
547directly comparable incumbent. With collections configured, orgo now reproduces
548548**all 182 of that site's URLs**.
549549
550550```
@@ -560,7 +560,7 @@ whole out-of-scope tail is 8 uses: four `#+TBLFM:` in a post *about* org-mode, t
560560
561561**`#+SLUG:` was a hole big enough to sink the project.** 178 of 179 files set it, and the
562562published URL comes from it, not from the filename: `2018-11-28-aes-encryption.org` is
563served at `blog/aes-encryption.html`. org-ssg derived output paths from source filenames,
563served at `blog/aes-encryption.html`. orgo derived output paths from source filenames,
564564so **169 of 179 pages would have been published at the wrong URL** — every inbound link and
565565every search result, broken, by a tool that reported a clean build. Output paths now come
566566from `#+SLUG:` when present ([`util::output_path`](src/util.rs)); slugs are sanitized so an
@@ -588,10 +588,10 @@ report gets reviewed and shows up as a diff, where a permanently red test gets i
588588Three invariants are asserted outright, and all three hold — heading structure, list
589589nesting, and source-block text match Emacs exactly.
590590
591**No bugs in org-ssg.** Every remaining divergence is a deliberate choice to emit better
591**No bugs in orgo.** Every remaining divergence is a deliberate choice to emit better
592592HTML than org does:
593593
594| | org-ssg | Emacs | why |
594| | orgo | Emacs | why |
595595|---|---|---|---|
596596| emphasis | `<em>`/`<strong>` | `<i>`/`<b>` | semantic, not presentational |
597597| captioned image | `<figure>`/`<figcaption>` | `<p>` + `"Figure 1: …"` | real figure semantics |
@@ -695,7 +695,7 @@ a major version — is what you actually build a site against:
695695
696696| Stable | Detail |
697697|---|---|
698| `org-ssg.toml` keys | Names, types and meaning. New keys are minor releases; removing one is major. |
698| `orgo.toml` keys | Names, types and meaning. New keys are minor releases; removing one is major. |
699699| Template context | `page`, `site`, `nav`, `root`, `pages`, `group`, `groups`, `paginator`, `stylesheet`, and the `absolute` / `rfc822` / `truncate` filters. |
700700| CLI | Command names, flags, and exit codes. |
701701| URLs | How a source path becomes an output path, including `#+SLUG:`. A generator that moves your URLs breaks every link anyone has to you. |
@@ -704,14 +704,14 @@ Explicitly **not stable**, so that the above can be:
704704
705705- **The incremental cache.** Versioned, discarded on mismatch, never a correctness
706706 dependency. It changes whenever it needs to, in any release.
707- **Rendered HTML details.** org-ssg tracks what Emacs exports from the same file, and
707- **Rendered HTML details.** orgo tracks what Emacs exports from the same file, and
708708 closing a gap changes markup. Changes that affect output are called out in
709709 [CHANGELOG.md](CHANGELOG.md) — the class names the documentation names (`post-list`,
710710 `figure-number`, `section-number-N`, `footnote-ref`) are the ones to write CSS against.
711711- **The Rust API.** The crate is published so the binary can be installed with
712712 `cargo install`; the library exists to serve it, and its types move as the tool does.
713713
714The **MSRV is 1.88**, checked in CI on every change. org-ssg's own code compiles on
714The **MSRV is 1.88**, checked in CI on every change. orgo's own code compiles on
7157151.82; the floor comes from dependencies. Raising it is a minor version, never a patch.
716716
717717## Dependencies
RELEASING.md +4 −3
@@ -7,14 +7,15 @@ reports `0.17.0` is the kind of mistake nobody notices for months.
77
88## Before the first publish
99
10`repository` in `Cargo.toml` is commented out, because a wrong URL on a crates.io page is
11worse than none. Set it, then:
12
1310```bash
1411cargo login # a crates.io token, once per machine
1512cargo publish --dry-run
1613```
1714
15`repository` and `homepage` in `Cargo.toml` point at GitHub and at the documentation site
16on Pages. If git.krz.sh becomes the primary remote, `repository` should follow it —
17crates.io shows that link on the crate page, and it should lead somewhere you read.
18
1819## Every release
1920
20211. **Write the changelog entry first.** [CHANGELOG.md](CHANGELOG.md) names behaviour, not
docs/guide/01-cli.org +11 −11
@@ -7,7 +7,7 @@
77=build=, =serve= and =watch= all take the same pair:
88
99#+BEGIN_SRC sh
10org-ssg <command> <SOURCE> -o <OUTPUT>
10orgo <command> <SOURCE> -o <OUTPUT>
1111#+END_SRC
1212
1313*SOURCE is the URL root*, not "the project". =SOURCE/blog/post.org= is published at
@@ -22,7 +22,7 @@ consumes its own output.
2222* build
2323
2424#+BEGIN_SRC sh
25org-ssg build <INPUT> -o <OUTPUT> [--no-cache] [--strict] [--drafts] [--config FILE]
25orgo build <INPUT> -o <OUTPUT> [--no-cache] [--strict] [--drafts] [--config FILE]
2626#+END_SRC
2727
2828If =INPUT= is a directory, it is walked and built into a linked site at =OUTPUT=. If it
@@ -35,7 +35,7 @@ though with no other documents to resolve against, internal links keep a best-ef
3535| =--no-cache= | Ignore the incremental cache and re-render every page. |
3636| =--strict= | Broken internal links and parse diagnostics become a non-zero exit. |
3737| =--drafts= | Include pages marked =#+DRAFT:=. |
38| =--config FILE= | Use this config instead of =org-ssg.toml= in the source directory. |
38| =--config FILE= | Use this config instead of =orgo.toml= in the source directory. |
3939
4040The summary line reports what happened:
4141
@@ -52,13 +52,13 @@ Without it, a broken link is a warning and the build succeeds. With it, the buil
5252and names every problem. Use it wherever a bad build should not ship:
5353
5454#+BEGIN_SRC sh
55org-ssg build content -o _site --strict
55orgo build content -o _site --strict
5656#+END_SRC
5757
5858* serve
5959
6060#+BEGIN_SRC sh
61org-ssg serve <INPUT> -o <OUTPUT> [-p PORT] [--host HOST] [--drafts] [--config FILE]
61orgo serve <INPUT> -o <OUTPUT> [-p PORT] [--host HOST] [--drafts] [--config FILE]
6262#+END_SRC
6363
6464Builds, watches, serves, and reloads the browser when a rebuild lands. This is the
@@ -74,7 +74,7 @@ command to use while writing.
7474laptop, so reaching the local network is something you ask for:
7575
7676#+BEGIN_SRC sh
77org-ssg serve content -o _site --host 0.0.0.0
77orgo serve content -o _site --host 0.0.0.0
7878#+END_SRC
7979
8080The live-reload script is injected into responses and never written to disk, so what you
@@ -83,7 +83,7 @@ deploy stays clean. Details in [[file:../guide/08-workflow.org][Watching and ser
8383* watch
8484
8585#+BEGIN_SRC sh
86org-ssg watch <INPUT> -o <OUTPUT> [--no-cache] [--strict] [--drafts] [--config FILE]
86orgo watch <INPUT> -o <OUTPUT> [--no-cache] [--strict] [--drafts] [--config FILE]
8787#+END_SRC
8888
8989Rebuilds on filesystem events with no server — for when something else is already serving
@@ -92,10 +92,10 @@ the output, or you just want the build to keep up as you write.
9292* audit
9393
9494#+BEGIN_SRC sh
95org-ssg audit <INPUT>
95orgo audit <INPUT>
9696#+END_SRC
9797
98Reports which org constructs a corpus uses and how they land against what org-ssg
98Reports which org constructs a corpus uses and how they land against what orgo
9999supports, plus a census of every keyword, block type, drawer and link scheme seen. Point
100100it at your notes before trusting a tool with them. See [[file:../guide/09-auditing.org][Auditing a corpus]].
101101
@@ -105,7 +105,7 @@ private notes is safe to share.
105105* init
106106
107107#+BEGIN_SRC sh
108org-ssg init [DIRECTORY]
108orgo init [DIRECTORY]
109109#+END_SRC
110110
111111Scaffolds a working site: a fully commented config, an editable copy of the built-in
@@ -118,7 +118,7 @@ that already has content — it fills in what is missing and leaves the rest alo
118118* clean
119119
120120#+BEGIN_SRC sh
121org-ssg clean <OUTPUT>
121orgo clean <OUTPUT>
122122#+END_SRC
123123
124124Removes the output directory, including the incremental cache manifest inside it. You
docs/guide/02-configuration.org +6 −6
@@ -1,8 +1,8 @@
11#+TITLE: Configuration
2#+DESCRIPTION: Every setting in org-ssg.toml, what it changes, and what it costs.
2#+DESCRIPTION: Every setting in orgo.toml, what it changes, and what it costs.
33#+LEDE: All of it optional. A missing config is a valid config.
44
5org-ssg looks for =org-ssg.toml= in the source directory. Pass a different path with
5orgo looks for =orgo.toml= in the source directory. Pass a different path with
66=--config=. Every field has a default, so a directory of org files with no config still
77builds a complete site.
88
@@ -14,7 +14,7 @@ lose an afternoon.
1414
1515#+BEGIN_SRC toml
1616[site]
17title = "org-ssg site"
17title = "orgo site"
1818base_url = ""
1919description = ""
2020language = "en"
@@ -47,7 +47,7 @@ Plus any number of =[[collections]]= blocks, documented in [[file:03-collections
4747
4848| Key | Default | Meaning |
4949|-----+---------+---------|
50| =title= | ="org-ssg site"= | Site name. Available as ={{ site.title }}=. |
50| =title= | ="orgo site"= | Site name. Available as ={{ site.title }}=. |
5151| =base_url= | ="" | Absolute origin, *no trailing slash*. |
5252| =description= | ="" | Available as ={{ site.description }}=. |
5353| =language= | ="en"= | Goes in =<html lang>= in the built-in layout. |
@@ -181,7 +181,7 @@ An unknown name is an error listing the valid ones.
181181Highlighting emits *CSS classes*, never inline styles, so themes live in a stylesheet.
182182Each build writes =syntax.css= into the output and every page links it.
183183
184org-ssg bundles TOML and Org on top of syntect's built-in languages. Anything else
184orgo bundles TOML and Org on top of syntect's built-in languages. Anything else
185185missing is a file away: put a =.sublime-syntax= definition in =syntaxes_dir= and it is
186186loaded. A definition that fails to parse is reported and skipped, because one bad file
187187should not stop a site from building.
@@ -258,6 +258,6 @@ Off is spelled =nil=, =false=, =no=, =0= or =off=; anything else is on.
258258
259259* Configuration is a cache input
260260
261The resolved config is hashed into every page's render key, so editing =org-ssg.toml=
261The resolved config is hashed into every page's render key, so editing =orgo.toml=
262262re-renders exactly the pages it affects — which for most settings is all of them. You
263263never need =--no-cache= after a config change.
docs/guide/03-collections.org +1 −1
@@ -230,7 +230,7 @@ title = "Feed"
230230#+END_SRC
231231
232232This needs =site.base_url=, because a feed with relative links is invalid everywhere it
233is read. =org-ssg init= writes this template and leaves the collection commented out
233is read. =orgo init= writes this template and leaves the collection commented out
234234until there is a base URL to make absolute links from.
235235
236236* Every setting
docs/guide/04-templates.org +1 −1
@@ -122,7 +122,7 @@ Empty on generated pages, which build their content from =pages= or =groups= ins
122122| =keywords= | *Every* =#+KEYWORD:=, by lowercased name. |
123123
124124=page.keywords= is the escape hatch: =#+CUSTOM_THING: x= is
125={{ page.keywords.custom_thing }}=, so your own metadata works without org-ssg knowing it
125={{ page.keywords.custom_thing }}=, so your own metadata works without orgo knowing it
126126exists.
127127
128128** site
docs/guide/05-org-support.org +8 −8
@@ -2,7 +2,7 @@
22#+DESCRIPTION: Exactly which org syntax is handled, which is not, and how the rest degrades.
33#+LEDE: A deliberate subset, with the boundary enforced by tests rather than by hope.
44
5org-ssg parses a defined slice of org. The boundary is not aspirational: every supported
5orgo parses a defined slice of org. The boundary is not aspirational: every supported
66construct has a golden-file test, and every excluded one has a test asserting how it
77degrades. That is what stops the parser drifting toward all-of-org.
88
@@ -37,7 +37,7 @@ machine-readable =datetime=.
3737
3838*** Text conversions
3939
40Org rewrites some prose on export, and so does org-ssg:
40Org rewrites some prose on export, and so does orgo:
4141
4242| Written | Published |
4343|---------+-----------|
@@ -94,7 +94,7 @@ An =html= export block passes through verbatim; every other backend is dropped,
9494emitting LaTeX into an HTML page is worse than emitting nothing.
9595
9696*Any other name is a special block*: =#+BEGIN_NOTE= becomes =<div class="note">= holding
97*parsed org*, which is what makes the convention usable without org-ssg knowing the word
97*parsed org*, which is what makes the convention usable without orgo knowing the word
9898"note". A =COMMENT= block is not published.
9999
100100*** Which languages highlight
@@ -108,7 +108,7 @@ Recognised, among others: =bash= / =sh=, =c=, =c++=, =css=, =clojure=, =diff=, =
108108=makefile=, =markdown=, =matlab=, =objective-c=, =ocaml=, =perl=, =php=, =python=, =r=,
109109=ruby=, =rust=, =scala=, =sql=, =tcl=, =xml=, =yaml=.
110110
111org-ssg adds two syntect does not ship: *TOML* and *Org*. Both are what this project's
111orgo adds two syntect does not ship: *TOML* and *Org*. Both are what this project's
112112own documentation needed on its first page — every config example is TOML, and a tool for
113113org users gets written about in org — so they are compiled into the binary and work with
114114no setup.
@@ -121,7 +121,7 @@ to parse is reported and skipped rather than failing the build.
121121*** The comma escape
122122
123123A line inside a block that would otherwise look like document structure is written with a
124leading comma — =,* heading=, =,#+KEYWORD:= — and org-ssg removes exactly one comma on
124leading comma — =,* heading=, =,#+KEYWORD:= — and orgo removes exactly one comma on
125125output, as Emacs does. Every org example in this documentation relies on it.
126126
127127The escape is not optional politeness: an unescaped =*= at column zero *ends the block*,
@@ -169,7 +169,7 @@ broken.
169169| =#+CAPTION:=, =#+ATTR_HTML:= | Attach to the image *directly* below them — a blank line in between attaches to nothing, as in org. A captioned image is numbered =Figure N:=. |
170170
171171Every other =#+KEYWORD:= is available to templates as
172={{ page.keywords.that_keyword }}=, so metadata org-ssg has never heard of still reaches
172={{ page.keywords.that_keyword }}=, so metadata orgo has never heard of still reaches
173173your layout.
174174
175175* Not supported, and what happens instead
@@ -219,7 +219,7 @@ source-block text are asserted to match exactly.
219219
220220The rest differs deliberately:
221221
222| | org-ssg | Emacs |
222| | orgo | Emacs |
223223|-+---------+-------|
224224| emphasis | =<em>= / =<strong>= | =<i>= / =<b>= |
225225| captioned image | =<figure>= / =<figcaption>= | =<p>= + "Figure 1: …" |
@@ -229,6 +229,6 @@ The rest differs deliberately:
229229| code | =<pre><code>= | =<pre>= |
230230
231231One genuine semantic difference: org treats a single blank line between a =1.= list and a
232following =-= list as *one* list, keeping the first item's bullet type. org-ssg starts a
232following =-= list as *one* list, keeping the first item's bullet type. orgo starts a
233233second list. That was kept on measurement — the pattern occurred zero times across a
234234179-file reference corpus — rather than on taste.
docs/guide/06-authoring.org +4 −4
@@ -39,7 +39,7 @@ That is deliberate: it is what that link would be on the published site, and bet
3939now than by a reader.
4040
4141#+BEGIN_SRC sh
42org-ssg serve content -o _site --drafts
42orgo serve content -o _site --drafts
4343#+END_SRC
4444
4545The keyword is read forgivingly. =t=, =yes=, =1= and a bare =#+DRAFT:= all mean draft,
@@ -125,7 +125,7 @@ Every =#+KEYWORD:= reaches templates under its lowercased name:
125125{% if page.keywords.subtitle %}<p class="subtitle">{{ page.keywords.subtitle }}</p>{% endif %}
126126#+END_SRC
127127
128Nothing needs to be registered, and org-ssg needs no release to support a keyword you
128Nothing needs to be registered, and orgo needs no release to support a keyword you
129129invented.
130130
131131* Assets
@@ -138,9 +138,9 @@ Four things are *never* published:
138138
139139- Dot-entries such as =.git= and =.env=. A source directory is often a repository, and
140140 publishing its history next to the homepage is a real way to leak a project.
141- =org-ssg.toml=, which is a build input.
141- =orgo.toml=, which is a build input.
142142- The templates directory, likewise.
143- The output directory, when it lives inside the source — so =org-ssg build . -o _site=
143- The output directory, when it lives inside the source — so =orgo build . -o _site=
144144 does the obvious thing rather than copying its own output back into itself.
145145
146146Note that excluding dot-entries also means =.well-known/= cannot be published.
docs/guide/07-incremental.org +7 −7
@@ -2,7 +2,7 @@
22#+DESCRIPTION: How the cache decides what to re-render, and why that shape is the architecture.
33#+LEDE: Editing one post rebuilds four pages, whatever the size of the site.
44
5Incremental rebuilding is not an optimisation bolted on to org-ssg; it is the constraint
5Incremental rebuilding is not an optimisation bolted on to orgo; it is the constraint
66the data model was built around. Parsing is a pure function of one file's bytes, link
77resolution reports the edges it used, and rendering is a pure function of a resolved
88document. Those properties are what make caching sound — and they are also what make the
@@ -14,13 +14,13 @@ invalidates what explains the behaviour you will see.
1414* What you observe
1515
1616#+BEGIN_EXAMPLE
17$ org-ssg build content -o _site
17$ orgo build content -o _site
1818built 182 page(s) (182 rendered, 0 cached) ...
1919
20$ org-ssg build content -o _site
20$ orgo build content -o _site
2121built 182 page(s) (0 rendered, 182 cached) ...
2222
23$ vim content/blog/post.org && org-ssg build content -o _site
23$ vim content/blog/post.org && orgo build content -o _site
2424built 182 page(s) (4 rendered, 178 cached) ...
2525#+END_EXAMPLE
2626
@@ -35,7 +35,7 @@ Every page has a key composed from four hashes:
3535|-----------+--------------|
3636| content | The source file's bytes change. |
3737| resolved links | A link's target moves, is renamed, or disappears. |
38| config | =org-ssg.toml= changes, or the shared chrome does. |
38| config | =orgo.toml= changes, or the shared chrome does. |
3939| templates | *This page's* layout changes, or something that layout extends or includes. |
4040
4141If a page's key matches the cached one and its output file still exists, the file on disk
@@ -54,7 +54,7 @@ A template whose include is computed at render time — ={% include chooser %}=
5454followed, so it is treated as depending on every template. Over-invalidating costs time;
5555under-invalidating publishes a stale page.
5656
57The cache lives in =<output>/.org-ssg-cache.json= and is tagged with a format version. A
57The cache lives in =<output>/.orgo-cache.json= and is tagged with a format version. A
5858version mismatch, a missing file or a corrupt file all fall back to a full rebuild — the
5959cache is an optimisation, never a correctness dependency. There is a test for each of
6060those three fallbacks.
@@ -124,5 +124,5 @@ correctly on their own. It exists to answer "is the cache lying to me?" — and
124124is, that is a bug worth reporting, with the two builds' output to compare.
125125
126126#+BEGIN_SRC sh
127org-ssg build content -o _site --no-cache
127orgo build content -o _site --no-cache
128128#+END_SRC
docs/guide/08-workflow.org +6 −6
@@ -5,7 +5,7 @@
55* serve
66
77#+BEGIN_SRC sh
8org-ssg serve content -o _site
8orgo serve content -o _site
99#+END_SRC
1010
1111Builds, watches, serves at [[http://127.0.0.1:3000][127.0.0.1:3000]], and reloads the
@@ -43,7 +43,7 @@ the build just failed tells you nothing — the error is already on your termina
4343* watch
4444
4545#+BEGIN_SRC sh
46org-ssg watch content -o _site
46orgo watch content -o _site
4747#+END_SRC
4848
4949The same rebuilding without the server, for when something else already serves the output.
@@ -54,7 +54,7 @@ Rebuilds are driven by OS filesystem events, so nothing happens while nothing ha
5454The rule for what triggers one is deliberately *not* the rule the build uses to find
5555content — the question is "would this change the site?", not "is this a page?".
5656
57*Triggers a rebuild:* any =.org= file, any asset, =org-ssg.toml=, and anything in the
57*Triggers a rebuild:* any =.org= file, any asset, =orgo.toml=, and anything in the
5858templates directory. The last two are skipped by the build when looking for content, but
5959both change the output.
6060
@@ -86,7 +86,7 @@ blog/post.org changed: 2 rendered, 180 cached
8686
8787* Where native watching is unavailable
8888
89Some network and container filesystems have no event API. org-ssg falls back to polling
89Some network and container filesystems have no event API. orgo falls back to polling
9090every two seconds and says so, rather than failing:
9191
9292#+BEGIN_EXAMPLE
@@ -106,10 +106,10 @@ note: native file watching unavailable (...); polling every 2s
106106
107107#+BEGIN_SRC sh
108108# One terminal, left running.
109org-ssg serve content -o _site --drafts
109orgo serve content -o _site --drafts
110110
111111# Write. The browser keeps up.
112112
113113# Before publishing, check what a real build says.
114org-ssg build content -o _site --strict
114orgo build content -o _site --strict
115115#+END_SRC
docs/guide/09-auditing.org +6 −6
@@ -3,7 +3,7 @@
33#+LEDE: Construct frequencies, unknown-name census, and no document text in the output.
44
55#+BEGIN_SRC sh
6org-ssg audit ~/notes
6orgo audit ~/notes
77#+END_SRC
88
99The audit answers two questions about a body of org files:
@@ -11,7 +11,7 @@ The audit answers two questions about a body of org files:
11111. *Coverage.* Of the constructs this corpus uses, which are supported? A construct that
1212 is common here and unsupported is a problem with the tool's scope, not with your
1313 writing.
142. *Blind spots.* Which names appear that org-ssg has no opinion about at all? These are
142. *Blind spots.* Which names appear that orgo has no opinion about at all? These are
1515 the dangerous ones — not "known unsupported" but unknown.
1616
1717* Reading the output
@@ -40,7 +40,7 @@ KEYWORDS
4040- =IN= is supported; =OUT= is excluded by design and degrades as described in
4141 [[file:05-org-support.org][Org support]].
4242- The *coverage* line is the number to look at first.
43- =???= marks a name org-ssg does not recognise at all — in this example =#+SLUG:=, from
43- =???= marks a name orgo does not recognise at all — in this example =#+SLUG:=, from
4444 a run made before it was supported.
4545
4646Four censuses follow the construct table: every distinct =#+KEYWORD:=, block type,
@@ -80,13 +80,13 @@ green run; it simply measures one thing less.
8080
8181#+BEGIN_SRC sh
8282# What is in there?
83org-ssg audit ~/notes
83orgo audit ~/notes
8484
8585# Build it and see what the builder itself complains about.
86org-ssg build ~/notes -o /tmp/preview --strict
86orgo build ~/notes -o /tmp/preview --strict
8787
8888# Look at the result.
89org-ssg serve ~/notes -o /tmp/preview
89orgo serve ~/notes -o /tmp/preview
9090#+END_SRC
9191
9292=--strict= surfaces broken internal links and malformed constructs as failures rather
docs/guide/10-deploying.org +10 −10
@@ -5,7 +5,7 @@
55* The production build
66
77#+BEGIN_SRC sh
8org-ssg build content -o _site --strict
8orgo build content -o _site --strict
99#+END_SRC
1010
1111Two differences from the build you run while writing:
@@ -34,12 +34,12 @@ No trailing slash.
3434
3535* One thing to exclude
3636
37The build writes =.org-ssg-cache.json= into the output directory. It is a dot-file, so
37The build writes =.orgo-cache.json= into the output directory. It is a dot-file, so
3838most static hosts ignore it, but it is not part of the site — exclude it if your host
3939uploads everything:
4040
4141#+BEGIN_SRC sh
42rsync -a --delete --exclude '.org-ssg-cache.json' _site/ user@host:/var/www/site/
42rsync -a --delete --exclude '.orgo-cache.json' _site/ user@host:/var/www/site/
4343#+END_SRC
4444
4545Keeping the cache *between* deploys, where the CI runner can see it, is what makes CI
@@ -57,7 +57,7 @@ jobs:
5757 - uses: actions/checkout@v4
5858 - uses: dtolnay/rust-toolchain@stable
5959 - run: cargo install --path .
60 - run: org-ssg build content -o _site --strict
60 - run: orgo build content -o _site --strict
6161 - uses: actions/upload-artifact@v4
6262 with:
6363 name: site
@@ -69,7 +69,7 @@ failed build.
6969
7070** Caching between runs
7171
72Cache =_site/.org-ssg-cache.json= *and* =_site= together, or not at all. The manifest
72Cache =_site/.orgo-cache.json= *and* =_site= together, or not at all. The manifest
7373describes files it expects to find; a cache without its outputs simply triggers a full
7474rebuild, which is correct but pointless.
7575
@@ -78,16 +78,16 @@ builds is rarely worth the configuration.
7878
7979* Static hosts
8080
81Nothing here is org-ssg-specific; a built site is ordinary static files.
81Nothing here is orgo-specific; a built site is ordinary static files.
8282
8383- *Netlify, Vercel, Cloudflare Pages*: publish directory =_site=, build command
84 =cargo install --path . && org-ssg build content -o _site --strict=.
84 =cargo install --path . && orgo build content -o _site --strict=.
8585- *GitHub Pages*: upload =_site= as the Pages artifact.
8686- *Any web server*: copy =_site= to the document root.
8787
8888** URLs end in .html
8989
90org-ssg writes =blog/post.html= and links to it that way, so the site works with no
90orgo writes =blog/post.html= and links to it that way, so the site works with no
9191server configuration at all — including opening it from a filesystem path.
9292
9393If you prefer extensionless URLs, that is a server-side rewrite, and you should also set
@@ -96,8 +96,8 @@ If you prefer extensionless URLs, that is a server-side rewrite, and you should
9696* Checking a build before shipping
9797
9898#+BEGIN_SRC sh
99org-ssg build content -o _site --strict
100org-ssg serve content -o _site
99orgo build content -o _site --strict
100orgo serve content -o _site
101101#+END_SRC
102102
103103Serving the production build locally is the last check worth doing: it catches a missing
docs/guide/11-versioning.org +6 −6
@@ -11,7 +11,7 @@ Changing any of this incompatibly requires a major version.
1111
1212| Stable | What that covers |
1313|--------+------------------|
14| =org-ssg.toml= keys | Their names, types and meaning. |
14| =orgo.toml= keys | Their names, types and meaning. |
1515| Template context | =page=, =site=, =nav=, =root=, =pages=, =group=, =groups=, =paginator=, =stylesheet=, and the =absolute=, =rfc822= and =truncate= filters. |
1616| The CLI | Command names, flags and exit codes. |
1717| URLs | How a source path becomes an output path, =#+SLUG:= included. |
@@ -28,13 +28,13 @@ Three things move freely, so the list above can hold still.
2828
2929** The incremental cache
3030
31=<output>/.org-ssg-cache.json= is versioned and discards itself on a mismatch. A cache
31=<output>/.orgo-cache.json= is versioned and discards itself on a mismatch. A cache
3232format bump means one full rebuild, and nothing else. It is never a correctness
3333dependency: a missing, stale or corrupt cache produces exactly the same site, more slowly.
3434
3535** Rendered HTML details
3636
37org-ssg aims at what Emacs exports from the same file, and closing a gap changes markup.
37orgo aims at what Emacs exports from the same file, and closing a gap changes markup.
3838That is the product working rather than a regression — but it is called out in the
3939changelog every time, because your stylesheet is downstream of it.
4040
@@ -50,7 +50,7 @@ exists to serve the binary, and its types move as the tool does.
5050* The compiler floor
5151
5252The MSRV is *1.88*, checked in CI on every change rather than assumed — which is how it
53came to be 1.88 rather than the 1.82 org-ssg's own code needs. The floor is set by
53came to be 1.88 rather than the 1.82 orgo's own code needs. The floor is set by
5454dependencies, and a dependency raising its own is invisible until someone on an older
5555compiler tries to build.
5656
@@ -59,8 +59,8 @@ Raising it is a minor version, never a patch.
5959* Upgrading
6060
6161#+BEGIN_SRC sh
62cargo install org-ssg # or download a release binary
63org-ssg build content -o _site --no-cache --strict
62cargo install orgo # or download a release binary
63orgo build content -o _site --no-cache --strict
6464#+END_SRC
6565
6666=--no-cache= makes the first build after an upgrade a full one, so you are comparing the
docs/index.org +6 −6
@@ -1,9 +1,9 @@
1#+TITLE: org-ssg
1#+TITLE: orgo
22#+DESCRIPTION: An org-mode static site generator in Rust, where the org element tree is the document model.
33#+LEDE: Org is the source language, not an inconvenient input to be normalised into markdown.
44#+OPTIONS: toc:nil
55
6org-ssg turns a directory of =.org= files into a static website. It treats org as the
6orgo turns a directory of =.org= files into a static website. It treats org as the
77*source language*: the org element tree — headings, drawers, blocks, links with their
88org-specific semantics — /is/ the document model, and that tree is rendered straight to
99HTML. There is no markdown-shaped intermediate representation, because the point is to
@@ -26,10 +26,10 @@ Open [[http://127.0.0.1:3000][127.0.0.1:3000]], edit any =.org= file, and the br
2626
2727Point it at a directory of org files and you get a complete site: pages, navigation,
2828syntax-highlighted code, and the stylesheet that colours it. Nothing about your files has
29to change, and no =org-ssg.toml= is required.
29to change, and no =orgo.toml= is required.
3030
3131#+BEGIN_SRC sh
32org-ssg build ~/notes -o _site
32orgo build ~/notes -o _site
3333#+END_SRC
3434
3535Configuration changes what you get. It is never what makes it work.
@@ -54,7 +54,7 @@ listed in [[file:guide/05-org-support.org][Org support]].
5454** It tells you what your corpus actually uses
5555
5656#+BEGIN_SRC sh
57org-ssg audit ~/notes
57orgo audit ~/notes
5858#+END_SRC
5959
6060The audit reports which org constructs appear in a corpus, how often, and whether each is
@@ -75,6 +75,6 @@ notes stays safe to paste into an issue.
7575
7676* Status
7777
78This documentation site is itself an org-ssg site — the sources are in =docs/= and it is
78This documentation site is itself an orgo site — the sources are in =docs/= and it is
7979built with the command in [[file:quickstart.org][Quick start]]. If a feature is described here, it is being used
8080to render the page describing it.
docs/install.org +13 −13
@@ -1,5 +1,5 @@
11#+TITLE: Install
2#+DESCRIPTION: Build org-ssg from source, put it on your PATH, and check that it works.
2#+DESCRIPTION: Build orgo from source, put it on your PATH, and check that it works.
33#+LEDE: One Rust toolchain, one command, no runtime dependencies.
44
55* Requirements
@@ -13,15 +13,15 @@
1313* From source
1414
1515#+BEGIN_SRC sh
16git clone <repository-url> org-ssg
17cd org-ssg
16git clone <repository-url> orgo
17cd orgo
1818cargo build --release
1919#+END_SRC
2020
21The binary lands at =target/release/org-ssg=. Copy it somewhere on your =PATH=:
21The binary lands at =target/release/orgo=. Copy it somewhere on your =PATH=:
2222
2323#+BEGIN_SRC sh
24cp target/release/org-ssg ~/.local/bin/
24cp target/release/orgo ~/.local/bin/
2525#+END_SRC
2626
2727Or let cargo do it, which puts it in =~/.cargo/bin=:
@@ -33,7 +33,7 @@ cargo install --path .
3333* Running without installing
3434
3535Every command in this documentation works through cargo if you would rather not install
36anything. Replace =org-ssg= with =cargo run --= and add =--release= for a fast build:
36anything. Replace =orgo= with =cargo run --= and add =--release= for a fast build:
3737
3838#+BEGIN_SRC sh
3939cargo run --release -- build my-site -o _site
@@ -45,9 +45,9 @@ syntax highlighting dominates and is not optimised in a debug profile.
4545* Check that it works
4646
4747#+BEGIN_SRC sh
48org-ssg --version
49org-ssg init /tmp/org-ssg-check
50org-ssg build /tmp/org-ssg-check -o /tmp/org-ssg-check/_site
48orgo --version
49orgo init /tmp/orgo-check
50orgo build /tmp/orgo-check -o /tmp/orgo-check/_site
5151#+END_SRC
5252
5353You should see a line reporting the pages built:
@@ -56,10 +56,10 @@ You should see a line reporting the pages built:
5656built 5 page(s) (5 rendered, 0 cached), copied 0 asset(s) ... (0 unresolved link(s), 0 diagnostic(s))
5757#+END_EXAMPLE
5858
59Open =/tmp/org-ssg-check/_site/index.html= in a browser, or serve it properly:
59Open =/tmp/orgo-check/_site/index.html= in a browser, or serve it properly:
6060
6161#+BEGIN_SRC sh
62org-ssg serve /tmp/org-ssg-check -o /tmp/org-ssg-check/_site
62orgo serve /tmp/orgo-check -o /tmp/orgo-check/_site
6363#+END_SRC
6464
6565* Running the test suite
@@ -82,13 +82,13 @@ still gets a green test run — it simply measures one thing less.
8282
8383* Upgrading
8484
85org-ssg stores an incremental cache in =<output>/.org-ssg-cache.json=, tagged with a
85orgo stores an incremental cache in =<output>/.orgo-cache.json=, tagged with a
8686format version. A newer binary that changes how output is produced bumps that version,
8787and a version it does not recognise is discarded in favour of a full rebuild. You never
8888need to clear the cache by hand after an upgrade — but if you want to:
8989
9090#+BEGIN_SRC sh
91org-ssg clean _site
91orgo clean _site
9292#+END_SRC
9393
9494* Next
docs/org-ssg.toml → docs/orgo.toml renamed +3 −3
@@ -1,10 +1,10 @@
1# Configuration for the org-ssg documentation site.
1# Configuration for the orgo documentation site.
22#
3# This site is built by org-ssg itself, so this file doubles as a worked example: every
3# This site is built by orgo itself, so this file doubles as a worked example: every
44# setting here is one the docs describe, used the way the docs recommend.
55
66[site]
7title = "org-ssg"
7title = "orgo"
88description = "An org-mode static site generator, in Rust."
99language = "en"
1010# Left empty so the docs build with relative URLs and open from the filesystem. Set it to
docs/quickstart.org +19 −19
@@ -5,8 +5,8 @@
55* Two commands
66
77#+BEGIN_SRC sh
8org-ssg init my-site
9org-ssg serve my-site -o _site
8orgo init my-site
9orgo serve my-site -o _site
1010#+END_SRC
1111
1212Open [[http://127.0.0.1:3000][127.0.0.1:3000]]. Edit =my-site/index.org= in your editor, save, and the page reloads
@@ -19,7 +19,7 @@ already has content is safe and additive.
1919
2020#+BEGIN_EXAMPLE
2121my-site/
22 org-ssg.toml every setting, at its default, commented
22 orgo.toml every setting, at its default, commented
2323 index.org the home page
2424 blog/first-post.org a post, to show the collection working
2525 templates/
@@ -35,7 +35,7 @@ You do not need =init=, a config file, or templates. Every command takes the sam
3535paths:
3636
3737#+BEGIN_SRC sh
38org-ssg serve <SOURCE> -o <OUTPUT>
38orgo serve <SOURCE> -o <OUTPUT>
3939#+END_SRC
4040
4141** SOURCE is the URL root
@@ -49,14 +49,14 @@ repository around it:
4949
5050#+BEGIN_SRC sh
5151cd ~/my-site
52org-ssg serve content -o _site # → /blog/post.html
52orgo serve content -o _site # → /blog/post.html
5353#+END_SRC
5454
5555Pointing one level too high still builds, which is what makes it worth saying out loud.
5656It just builds the wrong site:
5757
5858#+BEGIN_SRC sh
59org-ssg serve . -o _site # → /content/blog/post.html
59orgo serve . -o _site # → /content/blog/post.html
6060#+END_SRC
6161
6262Every URL gains a =/content/= prefix, and every non-org file in the repository —
@@ -67,24 +67,24 @@ If you see either symptom, you picked the directory above the one you meant.
6767
6868| Your files | Command |
6969|------------+---------|
70| =~/notes/*.org= | =org-ssg serve ~/notes -o /tmp/notes-site= |
71| =my-site/content/**/*.org= | =cd my-site && org-ssg serve content -o _site= |
72| =my-site/*.org= at the top level | =cd my-site && org-ssg serve . -o _site= |
70| =~/notes/*.org= | =orgo serve ~/notes -o /tmp/notes-site= |
71| =my-site/content/**/*.org= | =cd my-site && orgo serve content -o _site= |
72| =my-site/*.org= at the top level | =cd my-site && orgo serve . -o _site= |
7373| Org files scattered in a code repo | Do not. Copy or symlink the ones you publish into one directory. |
7474
7575** OUTPUT can live inside the source
7676
77=org-ssg serve . -o _site= is fine: the output directory is recognised and skipped, so
77=orgo serve . -o _site= is fine: the output directory is recognised and skipped, so
7878the build never copies its own output back into itself. Nothing dot-prefixed is published
7979either, so =.git= stays out of a site built from a repository root.
8080
8181** Where config and templates go
8282
83Both live in the *source* directory — =SOURCE/org-ssg.toml= and =SOURCE/templates/= — and
83Both live in the *source* directory — =SOURCE/orgo.toml= and =SOURCE/templates/= — and
8484neither is published. If you would rather keep the config elsewhere, name it:
8585
8686#+BEGIN_SRC sh
87org-ssg serve content -o _site --config config/org-ssg.toml
87orgo serve content -o _site --config config/orgo.toml
8888#+END_SRC
8989
9090** A worked example
@@ -95,13 +95,13 @@ A repository laid out as =content/= (org files), =theme/= (unrelated), =build.py
9595cd ~/my-site
9696
9797# What is actually in there, before trusting anything with it.
98org-ssg audit content
98orgo audit content
9999
100100# Build it somewhere disposable and look.
101org-ssg serve content -o /tmp/preview
101orgo serve content -o /tmp/preview
102102
103103# Happy with it? Build for real, failing on broken links.
104org-ssg build content -o _site --strict
104orgo build content -o _site --strict
105105#+END_SRC
106106
107107The audit reports which org constructs appear, how often, and whether each is supported —
@@ -159,7 +159,7 @@ The page is not written, and does not appear in listings or navigation. Preview
159159you work with =--drafts=:
160160
161161#+BEGIN_SRC sh
162org-ssg serve my-site -o _site --drafts
162orgo serve my-site -o _site --drafts
163163#+END_SRC
164164
165165* Change the design
@@ -193,7 +193,7 @@ still looking at it. The full list of variables is in [[file:guide/04-templates.
193193
194194* Add a blog index
195195
196Listing pages have no source file; they are declared in =org-ssg.toml=:
196Listing pages have no source file; they are declared in =orgo.toml=:
197197
198198#+BEGIN_SRC toml
199199[[collections]]
@@ -212,7 +212,7 @@ settings. See [[file:guide/03-collections.org][Collections]].
212212* Build for real
213213
214214#+BEGIN_SRC sh
215org-ssg build my-site -o _site --strict
215orgo build my-site -o _site --strict
216216#+END_SRC
217217
218218=--strict= turns broken internal links and parse diagnostics into a non-zero exit, which
@@ -222,5 +222,5 @@ is what you want in CI. Deployment is just copying =_site= somewhere; see
222222* Next
223223
224224- [[file:guide/01-cli.org][Command reference]] — every command and flag.
225- [[file:guide/02-configuration.org][Configuration]] — every setting in =org-ssg.toml=.
225- [[file:guide/02-configuration.org][Configuration]] — every setting in =orgo.toml=.
226226- [[file:guide/05-org-support.org][Org support]] — exactly which org syntax is handled.
docs/style.css +1 −1
@@ -74,7 +74,7 @@ code {
7474}
7575
7676/* Code blocks are dark in both colour schemes, matching the syntect theme named in
77 org-ssg.toml. One generated stylesheet cannot follow prefers-color-scheme, so the page
77 orgo.toml. One generated stylesheet cannot follow prefers-color-scheme, so the page
7878 commits to the theme's palette instead of leaving highlighted code unreadable in one
7979 of the two. */
8080pre {
docs/templates/base.html +1 −1
@@ -45,7 +45,7 @@
4545{%- endif %}
4646{% block content %}{{ body | safe }}{% endblock %}</main>
4747<footer class="site">
48Built with org-ssg &mdash; these docs are an org-ssg site.
48Built with orgo &mdash; these docs are an orgo site.
4949</footer>
5050</body>
5151</html>
fixtures/site/about.org +1 −1
@@ -1,3 +1,3 @@
11#+TITLE: About
22
3This site is built by org-ssg. Back to [[file:index.org][home]].
3This site is built by orgo. Back to [[file:index.org][home]].
src/config.rs +10 −10
@@ -1,4 +1,4 @@
1//! User-facing build configuration (`org-ssg.toml`).
1//! User-facing build configuration (`orgo.toml`).
22//!
33//! Everything here was once a constant in the source: the page layout, the nav rule, the
44//! highlighting theme. That made the generator produce exactly one kind of site — a
@@ -7,10 +7,10 @@
77//! Two properties matter beyond the settings themselves:
88//!
99//! 1. **Absent config is a valid config.** Every field has a default, so a directory of
10//! `.org` files with no `org-ssg.toml` still builds. Configuration is how you change
10//! `.org` files with no `orgo.toml` still builds. Configuration is how you change
1111//! the output, never how you make it work at all.
1212//! 2. **Config is a hash input** (spec §4.1). [`Config`] serializes deterministically and
13//! its hash is folded into every page's render key, so editing `org-ssg.toml` re-renders
13//! its hash is folded into every page's render key, so editing `orgo.toml` re-renders
1414//! exactly the pages it affects — which for most settings is all of them.
1515
1616use anyhow::{Context, Result};
@@ -18,7 +18,7 @@ use camino::{Utf8Path, Utf8PathBuf};
1818use serde::{Deserialize, Serialize};
1919
2020/// The config file's name, looked for in the source directory.
21pub const CONFIG_FILE: &str = "org-ssg.toml";
21pub const CONFIG_FILE: &str = "orgo.toml";
2222
2323/// Resolved build configuration. Serialized into the config hash, so field order and
2424/// defaults are part of the cache contract.
@@ -319,7 +319,7 @@ pub struct Site {
319319impl Default for Site {
320320 fn default() -> Self {
321321 Site {
322 title: "org-ssg site".to_string(),
322 title: "orgo site".to_string(),
323323 base_url: String::new(),
324324 description: String::new(),
325325 language: "en".to_string(),
@@ -405,7 +405,7 @@ impl Default for Highlight {
405405}
406406
407407impl Config {
408 /// Load `org-ssg.toml` from `dir`, or return defaults if there is none.
408 /// Load `orgo.toml` from `dir`, or return defaults if there is none.
409409 ///
410410 /// A *missing* config is normal and silent. A *malformed* one is an error: someone
411411 /// who wrote a config meant it, and silently building the default site would hide
@@ -533,13 +533,13 @@ impl Config {
533533 }
534534}
535535
536/// The starter config written by `org-ssg init`, and the documentation of record for
536/// The starter config written by `orgo init`, and the documentation of record for
537537/// what is configurable. Every value shown is the default, so deleting any line is safe.
538pub const STARTER_CONFIG: &str = r#"# org-ssg configuration. Every setting here is optional and shown at its default,
538pub const STARTER_CONFIG: &str = r#"# orgo configuration. Every setting here is optional and shown at its default,
539539# so you can delete any line you do not need — or the whole file.
540540
541541[site]
542title = "org-ssg site"
542title = "orgo site"
543543# Absolute base URL, no trailing slash. Needed for feeds and canonical links, which
544544# cannot be relative — set it and uncomment the [[collections]] feed block below.
545545base_url = ""
@@ -574,7 +574,7 @@ expose_page_list = false
574574# A syntect theme name: InspiredGitHub, Solarized (dark), base16-ocean.dark,
575575# base16-eighties.dark, base16-mocha.dark, base16-ocean.light.
576576theme = "InspiredGitHub"
577# Extra .sublime-syntax files for languages neither syntect nor org-ssg bundles.
577# Extra .sublime-syntax files for languages neither syntect nor orgo bundles.
578578syntaxes_dir = "syntaxes"
579579
580580[build]
src/incremental.rs +2 −2
@@ -35,7 +35,7 @@ pub const CACHE_FORMAT_VERSION: u32 = 7;
3535pub type Hash = ContentHash;
3636
3737/// The resolved global build config is [`crate::config::Config`]; its hash is a
38/// component of every page's render key (spec §4.1), so editing `org-ssg.toml`
38/// component of every page's render key (spec §4.1), so editing `orgo.toml`
3939/// invalidates the pages it affects.
4040pub use crate::config::Config as BuildConfig;
4141
@@ -199,7 +199,7 @@ pub struct Manifest {
199199/// The cache-manifest file lives inside the output directory (spec §4.5: an on-disk
200200/// cache dir). `clean` removes the output directory, taking the cache with it.
201201pub fn manifest_path(out: &Utf8Path) -> Utf8PathBuf {
202 out.join(".org-ssg-cache.json")
202 out.join(".orgo-cache.json")
203203}
204204
205205/// Load the manifest, returning `None` on ANY of: missing file, read/parse error, or a
src/lib.rs +1 −1
@@ -1,4 +1,4 @@
1//! org-ssg — an org-mode static site generator.
1//! orgo — an org-mode static site generator.
22//!
33//! Org is the source language, not an input to be normalized into markdown. The org
44//! element tree ([`model`]) *is* the document model; we render it straight to HTML.
src/main.rs +19 −19
@@ -6,15 +6,15 @@ use anyhow::{Context, Result};
66use camino::{Utf8Path, Utf8PathBuf};
77use clap::{Parser, Subcommand};
88
9use org_ssg::parser::parse;
10use org_ssg::config::{self, Config};
11use org_ssg::render::{self, render, Html, SyntectHighlighter};
12use org_ssg::resolve::ResolvedDoc;
13use org_ssg::site::{build_site, BuildOptions, SYNTAX_STYLESHEET};
14use org_ssg::template::{PageContext, RenderContext, SiteContext, Templater};
9use orgo::parser::parse;
10use orgo::config::{self, Config};
11use orgo::render::{self, render, Html, SyntectHighlighter};
12use orgo::resolve::ResolvedDoc;
13use orgo::site::{build_site, BuildOptions, SYNTAX_STYLESHEET};
14use orgo::template::{PageContext, RenderContext, SiteContext, Templater};
1515
1616#[derive(Parser)]
17#[command(name = "org-ssg", version, about = "Org-mode static site generator")]
17#[command(name = "orgo", version, about = "Org-mode static site generator")]
1818struct Cli {
1919 #[command(subcommand)]
2020 command: Command,
@@ -36,7 +36,7 @@ enum Command {
3636 /// Treat broken links and parse diagnostics as errors (spec §4.3.4).
3737 #[arg(long)]
3838 strict: bool,
39 /// Config file to use, overriding `org-ssg.toml` in the source directory.
39 /// Config file to use, overriding `orgo.toml` in the source directory.
4040 #[arg(long, value_name = "FILE")]
4141 config: Option<Utf8PathBuf>,
4242 /// Include pages marked `#+DRAFT:`.
@@ -57,7 +57,7 @@ enum Command {
5757 /// Treat broken links and parse diagnostics as errors.
5858 #[arg(long)]
5959 strict: bool,
60 /// Config file to use, overriding `org-ssg.toml` in the source directory.
60 /// Config file to use, overriding `orgo.toml` in the source directory.
6161 #[arg(long, value_name = "FILE")]
6262 config: Option<Utf8PathBuf>,
6363 /// Include pages marked `#+DRAFT:`. Handy while writing one.
@@ -81,7 +81,7 @@ enum Command {
8181 /// Include pages marked `#+DRAFT:`.
8282 #[arg(long)]
8383 drafts: bool,
84 /// Config file to use, overriding `org-ssg.toml` in the source directory.
84 /// Config file to use, overriding `orgo.toml` in the source directory.
8585 #[arg(long, value_name = "FILE")]
8686 config: Option<Utf8PathBuf>,
8787 },
@@ -151,7 +151,7 @@ fn main() -> Result<()> {
151151 strict,
152152 config,
153153 drafts,
154 } => org_ssg::watch::run(
154 } => orgo::watch::run(
155155 &input,
156156 &output,
157157 &BuildOptions {
@@ -162,8 +162,8 @@ fn main() -> Result<()> {
162162 },
163163 ),
164164 Command::Audit { input } => {
165 let result = org_ssg::audit::audit(&input)?;
166 print!("{}", org_ssg::audit::report(&result));
165 let result = orgo::audit::audit(&input)?;
166 print!("{}", orgo::audit::report(&result));
167167 Ok(())
168168 }
169169 Command::Serve {
@@ -173,7 +173,7 @@ fn main() -> Result<()> {
173173 host,
174174 drafts,
175175 config,
176 } => org_ssg::serve::run(
176 } => orgo::serve::run(
177177 &input,
178178 &output,
179179 &BuildOptions {
@@ -201,8 +201,8 @@ fn main() -> Result<()> {
201201/// Scaffold a working site. Writes only files that do not already exist, so running it
202202/// in a directory that has content is safe and additive rather than destructive.
203203fn init(dir: &Utf8Path) -> Result<()> {
204 use org_ssg::config::{CONFIG_FILE, STARTER_CONFIG};
205 use org_ssg::template::{
204 use orgo::config::{CONFIG_FILE, STARTER_CONFIG};
205 use orgo::template::{
206206 starter_template, STARTER_FEED_TEMPLATE, STARTER_LIST_TEMPLATE, STARTER_TAGS_TEMPLATE,
207207 };
208208
@@ -234,7 +234,7 @@ fn init(dir: &Utf8Path) -> Result<()> {
234234 "#+FILETAGS: :example:\n",
235235 "\n",
236236 "Posts in this directory are collected into /blog/ by the [[collections]] block\n",
237 "in org-ssg.toml, newest first.\n",
237 "in orgo.toml, newest first.\n",
238238 );
239239
240240 let files: [(Utf8PathBuf, &str); 7] = [
@@ -260,7 +260,7 @@ fn init(dir: &Utf8Path) -> Result<()> {
260260 for path in &created {
261261 println!("created {path}");
262262 }
263 println!("\nNext: org-ssg build {dir} -o _site");
263 println!("\nNext: orgo build {dir} -o _site");
264264 Ok(())
265265}
266266
@@ -321,7 +321,7 @@ fn build_file(input: &Utf8Path, output: &Utf8Path) -> Result<()> {
321321 word_count: 0,
322322 reading_time: 0,
323323 keywords: Default::default(),
324 toc: org_ssg::util::table_of_contents(&resolved.document.root),
324 toc: orgo::util::table_of_contents(&resolved.document.root),
325325 };
326326 let mut ctx = RenderContext::new(&site, &page_ctx, &[], SYNTAX_STYLESHEET, "");
327327 ctx.body = &fragment;
src/render.rs +2 −2
@@ -45,7 +45,7 @@ const CLASS_STYLE: ClassStyle = ClassStyle::Spaced;
4545
4646/// Syntax definitions syntect does not bundle, compiled into the binary.
4747///
48/// Both are gaps this project hits on its own first page: every `org-ssg.toml` example is
48/// Both are gaps this project hits on its own first page: every `orgo.toml` example is
4949/// TOML, and a tool for org users is going to be written about in org. Embedding them
5050/// rather than shipping files means they work with no setup, which is the same promise
5151/// the rest of the zero-config path makes.
@@ -928,7 +928,7 @@ fn strip_special_column(table: &crate::model::Table) -> crate::model::Table {
928928
929929/// Split text into alternating prose and LaTeX spans, `(text, is_latex)`.
930930///
931/// org-ssg does not typeset LaTeX — it passes it through for MathJax or a reader's eyes —
931/// orgo does not typeset LaTeX — it passes it through for MathJax or a reader's eyes —
932932/// but it must know where a fragment *is*, because the export-time text conversions would
933933/// otherwise rewrite the mathematics: `x^2` inside `$…$` is not a superscript to be
934934/// marked up, and `--` inside one is a minus sign twice.
src/serve.rs +1 −1
@@ -30,7 +30,7 @@ use crate::site::{build_site, BuildOptions};
3030
3131/// Where the browser subscribes for reload events. Namespaced so it cannot collide with
3232/// a real page.
33pub const RELOAD_PATH: &str = "/__org-ssg/reload";
33pub const RELOAD_PATH: &str = "/__orgo/reload";
3434
3535/// How long a poll waits before answering "nothing yet". Long enough that an idle tab is
3636/// nearly silent, short enough to stay under any proxy or browser idle timeout.
src/site.rs +2 −2
@@ -62,7 +62,7 @@ pub struct BuildOptions {
6262 pub no_cache: bool,
6363 /// Treat broken internal links as a build error rather than a warning (spec §4.3.4).
6464 pub strict: bool,
65 /// Explicit config file, overriding `org-ssg.toml` in the source directory.
65 /// Explicit config file, overriding `orgo.toml` in the source directory.
6666 pub config_path: Option<Utf8PathBuf>,
6767 /// Include pages marked `#+DRAFT:`, overriding `build.drafts` when set.
6868 pub drafts: bool,
@@ -1368,7 +1368,7 @@ fn collect_assets(
13681368/// directory (build input, not content) and the output directory when it lives inside
13691369/// the source.
13701370///
1371/// The output case is not a corner case — `org-ssg build . -o _site` is the obvious
1371/// The output case is not a corner case — `orgo build . -o _site` is the obvious
13721372/// thing to type, and without this the build copies its own output back into itself,
13731373/// growing `_site/_site/_site/…` on every run.
13741374fn excluded_dirs(src: &Utf8Path, config: &Config, out: Option<&Utf8Path>) -> Vec<Utf8PathBuf> {
src/template.rs +5 −5
@@ -375,7 +375,7 @@ impl<'a> RenderContext<'a> {
375375 }
376376}
377377
378/// The starter tag-index template written by `org-ssg init`: shows how `groups` is
378/// The starter tag-index template written by `orgo init`: shows how `groups` is
379379/// iterated, and how a group page is linked.
380380pub const STARTER_TAGS_TEMPLATE: &str = r#"<!DOCTYPE html>
381381<html lang="{{ site.language }}">
@@ -410,7 +410,7 @@ pub const STARTER_TAGS_TEMPLATE: &str = r#"<!DOCTYPE html>
410410</html>
411411"#;
412412
413/// The starter listing template written by `org-ssg init`: a blog index, showing how a
413/// The starter listing template written by `orgo init`: a blog index, showing how a
414414/// collection's `pages` are iterated.
415415pub const STARTER_LIST_TEMPLATE: &str = r#"<!DOCTYPE html>
416416<html lang="{{ site.language }}">
@@ -486,7 +486,7 @@ fn add_filters(env: &mut Environment<'static>, base_url: &str) {
486486 return Err(minijinja::Error::new(
487487 minijinja::ErrorKind::InvalidOperation,
488488 "the `absolute` filter needs site.base_url, which is empty; \
489 set it in org-ssg.toml (e.g. base_url = \"https://example.com\")",
489 set it in orgo.toml (e.g. base_url = \"https://example.com\")",
490490 ));
491491 }
492492 if path.starts_with("http://") || path.starts_with("https://") {
@@ -538,7 +538,7 @@ fn add_filters(env: &mut Environment<'static>, base_url: &str) {
538538 });
539539}
540540
541/// The starter RSS feed written by `org-ssg init`. A listing page with an XML template:
541/// The starter RSS feed written by `orgo init`. A listing page with an XML template:
542542/// no feed-specific machinery, just `absolute` and `rfc822` doing what syndication needs.
543543///
544544/// Emitted commented-out guidance rather than a broken feed when `site.base_url` is
@@ -667,7 +667,7 @@ fn render_error_detail(error: minijinja::Error) -> String {
667667 out
668668}
669669
670/// The starter layout written by `org-ssg init`: the built-in template, on disk, ready
670/// The starter layout written by `orgo init`: the built-in template, on disk, ready
671671/// to edit.
672672pub fn starter_template() -> &'static str {
673673 BASE_TEMPLATE
src/watch.rs +1 −1
@@ -6,7 +6,7 @@
66//!
77//! Two things matter more than the watching itself:
88//!
9//! 1. **Not watching our own output.** `org-ssg watch . -o _site` puts the output inside
9//! 1. **Not watching our own output.** `orgo watch . -o _site` puts the output inside
1010//! the source. Rebuilding writes files, writing files raises events, and events
1111//! trigger a rebuild — a loop that never stops and never idles. [`ChangeFilter`] is
1212//! what prevents it, and it is a pure function precisely so it can be tested without
syntaxes/Org.sublime-syntax +1 −1
@@ -5,7 +5,7 @@
55# needs it.
66#
77# This highlights org as *source text you are reading about*, which is a different job
8# from parsing it: org-ssg's own parser (src/parser.rs) is what turns org into a
8# from parsing it: orgo's own parser (src/parser.rs) is what turns org into a
99# document. Where the two could disagree, this one stays conservative — a highlighter
1010# that colours something wrongly is a cosmetic bug, and one that swallows a line is not.
1111name: Org
syntaxes/TOML.sublime-syntax +1 −1
@@ -1,7 +1,7 @@
11%YAML 1.2
22---
33# TOML, for syntect. Not one of the definitions syntect bundles, and the first thing a
4# config-heavy site needs — every org-ssg.toml example in the documentation is one.
4# config-heavy site needs — every orgo.toml example in the documentation is one.
55#
66# Scope names are the standard TextMate ones, so any syntect theme colours this without
77# knowing it exists.
tests/config.rs +51 −51
@@ -2,7 +2,7 @@
22//! generator for one site or for anyone's.
33//!
44//! The theme running through these tests is that **the zero-config path has to work**.
5//! A directory of `.org` files with no `org-ssg.toml`, no templates and no knowledge of
5//! A directory of `.org` files with no `orgo.toml`, no templates and no knowledge of
66//! this tool must build into a real site; configuration is how you change the output,
77//! never how you make it work at all.
88
@@ -10,15 +10,15 @@ use std::sync::atomic::{AtomicU32, Ordering};
1010
1111use camino::Utf8PathBuf;
1212
13use org_ssg::config::{Config, NavMode};
14use org_ssg::site::{build_site, BuildOptions};
13use orgo::config::{Config, NavMode};
14use orgo::site::{build_site, BuildOptions};
1515
1616fn tmpdir(tag: &str) -> Utf8PathBuf {
1717 static N: AtomicU32 = AtomicU32::new(0);
1818 let n = N.fetch_add(1, Ordering::Relaxed);
1919 let base = Utf8PathBuf::from_path_buf(std::env::temp_dir())
2020 .expect("utf-8 temp dir")
21 .join(format!("org-ssg-cfg-{}-{tag}-{n}", std::process::id()));
21 .join(format!("orgo-cfg-{}-{tag}-{n}", std::process::id()));
2222 let _ = std::fs::remove_dir_all(&base);
2323 std::fs::create_dir_all(&base).unwrap();
2424 base
@@ -36,7 +36,7 @@ fn write_site(src: &Utf8PathBuf) {
3636 .unwrap();
3737}
3838
39fn build(src: &Utf8PathBuf, out: &Utf8PathBuf) -> org_ssg::site::SiteReport {
39fn build(src: &Utf8PathBuf, out: &Utf8PathBuf) -> orgo::site::SiteReport {
4040 build_site(src, out, &BuildOptions::default()).expect("build")
4141}
4242
@@ -81,9 +81,9 @@ fn a_malformed_config_is_an_error_but_a_missing_one_is_not() {
8181
8282 assert_eq!(Config::load(&src).unwrap(), Config::default());
8383
84 std::fs::write(src.join("org-ssg.toml"), "[site\ntitle = broken").unwrap();
84 std::fs::write(src.join("orgo.toml"), "[site\ntitle = broken").unwrap();
8585 let err = Config::load(&src).expect_err("malformed config must fail");
86 assert!(format!("{err:#}").contains("org-ssg.toml"), "names the file: {err:#}");
86 assert!(format!("{err:#}").contains("orgo.toml"), "names the file: {err:#}");
8787}
8888
8989/// A misspelled key is a silent no-op in most config formats, which is exactly how
@@ -93,7 +93,7 @@ fn an_unknown_config_key_is_rejected() {
9393 let root = tmpdir("unknownkey");
9494 let src = root.join("src");
9595 std::fs::create_dir_all(&src).unwrap();
96 std::fs::write(src.join("org-ssg.toml"), "[site]\ntittle = \"typo\"\n").unwrap();
96 std::fs::write(src.join("orgo.toml"), "[site]\ntittle = \"typo\"\n").unwrap();
9797
9898 let err = Config::load(&src).expect_err("unknown key must fail");
9999 assert!(
@@ -126,7 +126,7 @@ fn nav_modes_select_different_pages() {
126126 std::fs::create_dir_all(&src).unwrap();
127127 write_site(&src);
128128 std::fs::write(
129 src.join("org-ssg.toml"),
129 src.join("orgo.toml"),
130130 format!("[nav]\nmode = \"{mode}\"\n"),
131131 )
132132 .unwrap();
@@ -155,7 +155,7 @@ fn explicit_nav_uses_the_configured_order() {
155155 std::fs::create_dir_all(&src).unwrap();
156156 write_site(&src);
157157 std::fs::write(
158 src.join("org-ssg.toml"),
158 src.join("orgo.toml"),
159159 "[nav]\nmode = \"explicit\"\npages = [\"blog/post.org\", \"index.org\"]\n",
160160 )
161161 .unwrap();
@@ -178,7 +178,7 @@ fn explicit_nav_rejects_a_page_that_does_not_exist() {
178178 std::fs::create_dir_all(&src).unwrap();
179179 write_site(&src);
180180 std::fs::write(
181 src.join("org-ssg.toml"),
181 src.join("orgo.toml"),
182182 "[nav]\nmode = \"explicit\"\npages = [\"nope.org\"]\n",
183183 )
184184 .unwrap();
@@ -304,7 +304,7 @@ fn the_page_list_is_opt_in_and_widens_invalidation() {
304304 std::fs::create_dir_all(src.join("templates")).unwrap();
305305 write_site(&src);
306306 std::fs::write(
307 src.join("org-ssg.toml"),
307 src.join("orgo.toml"),
308308 "[templates]\nexpose_page_list = true\n",
309309 )
310310 .unwrap();
@@ -352,7 +352,7 @@ fn heading_offset_shifts_content_headings_below_the_page_title() {
352352 "a level-1 org heading renders as <h2> by default"
353353 );
354354
355 std::fs::write(src.join("org-ssg.toml"), "[html]\nheading_offset = 0\n").unwrap();
355 std::fs::write(src.join("orgo.toml"), "[html]\nheading_offset = 0\n").unwrap();
356356 let out2 = root.join("out2");
357357 build(&src, &out2);
358358 assert!(
@@ -369,7 +369,7 @@ fn an_unknown_highlight_theme_is_rejected_with_the_available_ones() {
369369 let src = root.join("src");
370370 std::fs::create_dir_all(&src).unwrap();
371371 write_site(&src);
372 std::fs::write(src.join("org-ssg.toml"), "[highlight]\ntheme = \"nope\"\n").unwrap();
372 std::fs::write(src.join("orgo.toml"), "[highlight]\ntheme = \"nope\"\n").unwrap();
373373
374374 let err = build_site(&src, &root.join("out"), &BuildOptions::default())
375375 .expect_err("unknown theme must fail");
@@ -385,7 +385,7 @@ fn an_unknown_highlight_theme_is_rejected_with_the_available_ones() {
385385// Discovery
386386// ---------------------------------------------------------------------------
387387
388/// `org-ssg build . -o _site` is the obvious thing to type. Without excluding the output
388/// `orgo build . -o _site` is the obvious thing to type. Without excluding the output
389389/// directory, the build copies its own output back into itself, growing `_site/_site/…`
390390/// on every run.
391391#[test]
@@ -421,7 +421,7 @@ fn dot_directories_and_build_inputs_are_never_published() {
421421 write_site(&src);
422422 std::fs::write(src.join(".git/config"), "[remote]\nurl = private\n").unwrap();
423423 std::fs::write(src.join(".env"), "SECRET=hunter2\n").unwrap();
424 std::fs::write(src.join("org-ssg.toml"), "[site]\ntitle = \"T\"\n").unwrap();
424 std::fs::write(src.join("orgo.toml"), "[site]\ntitle = \"T\"\n").unwrap();
425425 std::fs::write(src.join("templates/base.html"), "<html>{{ body | safe }}</html>").unwrap();
426426 std::fs::write(src.join("style.css"), "body{}\n").unwrap();
427427 let out = root.join("out");
@@ -430,7 +430,7 @@ fn dot_directories_and_build_inputs_are_never_published() {
430430 assert!(!out.join(".git").exists(), ".git must never be published");
431431 assert!(!out.join(".env").exists(), "dotfiles must never be published");
432432 assert!(
433 !out.join("org-ssg.toml").exists(),
433 !out.join("orgo.toml").exists(),
434434 "the config is a build input, not content"
435435 );
436436 assert!(
@@ -472,7 +472,7 @@ fn write_blog(src: &Utf8PathBuf, extra_config: &str) {
472472 )
473473 .unwrap();
474474 std::fs::write(
475 src.join("org-ssg.toml"),
475 src.join("orgo.toml"),
476476 format!(
477477 "[[collections]]\nsource = \"blog\"\noutput = \"blog/index.html\"\n\
478478 template = \"list.html\"\ntitle = \"Blog\"\n{extra_config}"
@@ -758,12 +758,12 @@ fn a_feed_is_just_a_listing_page_with_an_xml_template() {
758758 <pubDate>{{ p.date_iso }}</pubDate></item>{% endfor %}</channel></rss>",
759759 )
760760 .unwrap();
761 let mut config = std::fs::read_to_string(src.join("org-ssg.toml")).unwrap();
761 let mut config = std::fs::read_to_string(src.join("orgo.toml")).unwrap();
762762 config.push_str(
763763 "\n[[collections]]\nsource = \"blog\"\noutput = \"feed.xml\"\n\
764764 template = \"feed.xml\"\ntitle = \"Feed\"\n",
765765 );
766 std::fs::write(src.join("org-ssg.toml"), config).unwrap();
766 std::fs::write(src.join("orgo.toml"), config).unwrap();
767767 let out = root.join("out");
768768 build(&src, &out);
769769
@@ -833,16 +833,16 @@ fn colliding_collection_outputs_are_rejected() {
833833 std::fs::create_dir_all(&src).unwrap();
834834 write_blog(&src, "");
835835
836 let mut config = std::fs::read_to_string(src.join("org-ssg.toml")).unwrap();
836 let mut config = std::fs::read_to_string(src.join("orgo.toml")).unwrap();
837837 config.push_str("\n[[collections]]\nsource = \"\"\noutput = \"blog/index.html\"\n");
838 std::fs::write(src.join("org-ssg.toml"), &config).unwrap();
838 std::fs::write(src.join("orgo.toml"), &config).unwrap();
839839 let err = build_site(&src, &root.join("out"), &BuildOptions::default())
840840 .expect_err("two collections writing one file must fail");
841841 assert!(format!("{err:#}").contains("blog/index.html"), "{err:#}");
842842
843843 // And a listing that would overwrite a real page.
844844 std::fs::write(
845 src.join("org-ssg.toml"),
845 src.join("orgo.toml"),
846846 "[[collections]]\nsource = \"blog\"\noutput = \"index.html\"\ntemplate = \"list.html\"\n",
847847 )
848848 .unwrap();
@@ -859,7 +859,7 @@ fn a_missing_collection_template_names_the_ones_that_exist() {
859859 std::fs::create_dir_all(&src).unwrap();
860860 write_blog(&src, "");
861861 std::fs::write(
862 src.join("org-ssg.toml"),
862 src.join("orgo.toml"),
863863 "[[collections]]\nsource = \"blog\"\noutput = \"blog/index.html\"\ntemplate = \"nope.html\"\n",
864864 )
865865 .unwrap();
@@ -912,7 +912,7 @@ fn write_tagged_blog(src: &Utf8PathBuf, extra: &str) {
912912 )
913913 .unwrap();
914914 std::fs::write(
915 src.join("org-ssg.toml"),
915 src.join("orgo.toml"),
916916 format!(
917917 "[[collections]]\nsource = \"blog\"\ngroup_by = \"tags\"\n\
918918 output = \"tags/{{tag}}.html\"\ntemplate = \"tag.html\"\ntitle = \"Tagged: {{tag}}\"\n\
@@ -1044,7 +1044,7 @@ fn a_collection_can_group_by_any_keyword() {
10441044 )
10451045 .unwrap();
10461046 std::fs::write(
1047 src.join("org-ssg.toml"),
1047 src.join("orgo.toml"),
10481048 "[[collections]]\nsource = \"blog\"\ngroup_by = \"category\"\n\
10491049 output = \"cat/{tag}.html\"\ntemplate = \"tag.html\"\ntitle = \"{tag}\"\n",
10501050 )
@@ -1081,7 +1081,7 @@ fn grouping_without_a_placeholder_is_rejected() {
10811081 std::fs::create_dir_all(&src).unwrap();
10821082 write_tagged_blog(&src, "");
10831083 std::fs::write(
1084 src.join("org-ssg.toml"),
1084 src.join("orgo.toml"),
10851085 "[[collections]]\nsource = \"blog\"\ngroup_by = \"tags\"\n\
10861086 output = \"tags/all.html\"\ntemplate = \"tag.html\"\n",
10871087 )
@@ -1149,7 +1149,7 @@ fn write_paginated_blog(src: &Utf8PathBuf, count: usize, extra: &str) {
11491149 )
11501150 .unwrap();
11511151 std::fs::write(
1152 src.join("org-ssg.toml"),
1152 src.join("orgo.toml"),
11531153 format!(
11541154 "[[collections]]\nsource = \"blog\"\noutput = \"blog/index.html\"\n\
11551155 template = \"list.html\"\ntitle = \"Blog\"\n{extra}"
@@ -1275,7 +1275,7 @@ fn groups_paginate_independently() {
12751275 .unwrap();
12761276 }
12771277 std::fs::write(
1278 src.join("org-ssg.toml"),
1278 src.join("orgo.toml"),
12791279 "[[collections]]\nsource = \"blog\"\ngroup_by = \"tags\"\n\
12801280 output = \"tags/{tag}.html\"\ntemplate = \"list.html\"\ntitle = \"{tag}\"\n\
12811281 paginate = 2\npaginate_output = \"tags/{tag}/page/{n}.html\"\n",
@@ -1317,7 +1317,7 @@ fn pagination_placeholders_are_validated() {
13171317
13181318 // Grouped without {tag} in the page pattern.
13191319 std::fs::write(
1320 src.join("org-ssg.toml"),
1320 src.join("orgo.toml"),
13211321 "[[collections]]\nsource = \"blog\"\ngroup_by = \"tags\"\n\
13221322 output = \"tags/{tag}.html\"\ntemplate = \"list.html\"\n\
13231323 paginate = 2\npaginate_output = \"tags/page/{n}.html\"\n",
@@ -1380,7 +1380,7 @@ fn write_feed_site(src: &Utf8PathBuf, base_url: &str) {
13801380 )
13811381 .unwrap();
13821382 std::fs::write(
1383 src.join("org-ssg.toml"),
1383 src.join("orgo.toml"),
13841384 format!(
13851385 "[site]\nbase_url = \"{base_url}\"\n\n\
13861386 [[collections]]\nsource = \"blog\"\noutput = \"feed.xml\"\n\
@@ -1442,7 +1442,7 @@ fn absolute_without_a_base_url_is_an_error_that_says_what_to_set() {
14421442 .expect_err("absolute with no base_url must fail");
14431443 let message = format!("{err:#}");
14441444 assert!(message.contains("base_url"), "names the setting: {message}");
1445 assert!(message.contains("org-ssg.toml"), "names where to set it: {message}");
1445 assert!(message.contains("orgo.toml"), "names where to set it: {message}");
14461446 assert!(message.contains("feed.xml"), "names the template: {message}");
14471447}
14481448
@@ -1484,7 +1484,7 @@ fn the_default_layout_emits_a_canonical_link_only_with_a_base_url() {
14841484 std::fs::create_dir_all(&src).unwrap();
14851485 write_site(&src);
14861486 std::fs::write(
1487 src.join("org-ssg.toml"),
1487 src.join("orgo.toml"),
14881488 format!("[site]\nbase_url = \"{base}\"\n"),
14891489 )
14901490 .unwrap();
@@ -1509,7 +1509,7 @@ fn changing_base_url_re_renders_the_site() {
15091509 std::fs::create_dir_all(&src).unwrap();
15101510 write_site(&src);
15111511 std::fs::write(
1512 src.join("org-ssg.toml"),
1512 src.join("orgo.toml"),
15131513 "[site]\nbase_url = \"https://example.com\"\n",
15141514 )
15151515 .unwrap();
@@ -1518,7 +1518,7 @@ fn changing_base_url_re_renders_the_site() {
15181518 assert!(build(&src, &out).rendered.is_empty(), "unchanged rebuild renders nothing");
15191519
15201520 std::fs::write(
1521 src.join("org-ssg.toml"),
1521 src.join("orgo.toml"),
15221522 "[site]\nbase_url = \"https://moved.example\"\n",
15231523 )
15241524 .unwrap();
@@ -1556,7 +1556,7 @@ fn write_excerpt_site(src: &Utf8PathBuf, extra_config: &str) {
15561556 )
15571557 .unwrap();
15581558 std::fs::write(
1559 src.join("org-ssg.toml"),
1559 src.join("orgo.toml"),
15601560 format!(
15611561 "[[collections]]\nsource = \"blog\"\noutput = \"blog/index.html\"\n\
15621562 template = \"list.html\"\ntitle = \"Blog\"\n{extra_config}"
@@ -1731,9 +1731,9 @@ fn a_link_to_a_draft_is_reported_as_broken() {
17311731/// "no" has to mean no.
17321732#[test]
17331733fn draft_truthiness_is_forgiving_but_respects_an_explicit_negative() {
1734 use org_ssg::model::Keywords;
1734 use orgo::model::Keywords;
17351735 let draft = |value: &str| {
1736 org_ssg::util::is_draft(&Keywords {
1736 orgo::util::is_draft(&Keywords {
17371737 entries: vec![("DRAFT".to_string(), value.to_string())],
17381738 })
17391739 };
@@ -1744,7 +1744,7 @@ fn draft_truthiness_is_forgiving_but_respects_an_explicit_negative() {
17441744 assert!(!draft(no), "{no:?} should mean published");
17451745 }
17461746 assert!(
1747 !org_ssg::util::is_draft(&Keywords::default()),
1747 !orgo::util::is_draft(&Keywords::default()),
17481748 "no keyword at all means published"
17491749 );
17501750}
@@ -1773,7 +1773,7 @@ fn write_toc_site(src: &Utf8PathBuf, options: &str, config: &str) {
17731773 <nav>{{ walk(page.toc) }}</nav>{{ body | safe }}</body></html>",
17741774 )
17751775 .unwrap();
1776 std::fs::write(src.join("org-ssg.toml"), config).unwrap();
1776 std::fs::write(src.join("orgo.toml"), config).unwrap();
17771777}
17781778
17791779/// A table of contents is a tree, and reconstructing one from a flat list of levels
@@ -1889,7 +1889,7 @@ fn section_numbering_resets_at_each_level() {
18891889 * One\n** A\n** B\n* Two\n** C\n*** Deep\n* Three\n",
18901890 )
18911891 .unwrap();
1892 std::fs::write(src.join("org-ssg.toml"), "").unwrap();
1892 std::fs::write(src.join("orgo.toml"), "").unwrap();
18931893 let out = root.join("out");
18941894 build(&src, &out);
18951895
@@ -1914,8 +1914,8 @@ fn section_numbering_resets_at_each_level() {
19141914/// `#+OPTIONS:` is a space-separated list of switches, and org spells "off" several ways.
19151915#[test]
19161916fn export_options_parse_as_org_writes_them() {
1917 use org_ssg::model::Keywords;
1918 use org_ssg::util::option_enabled;
1917 use orgo::model::Keywords;
1918 use orgo::util::option_enabled;
19191919 let keywords = |v: &str| Keywords {
19201920 entries: vec![("OPTIONS".to_string(), v.to_string())],
19211921 };
@@ -1955,7 +1955,7 @@ fn write_two_layouts(src: &Utf8PathBuf, config: &str) {
19551955 <p>Reply by email</p></body></html>",
19561956 )
19571957 .unwrap();
1958 std::fs::write(src.join("org-ssg.toml"), config).unwrap();
1958 std::fs::write(src.join("orgo.toml"), config).unwrap();
19591959}
19601960
19611961/// A section's layout is a property of the section: one rule covers every page under it,
@@ -2124,7 +2124,7 @@ fn adding_a_pages_rule_rerenders_the_pages_it_covers() {
21242124 build(&src, &out);
21252125
21262126 std::fs::write(
2127 src.join("org-ssg.toml"),
2127 src.join("orgo.toml"),
21282128 "[[pages]]\nmatch = \"blog\"\ntemplate = \"post.html\"\n",
21292129 )
21302130 .unwrap();
@@ -2142,7 +2142,7 @@ fn adding_a_pages_rule_rerenders_the_pages_it_covers() {
21422142#[test]
21432143fn a_pages_rule_without_a_template_is_rejected() {
21442144 let mut config = Config::default();
2145 config.pages.push(org_ssg::config::PageRule {
2145 config.pages.push(orgo::config::PageRule {
21462146 pattern: Utf8PathBuf::from("blog"),
21472147 template: String::new(),
21482148 });
@@ -2211,7 +2211,7 @@ fn same_day_entries_sort_by_time_of_day() {
22112211 )
22122212 .unwrap();
22132213 std::fs::write(
2214 src.join("org-ssg.toml"),
2214 src.join("orgo.toml"),
22152215 "[[collections]]\nsource = \"blog\"\noutput = \"blog/index.html\"\n\
22162216 template = \"list.html\"\ntitle = \"Blog\"\nsort = \"date\"\norder = \"desc\"\n",
22172217 )
@@ -2249,7 +2249,7 @@ fn an_asset_root_publishes_to_the_site_root() {
22492249 std::fs::write(root.join("theme/static/robots.txt"), "User-agent: *\n").unwrap();
22502250 std::fs::write(root.join("theme/static/img/logo.svg"), "<svg/>").unwrap();
22512251 std::fs::write(
2252 src.join("org-ssg.toml"),
2252 src.join("orgo.toml"),
22532253 "[build]\nassets = [\"../theme/static\"]\n",
22542254 )
22552255 .unwrap();
@@ -2280,7 +2280,7 @@ fn two_assets_claiming_one_url_is_an_error() {
22802280 std::fs::create_dir_all(root.join("static")).unwrap();
22812281 std::fs::write(root.join("static/style.css"), "body{color:red}").unwrap();
22822282 std::fs::write(
2283 src.join("org-ssg.toml"),
2283 src.join("orgo.toml"),
22842284 "[build]\nassets = [\"../static\"]\n",
22852285 )
22862286 .unwrap();
@@ -2301,7 +2301,7 @@ fn a_missing_asset_root_is_an_error() {
23012301 std::fs::create_dir_all(&src).unwrap();
23022302 write_site(&src);
23032303 std::fs::write(
2304 src.join("org-ssg.toml"),
2304 src.join("orgo.toml"),
23052305 "[build]\nassets = [\"../nope\"]\n",
23062306 )
23072307 .unwrap();
@@ -2330,7 +2330,7 @@ fn a_collection_can_carry_its_entries_rendered_bodies() {
23302330 )
23312331 .unwrap();
23322332 std::fs::write(
2333 src.join("org-ssg.toml"),
2333 src.join("orgo.toml"),
23342334 "[[collections]]\nsource = \"blog\"\noutput = \"feed.xml\"\n\
23352335 template = \"feed.xml\"\ntitle = \"Feed\"\ninclude_content = true\n",
23362336 )
tests/constructs.rs +6 −6
@@ -13,10 +13,10 @@
1313
1414use camino::Utf8PathBuf;
1515
16use org_ssg::model::Document;
17use org_ssg::parser::parse;
18use org_ssg::render::{render, Html, SyntectHighlighter};
19use org_ssg::resolve::ResolvedDoc;
16use orgo::model::Document;
17use orgo::parser::parse;
18use orgo::render::{render, Html, SyntectHighlighter};
19use orgo::resolve::ResolvedDoc;
2020
2121fn parse_fixture(name: &str) -> Document {
2222 let path = Utf8PathBuf::from(env!("CARGO_MANIFEST_DIR"))
@@ -160,7 +160,7 @@ fn highlighting_emits_classes_not_inline_styles() {
160160 "highlighting must not emit inline styles:\n{html}"
161161 );
162162 assert!(
163 org_ssg::render::syntax_css("InspiredGitHub")
163 orgo::render::syntax_css("InspiredGitHub")
164164 .expect("a built-in theme")
165165 .contains(".storage"),
166166 "the generated stylesheet must define the emitted classes"
@@ -559,7 +559,7 @@ fn special_strings_leave_code_alone() {
559559 );
560560}
561561
562/// `#+OPTIONS: -:nil` is how a document opts out, and org-ssg honours org's own switch
562/// `#+OPTIONS: -:nil` is how a document opts out, and orgo honours org's own switch
563563/// rather than inventing one.
564564#[test]
565565fn a_document_can_turn_special_strings_off() {
tests/harness.rs +2 −2
@@ -5,8 +5,8 @@
55//! - an `insta` JSON snapshot of a hand-built element-tree value, standing in for the
66//! element-tree snapshots the parser will produce from Phase 1 onward.
77
8use org_ssg::model::{Link, LinkTarget, Object};
9use org_ssg::parser::content_hash;
8use orgo::model::{Link, LinkTarget, Object};
9use orgo::parser::content_hash;
1010
1111#[test]
1212fn content_hash_is_deterministic_blake3() {
tests/incremental.rs +5 −5
@@ -16,8 +16,8 @@ use std::sync::atomic::{AtomicU32, Ordering};
1616
1717use camino::Utf8PathBuf;
1818
19use org_ssg::incremental::{manifest_path, Manifest, CACHE_FORMAT_VERSION};
20use org_ssg::site::{build_site, BuildOptions};
19use orgo::incremental::{manifest_path, Manifest, CACHE_FORMAT_VERSION};
20use orgo::site::{build_site, BuildOptions};
2121
2222/// A fresh, empty temp directory unique to this process + call.
2323fn tmpdir(tag: &str) -> Utf8PathBuf {
@@ -25,7 +25,7 @@ fn tmpdir(tag: &str) -> Utf8PathBuf {
2525 let n = N.fetch_add(1, Ordering::Relaxed);
2626 let base = Utf8PathBuf::from_path_buf(std::env::temp_dir())
2727 .expect("utf-8 temp dir")
28 .join(format!("org-ssg-it-{}-{tag}-{n}", std::process::id()));
28 .join(format!("orgo-it-{}-{tag}-{n}", std::process::id()));
2929 if base.exists() {
3030 std::fs::remove_dir_all(&base).unwrap();
3131 }
@@ -47,7 +47,7 @@ fn output_files(out: &Utf8PathBuf) -> BTreeMap<String, Vec<u8>> {
4747 continue;
4848 }
4949 let path = Utf8PathBuf::from_path_buf(entry.path().to_owned()).unwrap();
50 if path.file_name() == Some(".org-ssg-cache.json") {
50 if path.file_name() == Some(".orgo-cache.json") {
5151 continue;
5252 }
5353 let rel = path.strip_prefix(out).unwrap().to_string();
@@ -476,7 +476,7 @@ fn editing_one_template_rebuilds_only_the_pages_that_use_it() {
476476 )
477477 .unwrap();
478478 std::fs::write(
479 src.join("org-ssg.toml"),
479 src.join("orgo.toml"),
480480 "[[pages]]\nmatch = \"blog\"\ntemplate = \"post.html\"\n",
481481 )
482482 .unwrap();
tests/oracle.el +3 −3
@@ -1,4 +1,4 @@
1;;; oracle.el --- ground-truth HTML export for the org-ssg differential tests -*- lexical-binding: t -*-
1;;; oracle.el --- ground-truth HTML export for the orgo differential tests -*- lexical-binding: t -*-
22
33;; Exports the org file named by $ORG_ORACLE_INPUT to HTML on stdout, using org's own
44;; exporter — the same one weblorg wraps to publish the corpus this project targets.
@@ -16,7 +16,7 @@
1616;; learn what stock org does — normalizing that away would be marking our own homework.
1717(setq org-export-with-toc nil ; we emit no table of contents
1818 org-export-with-section-numbers nil ; we do not number headings
19 ;; org-html-toplevel-hlevel is left at its default of 2. org-ssg's own default
19 ;; org-html-toplevel-hlevel is left at its default of 2. orgo's own default
2020 ;; heading_offset is 1, which produces the same <h2>, so both sides now agree
2121 ;; without the oracle being told to.
2222 org-html-htmlize-output-type nil ; plain <pre>, not htmlize spans: we highlight
@@ -24,7 +24,7 @@
2424 ; the meaningful part
2525 org-html-head-include-default-style nil
2626 org-html-head-include-scripts nil
27 ;; Fixtures link to ids that live in org-ssg's own symbol table, not in an
27 ;; Fixtures link to ids that live in orgo's own symbol table, not in an
2828 ;; `org-id' database. Without this, org aborts the whole export on the first one.
2929 org-export-with-broken-links t
3030 make-backup-files nil)
tests/oracle.rs +12 −12
@@ -1,6 +1,6 @@
11//! The `emacs --batch` ground-truth oracle (spec §5, Phase 0).
22//!
3//! Every other test in this suite checks org-ssg against org-ssg: a snapshot says our
3//! Every other test in this suite checks orgo against orgo: a snapshot says our
44//! output has not *changed*, never that it is *right*. Those two questions are different,
55//! and only one of them matters to someone whose site is currently published by Emacs.
66//! This file answers the second by exporting the same fixture with org's own HTML
@@ -26,9 +26,9 @@ use std::process::Command;
2626
2727use camino::Utf8PathBuf;
2828
29use org_ssg::parser::parse;
30use org_ssg::render::{render, Html, SyntectHighlighter};
31use org_ssg::resolve::ResolvedDoc;
29use orgo::parser::parse;
30use orgo::render::{render, Html, SyntectHighlighter};
31use orgo::resolve::ResolvedDoc;
3232
3333fn manifest_dir() -> Utf8PathBuf {
3434 Utf8PathBuf::from(env!("CARGO_MANIFEST_DIR"))
@@ -62,7 +62,7 @@ fn org_export(fixture: &str) -> String {
6262 String::from_utf8(output.stdout).expect("emacs emits UTF-8")
6363}
6464
65/// Render a fixture with org-ssg.
65/// Render a fixture with orgo.
6666fn our_export(fixture: &str) -> String {
6767 let path = manifest_dir().join("fixtures").join(fixture);
6868 let source = std::fs::read_to_string(&path).expect("read fixture");
@@ -252,12 +252,12 @@ fn decode_entities(s: &str) -> String {
252252// Divergence report
253253// ---------------------------------------------------------------------------
254254
255/// One divergence org-ssg makes on purpose, so the report can separate "we chose this"
255/// One divergence orgo makes on purpose, so the report can separate "we chose this"
256256/// from "we got this wrong".
257257///
258258/// Without this split the agreement percentage is noise: the timestamps fixture sat at
259259/// 40% while being entirely correct, because org writes `<2024-01-15 Mon>` as text and
260/// org-ssg writes a `<time datetime>` element. A number that cannot fall when a real
260/// orgo writes a `<time datetime>` element. A number that cannot fall when a real
261261/// defect appears is not measuring anything.
262262struct Deliberate {
263263 name: &'static str,
@@ -335,7 +335,7 @@ const DELIBERATE: &[Deliberate] = &[
335335 },
336336 in_notes_only: false,
337337 },
338 // Org emits a `<colgroup>` of empty `<col>`s to carry column alignment; org-ssg
338 // Org emits a `<colgroup>` of empty `<col>`s to carry column alignment; orgo
339339 // leaves alignment to the stylesheet.
340340 Deliberate {
341341 name: "no-colgroup",
@@ -511,7 +511,7 @@ fn align(ours: &[String], theirs: &[String]) -> Vec<Op> {
511511}
512512
513513/// A unified diff of the two skeletons, with hunks that are deliberate collapsed to a
514/// named line. `-` is org-ssg, `+` is Emacs.
514/// named line. `-` is orgo, `+` is Emacs.
515515///
516516/// The number that matters is the last one: *unexplained* lines. Agreement can be low
517517/// while unexplained is zero, and that is a passing state.
@@ -566,7 +566,7 @@ fn divergence(ours: &[String], theirs: &[String]) -> String {
566566 "agreement: {agreed}/{total} skeleton lines ({pct:.1}%)\n\
567567 deliberate: {deliberate} line(s){}\n\
568568 unexplained: {unexplained} line(s)\n\
569 (- org-ssg, + emacs, ~ a difference we mean to have)\n\n{body}",
569 (- orgo, + emacs, ~ a difference we mean to have)\n\n{body}",
570570 if rules.is_empty() {
571571 String::new()
572572 } else {
@@ -575,7 +575,7 @@ fn divergence(ours: &[String], theirs: &[String]) -> String {
575575 )
576576}
577577
578/// Snapshot the divergence between org-ssg and Emacs for one fixture.
578/// Snapshot the divergence between orgo and Emacs for one fixture.
579579fn compare(fixture: &str) -> Option<String> {
580580 if !emacs_available() {
581581 eprintln!("skipping oracle comparison for {fixture}: no emacs on PATH");
@@ -612,7 +612,7 @@ oracle_test!(oracle_elements, "elements.org");
612612///
613613/// The percentages above are context, not a target — the timestamps fixture agrees on
614614/// 40% of its lines and is entirely correct, because org writes a date as text where
615/// org-ssg writes `<time datetime>`. What must hold is that nothing diverges for a
615/// orgo writes `<time datetime>`. What must hold is that nothing diverges for a
616616/// reason nobody has written down. A new unexplained line means either a defect to fix
617617/// or a decision to record in `DELIBERATE`.
618618#[test]
tests/pipeline.rs +4 −4
@@ -6,10 +6,10 @@
66
77use camino::Utf8PathBuf;
88
9use org_ssg::model::Document;
10use org_ssg::parser::parse;
11use org_ssg::render::{render, Html, SyntectHighlighter};
12use org_ssg::resolve::ResolvedDoc;
9use orgo::model::Document;
10use orgo::parser::parse;
11use orgo::render::{render, Html, SyntectHighlighter};
12use orgo::resolve::ResolvedDoc;
1313
1414fn parse_fixture(name: &str) -> Document {
1515 let path = Utf8PathBuf::from(env!("CARGO_MANIFEST_DIR"))
tests/serve.rs +11 −11
@@ -12,15 +12,15 @@ use std::time::{Duration, Instant};
1212
1313use camino::{Utf8Path, Utf8PathBuf};
1414
15use org_ssg::serve::{inject_reload_script, resolve, since_parameter};
16use org_ssg::site::BuildOptions;
15use orgo::serve::{inject_reload_script, resolve, since_parameter};
16use orgo::site::BuildOptions;
1717
1818fn tmpdir(tag: &str) -> Utf8PathBuf {
1919 static N: AtomicU32 = AtomicU32::new(0);
2020 let n = N.fetch_add(1, Ordering::Relaxed);
2121 let base = Utf8PathBuf::from_path_buf(std::env::temp_dir())
2222 .expect("utf-8 temp dir")
23 .join(format!("org-ssg-serve-{}-{tag}-{n}", std::process::id()));
23 .join(format!("orgo-serve-{}-{tag}-{n}", std::process::id()));
2424 let _ = std::fs::remove_dir_all(&base);
2525 std::fs::create_dir_all(&base).unwrap();
2626 base
@@ -107,10 +107,10 @@ fn plus_is_not_decoded_as_a_space() {
107107
108108#[test]
109109fn the_poll_parameter_is_read_from_the_query() {
110 assert_eq!(since_parameter("/__org-ssg/reload?since=7"), 7);
111 assert_eq!(since_parameter("/__org-ssg/reload?x=1&since=42"), 42);
112 assert_eq!(since_parameter("/__org-ssg/reload"), 0, "absent means start from zero");
113 assert_eq!(since_parameter("/__org-ssg/reload?since=nope"), 0, "unparseable means zero");
110 assert_eq!(since_parameter("/__orgo/reload?since=7"), 7);
111 assert_eq!(since_parameter("/__orgo/reload?x=1&since=42"), 42);
112 assert_eq!(since_parameter("/__orgo/reload"), 0, "absent means start from zero");
113 assert_eq!(since_parameter("/__orgo/reload?since=nope"), 0, "unparseable means zero");
114114}
115115
116116// ---------------------------------------------------------------------------
@@ -167,7 +167,7 @@ fn start_server(src: &Utf8Path, out: &Utf8Path) -> u16 {
167167 let port = 20000 + ((std::process::id() % 10000) as u16) + NEXT.fetch_add(1, Ordering::Relaxed) as u16;
168168 let (s, o) = (src.to_owned(), out.to_owned());
169169 std::thread::spawn(move || {
170 let _ = org_ssg::serve::run(&s, &o, &BuildOptions::default(), "127.0.0.1", port);
170 let _ = orgo::serve::run(&s, &o, &BuildOptions::default(), "127.0.0.1", port);
171171 });
172172 let deadline = Instant::now() + Duration::from_secs(20);
173173 while Instant::now() < deadline {
@@ -194,16 +194,16 @@ fn serving_a_site_reloads_the_browser_when_a_source_changes() {
194194 let home = get(port, "/", Duration::from_secs(5)).expect("a response");
195195 assert!(home.contains("200 OK"), "{home}");
196196 assert!(home.contains("First version."), "the page is served: {home}");
197 assert!(home.contains("__org-ssg/reload"), "with the reload script: {home}");
197 assert!(home.contains("__orgo/reload"), "with the reload script: {home}");
198198 assert!(
199 !std::fs::read_to_string(out.join("index.html")).unwrap().contains("__org-ssg"),
199 !std::fs::read_to_string(out.join("index.html")).unwrap().contains("__orgo"),
200200 "but the file on disk stays clean"
201201 );
202202
203203 // A poll for a generation we already have must block, not answer immediately.
204204 let poller = std::thread::spawn(move || {
205205 let started = Instant::now();
206 let body = get(port, "/__org-ssg/reload?since=0", Duration::from_secs(30));
206 let body = get(port, "/__orgo/reload?since=0", Duration::from_secs(30));
207207 (started.elapsed(), body)
208208 });
209209 std::thread::sleep(Duration::from_millis(400));
tests/site.rs +8 −8
@@ -6,11 +6,11 @@
66
77use camino::Utf8PathBuf;
88
9use org_ssg::index::{SymbolTable, TargetId};
10use org_ssg::parser::parse;
11use org_ssg::render::{render, Html, SyntectHighlighter};
12use org_ssg::resolve::{resolve, ResolvedDoc};
13use org_ssg::site::{render_site, BuiltPage};
9use orgo::index::{SymbolTable, TargetId};
10use orgo::parser::parse;
11use orgo::render::{render, Html, SyntectHighlighter};
12use orgo::resolve::{resolve, ResolvedDoc};
13use orgo::site::{render_site, BuiltPage};
1414
1515fn fixtures() -> Utf8PathBuf {
1616 Utf8PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures")
@@ -169,13 +169,13 @@ fn links_resolve_through_the_slug() {
169169/// be impossible by construction rather than by convention.
170170#[test]
171171fn slugs_cannot_escape_the_output_directory() {
172 use org_ssg::model::Keywords;
172 use orgo::model::Keywords;
173173 let source = Utf8PathBuf::from("blog/post.org");
174174 let slugged = |value: &str| {
175175 let keywords = Keywords {
176176 entries: vec![("SLUG".to_string(), value.to_string())],
177177 };
178 org_ssg::util::output_path(&source, &keywords).to_string()
178 orgo::util::output_path(&source, &keywords).to_string()
179179 };
180180 assert_eq!(slugged("../../etc/passwd"), "blog/etc-passwd.html");
181181 assert_eq!(slugged("/absolute"), "blog/absolute.html");
@@ -190,7 +190,7 @@ fn slugs_cannot_escape_the_output_directory() {
190190/// invisible in the source filenames, so the build refuses rather than picking a winner.
191191#[test]
192192fn colliding_slugs_are_a_build_error() {
193 let dir = std::env::temp_dir().join(format!("org-ssg-slug-{}", std::process::id()));
193 let dir = std::env::temp_dir().join(format!("orgo-slug-{}", std::process::id()));
194194 let dir = Utf8PathBuf::from_path_buf(dir).expect("utf-8 temp dir");
195195 let _ = std::fs::remove_dir_all(&dir);
196196 std::fs::create_dir_all(&dir).unwrap();
tests/snapshots/oracle__oracle_blocks.snap +1 −1
@@ -5,7 +5,7 @@ expression: report
55agreement: 67/77 skeleton lines (87.0%)
66deliberate: 11 line(s) — pre-code ×6, semantic-emphasis ×4, verse-trailing-break ×1
77unexplained: 0 line(s)
8(- org-ssg, + emacs, ~ a difference we mean to have)
8(- orgo, + emacs, ~ a difference we mean to have)
99
1010 <h2>
1111 "Quote"
tests/snapshots/oracle__oracle_core.snap +1 −1
@@ -5,7 +5,7 @@ expression: report
55agreement: 51/58 skeleton lines (87.9%)
66deliberate: 7 line(s) — list-per-bullet-type ×2, pre-code ×2, semantic-emphasis ×2
77unexplained: 0 line(s)
8(- org-ssg, + emacs, ~ a difference we mean to have)
8(- orgo, + emacs, ~ a difference we mean to have)
99
1010 <p>
1111 "Intro paragraph with a bare URL"
tests/snapshots/oracle__oracle_elements.snap +1 −1
@@ -5,7 +5,7 @@ expression: report
55agreement: 64/82 skeleton lines (78.0%)
66deliberate: 26 line(s) — footnote-anchor-naming ×1, footnote-section-shape ×2, id-link-resolution ×1, no-colgroup ×1, pre-code ×2
77unexplained: 0 line(s)
8(- org-ssg, + emacs, ~ a difference we mean to have)
8(- orgo, + emacs, ~ a difference we mean to have)
99
1010 <h2>
1111 "Code and tables"
tests/snapshots/oracle__oracle_footnote.snap +1 −1
@@ -5,7 +5,7 @@ expression: report
55agreement: 30/53 skeleton lines (56.6%)
66deliberate: 29 line(s) — footnote-anchor-naming ×3, footnote-section-shape ×6, semantic-emphasis ×2
77unexplained: 0 line(s)
8(- org-ssg, + emacs, ~ a difference we mean to have)
8(- orgo, + emacs, ~ a difference we mean to have)
99
1010 <p>
1111 "Text with a reference."
tests/snapshots/oracle__oracle_headings.snap +1 −1
@@ -5,7 +5,7 @@ expression: report
55agreement: 28/30 skeleton lines (93.3%)
66deliberate: 2 line(s) — semantic-time ×2
77unexplained: 0 line(s)
8(- org-ssg, + emacs, ~ a difference we mean to have)
8(- orgo, + emacs, ~ a difference we mean to have)
99
1010 <h2>
1111~ semantic-time (1 line(s))
tests/snapshots/oracle__oracle_images.snap +1 −1
@@ -5,7 +5,7 @@ expression: report
55agreement: 30/42 skeleton lines (71.4%)
66deliberate: 14 line(s) — figure-element ×8, semantic-emphasis ×2
77unexplained: 0 line(s)
8(- org-ssg, + emacs, ~ a difference we mean to have)
8(- orgo, + emacs, ~ a difference we mean to have)
99
1010 <h2>
1111 "Bare image"
tests/snapshots/oracle__oracle_lists.snap +1 −1
@@ -5,7 +5,7 @@ expression: report
55agreement: 109/111 skeleton lines (98.2%)
66deliberate: 2 line(s) — semantic-emphasis ×2
77unexplained: 0 line(s)
8(- org-ssg, + emacs, ~ a difference we mean to have)
8(- orgo, + emacs, ~ a difference we mean to have)
99
1010 <h2>
1111 "Nesting"
tests/snapshots/oracle__oracle_minimal.snap +1 −1
@@ -5,7 +5,7 @@ expression: report
55agreement: 38/42 skeleton lines (90.5%)
66deliberate: 4 line(s) — semantic-emphasis ×4
77unexplained: 0 line(s)
8(- org-ssg, + emacs, ~ a difference we mean to have)
8(- orgo, + emacs, ~ a difference we mean to have)
99
1010 <p>
1111 "A single paragraph of preamble text before any heading."
tests/snapshots/oracle__oracle_table.snap +1 −1
@@ -5,7 +5,7 @@ expression: report
55agreement: 30/34 skeleton lines (88.2%)
66deliberate: 4 line(s) — no-colgroup ×1
77unexplained: 0 line(s)
8(- org-ssg, + emacs, ~ a difference we mean to have)
8(- orgo, + emacs, ~ a difference we mean to have)
99
1010 <table>
1111~ no-colgroup (4 line(s))
tests/snapshots/oracle__oracle_timestamps.snap +1 −1
@@ -5,7 +5,7 @@ expression: report
55agreement: 25/62 skeleton lines (40.3%)
66deliberate: 37 line(s) — semantic-time ×5
77unexplained: 0 line(s)
8(- org-ssg, + emacs, ~ a difference we mean to have)
8(- orgo, + emacs, ~ a difference we mean to have)
99
1010 <h2>
1111 "Single"
tests/snapshots/site__site_guide_html.snap +2 −2
@@ -7,12 +7,12 @@ expression: "page(&pages, \"guide.org\").html"
77<head>
88<meta charset="utf-8">
99<meta name="viewport" content="width=device-width, initial-scale=1">
10<title>Guide &middot; org-ssg site</title>
10<title>Guide &middot; orgo site</title>
1111<link rel="stylesheet" href="syntax.css">
1212</head>
1313<body>
1414<header>
15<a class="site-title" href="index.html">org-ssg site</a>
15<a class="site-title" href="index.html">orgo site</a>
1616<nav>
1717<a href="about.html">About</a>
1818<a href="#">Guide</a>
tests/snapshots/site__site_index_html.snap +2 −2
@@ -7,12 +7,12 @@ expression: "page(&pages, \"index.org\").html"
77<head>
88<meta charset="utf-8">
99<meta name="viewport" content="width=device-width, initial-scale=1">
10<title>Home &middot; org-ssg site</title>
10<title>Home &middot; orgo site</title>
1111<link rel="stylesheet" href="syntax.css">
1212</head>
1313<body>
1414<header>
15<a class="site-title" href="index.html">org-ssg site</a>
15<a class="site-title" href="index.html">orgo site</a>
1616<nav>
1717<a href="about.html">About</a>
1818<a href="guide.html">Guide</a>
tests/watch.rs +7 −7
@@ -1,6 +1,6 @@
11//! `watch`: the change filter, and one end-to-end run against real filesystem events.
22//!
3//! The filter carries the weight here. `org-ssg watch . -o _site` puts the output inside
3//! The filter carries the weight here. `orgo watch . -o _site` puts the output inside
44//! the source, so a rebuild writes files, writing files raises events, and events trigger
55//! a rebuild — a loop that never stops. That it is a pure function is what makes the
66//! guarantee testable without waiting on a filesystem.
@@ -10,15 +10,15 @@ use std::time::{Duration, Instant};
1010
1111use camino::{Utf8Path, Utf8PathBuf};
1212
13use org_ssg::site::{build_site, BuildOptions};
14use org_ssg::watch::ChangeFilter;
13use orgo::site::{build_site, BuildOptions};
14use orgo::watch::ChangeFilter;
1515
1616fn tmpdir(tag: &str) -> Utf8PathBuf {
1717 static N: AtomicU32 = AtomicU32::new(0);
1818 let n = N.fetch_add(1, Ordering::Relaxed);
1919 let base = Utf8PathBuf::from_path_buf(std::env::temp_dir())
2020 .expect("utf-8 temp dir")
21 .join(format!("org-ssg-watch-{}-{tag}-{n}", std::process::id()));
21 .join(format!("orgo-watch-{}-{tag}-{n}", std::process::id()));
2222 let _ = std::fs::remove_dir_all(&base);
2323 std::fs::create_dir_all(&base).unwrap();
2424 base
@@ -39,7 +39,7 @@ fn changes_under_the_output_directory_are_ignored() {
3939 let filter = ChangeFilter::new(&src, &out);
4040 assert!(!filter.is_relevant(Utf8Path::new("_site/index.html")));
4141 assert!(!filter.is_relevant(Utf8Path::new("_site/blog/post.html")));
42 assert!(!filter.is_relevant(Utf8Path::new("_site/.org-ssg-cache.json")));
42 assert!(!filter.is_relevant(Utf8Path::new("_site/.orgo-cache.json")));
4343 assert!(filter.is_relevant(Utf8Path::new("index.org")), "real sources still count");
4444}
4545
@@ -68,7 +68,7 @@ fn build_inputs_trigger_a_rebuild_even_though_discovery_skips_them() {
6868 std::fs::create_dir_all(&src).unwrap();
6969 let filter = ChangeFilter::new(&src, &root.join("out"));
7070
71 assert!(filter.is_relevant(Utf8Path::new("org-ssg.toml")));
71 assert!(filter.is_relevant(Utf8Path::new("orgo.toml")));
7272 assert!(filter.is_relevant(Utf8Path::new("templates/base.html")));
7373 assert!(filter.is_relevant(Utf8Path::new("templates/feed.xml")));
7474 assert!(filter.is_relevant(Utf8Path::new("style.css")), "assets are copied through");
@@ -149,7 +149,7 @@ fn watching_rebuilds_the_site_when_a_source_file_changes() {
149149
150150 let (src_t, out_t) = (src.clone(), out.clone());
151151 let handle = std::thread::spawn(move || {
152 let _ = org_ssg::watch::run(&src_t, &out_t, &BuildOptions::default());
152 let _ = orgo::watch::run(&src_t, &out_t, &BuildOptions::default());
153153 });
154154
155155 // Give the watcher a moment to register before making the change it should see.