Commit bbe31403db
Verified · cmc
Layout: unified · split
.github/workflows/ci.yml added +104
| @@ -0,0 +1,104 @@ | |||
| 1 | name: CI | ||
| 2 | |||
| 3 | # Build, test and lint on both platforms org-ssg is used from, plus a compiler-floor job. | ||
| 4 | # | ||
| 5 | # WHAT THIS CATCHES THAT LOCAL WORK DOES NOT: | ||
| 6 | # | ||
| 7 | # 1. Linux. Development happens on macOS, and the two differ where this project is most | ||
| 8 | # likely to break: filesystem event paths (the watcher had a real `/var` vs | ||
| 9 | # `/private/var` bug on macOS), case-insensitive filenames, and path separators. | ||
| 10 | # 2. A clean checkout. The oracle tests skip when Emacs is absent and the cache is | ||
| 11 | # gitignored, so a machine that has been building all afternoon is not a fair test of | ||
| 12 | # what a fresh clone does. | ||
| 13 | # 3. The MSRV. A stabilised API used without noticing is invisible on a current | ||
| 14 | # toolchain and is a build failure for anyone on a distribution compiler. | ||
| 15 | # | ||
| 16 | # NOT GATED ON `cargo fmt`. The source is formatted by hand — comment tables, aligned | ||
| 17 | # match arms, and prose wrapped to fit the argument being made — and rustfmt disagrees | ||
| 18 | # with most of it. Clippy is the lint that catches defects; fmt would only catch taste. | ||
| 19 | |||
| 20 | on: | ||
| 21 | push: | ||
| 22 | branches: [main] | ||
| 23 | pull_request: | ||
| 24 | workflow_dispatch: | ||
| 25 | |||
| 26 | env: | ||
| 27 | CARGO_TERM_COLOR: always | ||
| 28 | # A failing build should print the error, not a backtrace-shaped wall. | ||
| 29 | RUST_BACKTRACE: 1 | ||
| 30 | |||
| 31 | jobs: | ||
| 32 | test: | ||
| 33 | name: test (${{ matrix.os }}) | ||
| 34 | runs-on: ${{ matrix.os }} | ||
| 35 | strategy: | ||
| 36 | # Both platforms report, so a macOS-only failure is distinguishable from a real one. | ||
| 37 | fail-fast: false | ||
| 38 | matrix: | ||
| 39 | os: [ubuntu-latest, macos-latest] | ||
| 40 | steps: | ||
| 41 | - name: Checkout | ||
| 42 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | ||
| 43 | |||
| 44 | - name: Install Rust | ||
| 45 | uses: dtolnay/rust-toolchain@1ff72ee08e3cb84d84adba594e0a297990fc1ed3 # stable | ||
| 46 | with: | ||
| 47 | toolchain: stable | ||
| 48 | components: clippy | ||
| 49 | |||
| 50 | - name: Cache cargo | ||
| 51 | uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 | ||
| 52 | |||
| 53 | # Emacs makes the oracle suite run for real instead of skipping. It is the only | ||
| 54 | # reason to trust that output still matches org's own exporter, so it is worth the | ||
| 55 | # install minute. | ||
| 56 | - name: Install Emacs (Linux) | ||
| 57 | if: runner.os == 'Linux' | ||
| 58 | run: sudo apt-get update && sudo apt-get install -y --no-install-recommends emacs-nox | ||
| 59 | |||
| 60 | - name: Install Emacs (macOS) | ||
| 61 | if: runner.os == 'macOS' | ||
| 62 | run: brew install emacs | ||
| 63 | |||
| 64 | - name: Build | ||
| 65 | run: cargo build --all-targets --locked | ||
| 66 | |||
| 67 | - name: Test | ||
| 68 | run: cargo test --locked | ||
| 69 | |||
| 70 | - name: Clippy | ||
| 71 | run: cargo clippy --all-targets --locked -- -D warnings | ||
| 72 | |||
| 73 | # The documentation site is built by the tool it documents, so a docs page that no | ||
| 74 | # longer builds is a product defect. `--strict` fails on broken internal links, | ||
| 75 | # which is the failure mode a docs site actually has. | ||
| 76 | - name: Build the documentation site | ||
| 77 | run: cargo run --locked -- build docs -o docs/_site --strict | ||
| 78 | |||
| 79 | msrv: | ||
| 80 | name: minimum supported Rust (1.88) | ||
| 81 | runs-on: ubuntu-latest | ||
| 82 | steps: | ||
| 83 | - name: Checkout | ||
| 84 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | ||
| 85 | |||
| 86 | # Pinned to the version in Cargo.toml's `rust-version`. When that moves, this moves | ||
| 87 | # with it in the same commit — a floor nobody checks is a floor nobody has. | ||
| 88 | - name: Install Rust 1.88 | ||
| 89 | uses: dtolnay/rust-toolchain@1ff72ee08e3cb84d84adba594e0a297990fc1ed3 # stable | ||
| 90 | with: | ||
| 91 | toolchain: "1.88" | ||
| 92 | |||
| 93 | - name: Cache cargo | ||
| 94 | uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 | ||
| 95 | |||
| 96 | # Build only. The tests pull in dev-dependencies whose own floors move | ||
| 97 | # independently, and chasing those would make this job about someone else's MSRV. | ||
| 98 | # | ||
| 99 | # The floor is set by dependencies rather than by org-ssg — its own code compiles on | ||
| 100 | # 1.82 — which is precisely why it is checked here instead of reasoned about: a | ||
| 101 | # dependency raising its floor is invisible until someone on an older compiler | ||
| 102 | # tries to build. | ||
| 103 | - name: Build | ||
| 104 | run: cargo build --locked | ||
.github/workflows/release.yml added +113
| @@ -0,0 +1,113 @@ | |||
| 1 | name: Release | ||
| 2 | |||
| 3 | # Build binaries for a tag and attach them to a GitHub release. | ||
| 4 | # | ||
| 5 | # WHY BINARIES AT ALL, given `cargo install org-ssg` exists: installing from source needs | ||
| 6 | # a Rust toolchain and about a minute of compiling syntect. Someone evaluating a site | ||
| 7 | # generator should be able to download one file and point it at their notes. | ||
| 8 | # | ||
| 9 | # The tag is the source of truth for the version. The build checks it against Cargo.toml | ||
| 10 | # rather than trusting them to match, because a release tagged v0.18.0 containing a binary | ||
| 11 | # that reports 0.17.0 is the kind of thing nobody notices for months. | ||
| 12 | |||
| 13 | on: | ||
| 14 | push: | ||
| 15 | tags: ["v*"] | ||
| 16 | workflow_dispatch: | ||
| 17 | inputs: | ||
| 18 | tag: | ||
| 19 | description: "Tag to build (for a re-run of a failed release)" | ||
| 20 | required: true | ||
| 21 | |||
| 22 | env: | ||
| 23 | CARGO_TERM_COLOR: always | ||
| 24 | |||
| 25 | jobs: | ||
| 26 | build: | ||
| 27 | name: ${{ matrix.target }} | ||
| 28 | runs-on: ${{ matrix.os }} | ||
| 29 | strategy: | ||
| 30 | fail-fast: false | ||
| 31 | matrix: | ||
| 32 | include: | ||
| 33 | # Apple Silicon and Intel Macs, built natively on their own runners so neither | ||
| 34 | # is a cross-compile nobody has run. | ||
| 35 | - { os: macos-latest, target: aarch64-apple-darwin } | ||
| 36 | - { os: macos-13, target: x86_64-apple-darwin } | ||
| 37 | # glibc for ordinary distributions, musl for containers and anything older than | ||
| 38 | # the runner's glibc — a dynamically linked binary is the usual reason a | ||
| 39 | # download does not run. | ||
| 40 | - { os: ubuntu-latest, target: x86_64-unknown-linux-gnu } | ||
| 41 | - { os: ubuntu-latest, target: x86_64-unknown-linux-musl } | ||
| 42 | steps: | ||
| 43 | - name: Checkout | ||
| 44 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | ||
| 45 | with: | ||
| 46 | ref: ${{ github.event.inputs.tag || github.ref }} | ||
| 47 | |||
| 48 | - name: Install Rust | ||
| 49 | uses: dtolnay/rust-toolchain@1ff72ee08e3cb84d84adba594e0a297990fc1ed3 # stable | ||
| 50 | with: | ||
| 51 | toolchain: stable | ||
| 52 | targets: ${{ matrix.target }} | ||
| 53 | |||
| 54 | - name: Install musl tools | ||
| 55 | if: endsWith(matrix.target, '-musl') | ||
| 56 | run: sudo apt-get update && sudo apt-get install -y --no-install-recommends musl-tools | ||
| 57 | |||
| 58 | - name: Check the tag against Cargo.toml | ||
| 59 | shell: bash | ||
| 60 | run: | | ||
| 61 | tag="${{ github.event.inputs.tag || github.ref_name }}" | ||
| 62 | crate=$(cargo metadata --no-deps --format-version 1 \ | ||
| 63 | | python3 -c 'import json,sys; print(json.load(sys.stdin)["packages"][0]["version"])') | ||
| 64 | if [ "$tag" != "v$crate" ]; then | ||
| 65 | echo "tag $tag does not match Cargo.toml version $crate" >&2 | ||
| 66 | exit 1 | ||
| 67 | fi | ||
| 68 | |||
| 69 | - name: Build | ||
| 70 | run: cargo build --release --locked --target ${{ matrix.target }} | ||
| 71 | |||
| 72 | # A tarball rather than a bare binary: it keeps the executable bit through GitHub's | ||
| 73 | # download path, and carries the licence with the thing it licenses. | ||
| 74 | - name: Package | ||
| 75 | shell: bash | ||
| 76 | run: | | ||
| 77 | staging="org-ssg-${{ github.event.inputs.tag || github.ref_name }}-${{ matrix.target }}" | ||
| 78 | mkdir "$staging" | ||
| 79 | cp "target/${{ matrix.target }}/release/org-ssg" "$staging/" | ||
| 80 | cp README.md LICENSE CHANGELOG.md "$staging/" | ||
| 81 | tar czf "$staging.tar.gz" "$staging" | ||
| 82 | shasum -a 256 "$staging.tar.gz" > "$staging.tar.gz.sha256" | ||
| 83 | |||
| 84 | - name: Upload | ||
| 85 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||
| 86 | with: | ||
| 87 | name: ${{ matrix.target }} | ||
| 88 | path: | | ||
| 89 | *.tar.gz | ||
| 90 | *.tar.gz.sha256 | ||
| 91 | |||
| 92 | release: | ||
| 93 | name: publish the release | ||
| 94 | needs: build | ||
| 95 | runs-on: ubuntu-latest | ||
| 96 | permissions: | ||
| 97 | contents: write | ||
| 98 | steps: | ||
| 99 | - name: Download every build | ||
| 100 | uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 | ||
| 101 | with: | ||
| 102 | merge-multiple: true | ||
| 103 | |||
| 104 | # A draft, deliberately. The changelog entry is written by a person, and a release | ||
| 105 | # that publishes itself before anyone has read it cannot be edited quietly. | ||
| 106 | - name: Create the draft release | ||
| 107 | uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2 | ||
| 108 | with: | ||
| 109 | draft: true | ||
| 110 | tag_name: ${{ github.event.inputs.tag || github.ref_name }} | ||
| 111 | files: | | ||
| 112 | *.tar.gz | ||
| 113 | *.tar.gz.sha256 | ||
CHANGELOG.md added +156
| @@ -0,0 +1,156 @@ | |||
| 1 | # Changelog | ||
| 2 | |||
| 3 | What changed and why, newest first. Entries name the *behaviour* that moved, since that is | ||
| 4 | what a rebuild will show you. | ||
| 5 | |||
| 6 | Two conventions worth knowing before reading: | ||
| 7 | |||
| 8 | - **A cache-format bump is not a change you need to act on.** The incremental cache is | ||
| 9 | 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 | ||
| 11 | file, so an entry that says "now renders X" means your pages will change. That is the | ||
| 12 | product, not a regression — but it belongs in a changelog rather than a diff you find | ||
| 13 | later. | ||
| 14 | |||
| 15 | Versions follow the compatibility promise in the README: config keys, template variables, | ||
| 16 | CLI flags and URLs are the stable surface. | ||
| 17 | |||
| 18 | ## 0.18.0 | ||
| 19 | |||
| 20 | Release engineering, so that a version number is worth reading. | ||
| 21 | |||
| 22 | - **A written compatibility promise.** Config keys, template variables, CLI flags and URLs | ||
| 23 | are the stable surface; the incremental cache, HTML details and the Rust API are not. | ||
| 24 | In the README, and in the guide under *Versioning and upgrades*. | ||
| 25 | - **CI** on Linux and macOS: build, test, clippy as an error, and the documentation site | ||
| 26 | built with `--strict`. Emacs is installed on both, so the oracle suite runs for real | ||
| 27 | instead of skipping. | ||
| 28 | - **A checked MSRV**, 1.88 — which is how it came to be 1.88 rather than the 1.82 | ||
| 29 | org-ssg's own code needs. The floor comes from dependencies, and nobody finds that out | ||
| 30 | by reasoning about it. | ||
| 31 | - **Release binaries** for macOS (arm64, x86_64) and Linux (gnu, musl), built on tag into | ||
| 32 | a draft release. The tag is checked against `Cargo.toml` before anything is built. | ||
| 33 | - A `LICENSE` file to go with the MIT declaration, crates.io metadata, and a release | ||
| 34 | profile that produces a 5.0 MB binary rather than 6.5 MB. | ||
| 35 | - This changelog, and `RELEASING.md`. | ||
| 36 | |||
| 37 | ## 0.17.0 | ||
| 38 | |||
| 39 | - **Asset directories outside the source.** `[build] assets = ["../theme/static"]` copies | ||
| 40 | a directory's contents to the site root. A site's static files do not always live where | ||
| 41 | its writing does, and copying them next to the writing is how a repository ends up with | ||
| 42 | two of every stylesheet. `watch` and `serve` watch these directories too. Two files | ||
| 43 | claiming one URL is a build error naming both. | ||
| 44 | - **Template hashing is per template.** A page's render key covered every template, so | ||
| 45 | editing a feed template re-rendered the whole site. It now covers the layout the page | ||
| 46 | uses plus what that layout extends, includes or imports. On a 196-page site, editing the | ||
| 47 | feed template renders one page instead of 196. | ||
| 48 | - Cache format 7. | ||
| 49 | |||
| 50 | ## 0.16.0 | ||
| 51 | |||
| 52 | - **Org's entity table.** `\alpha`, `\rarr`, `20\deg` and the other 412 names, generated | ||
| 53 | from Emacs' own `org-entities`. An unknown name stays literal; `#+OPTIONS: e:nil` turns | ||
| 54 | the table off. *Output changes* for any page using entities. | ||
| 55 | - **Table captions.** `#+CAPTION:` above a table becomes a numbered `<caption>`. | ||
| 56 | - **`#+INCLUDE:` reports itself.** It was inert and silent, which publishes a page with | ||
| 57 | content missing and nobody told. Now a diagnostic, and `--strict` makes it a failure. | ||
| 58 | - The Emacs oracle separates deliberate divergence from defects. Every difference from | ||
| 59 | org's exporter is named and justified, and a test asserts there are no others. | ||
| 60 | |||
| 61 | ## 0.15.0 | ||
| 62 | |||
| 63 | Export parity, from a page-by-page diff of a 179-file corpus against the site Emacs | ||
| 64 | publishes from the same sources. **All of these change output.** | ||
| 65 | |||
| 66 | - Heading levels are relative to a document's shallowest heading, as org exports them. | ||
| 67 | - Org's text conversions: `--`, `---`, `...`, and `x^2` / `a_{b}`. Never inside verbatim, | ||
| 68 | code, source blocks or LaTeX. `#+OPTIONS: -:nil`, `^:nil` and `^:{}` all work. | ||
| 69 | - Captioned figures are numbered `Figure N:`. | ||
| 70 | - A caption attaches to the element *directly* below it; a blank line between attaches to | ||
| 71 | nothing. | ||
| 72 | - Checkboxes render as org writes them, which keeps the `[-]` partly-done state a disabled | ||
| 73 | `<input>` could not express. `[@4]` sets a list item's number. | ||
| 74 | - A table's special marker column and its marker rows stay out of the output. | ||
| 75 | - `#+BEGIN_NOTE` and any other unrecognised name is a special block: a div holding parsed | ||
| 76 | org rather than a `<pre>` of literal text. Verse keeps its line breaks. | ||
| 77 | - Emphasis borders forbid whitespace and nothing else, so `="proxied":false=` is verbatim | ||
| 78 | and `~~/.config/doom/config.el~` is a path that starts with a tilde. | ||
| 79 | - Listings sort on the time of day when a timestamp carries one. | ||
| 80 | - Cache format 6. | ||
| 81 | |||
| 82 | ## 0.14.0 | ||
| 83 | |||
| 84 | - **Per-page layouts.** `[[pages]]` rules map a source path to a template, and | ||
| 85 | `#+TEMPLATE:` on a page overrides any rule. A missing template fails the build naming | ||
| 86 | the page, the template, and what does exist. | ||
| 87 | - `page.year`, for grouping a listing by year with minijinja's `groupby`. | ||
| 88 | - An explicit nav can order generated pages among authored ones. `nav.mode = "none"` now | ||
| 89 | really means none. | ||
| 90 | |||
| 91 | ## 0.13.0 | ||
| 92 | |||
| 93 | - Bundled TOML and Org syntax definitions, a `syntaxes_dir` for your own, and org's comma | ||
| 94 | escape (`,* heading` inside a block). | ||
| 95 | |||
| 96 | ## 0.12.0 | ||
| 97 | |||
| 98 | - `serve`: a development server with live reload, bound to loopback. | ||
| 99 | - A documentation site under `docs/`, built by org-ssg itself. | ||
| 100 | |||
| 101 | ## 0.11.0 | ||
| 102 | |||
| 103 | - Table of contents as `page.toc`, section numbers, and org's `#+OPTIONS:` per-file | ||
| 104 | switches. | ||
| 105 | |||
| 106 | ## 0.10.0 | ||
| 107 | |||
| 108 | - Excerpts, word count, reading time, a `truncate` filter, and `#+DRAFT:` pages. | ||
| 109 | |||
| 110 | ## 0.9.0 | ||
| 111 | |||
| 112 | - `watch`: rebuilds on OS filesystem events, debounced. | ||
| 113 | |||
| 114 | ## 0.8.0 | ||
| 115 | |||
| 116 | - `site.base_url`, the `absolute` and `rfc822` filters, canonical links, and an RSS feed | ||
| 117 | in the scaffold that validates. | ||
| 118 | |||
| 119 | ## 0.7.0 | ||
| 120 | |||
| 121 | - Pagination for large listings, with a `paginator` template context that composes with | ||
| 122 | grouping. | ||
| 123 | |||
| 124 | ## 0.6.0 | ||
| 125 | |||
| 126 | - Grouped collections: one page per tag plus a tag index. | ||
| 127 | - Generated listing pages (`[[collections]]`), sorted indexes, and feeds via XML | ||
| 128 | templates. | ||
| 129 | - A config file, user templates, nav modes, an `init` scaffold, and discovery that will | ||
| 130 | not publish `.git`. | ||
| 131 | |||
| 132 | ## 0.5.0 | ||
| 133 | |||
| 134 | - Parse diagnostics carry `file:line`, and pages render in parallel. | ||
| 135 | - The corpus audit (`org-ssg audit`) and the `emacs --batch` oracle. | ||
| 136 | - `#+SLUG:` decides a page's output filename — found by auditing a real corpus, where it | ||
| 137 | affected 169 of 182 URLs. | ||
| 138 | |||
| 139 | ## 0.4.0 | ||
| 140 | |||
| 141 | - The full v1 construct scope, with the IN/OUT line under test. | ||
| 142 | |||
| 143 | ## 0.3.0 | ||
| 144 | |||
| 145 | - The incremental build layer: content, config and template hashing, a dependency graph, | ||
| 146 | per-page render keys, and a persisted cache manifest. A full build and an incremental | ||
| 147 | build produce byte-identical output. | ||
| 148 | |||
| 149 | ## 0.2.0 | ||
| 150 | |||
| 151 | - Multi-file site builds: a symbol table, internal link resolution, minijinja templates, | ||
| 152 | tables and footnotes. | ||
| 153 | |||
| 154 | ## 0.1.0 | ||
| 155 | |||
| 156 | - Parse and render a single `.org` file to HTML. | ||
Cargo.lock +1 −1
| @@ -675,7 +675,7 @@ dependencies = [ | |||
| 675 | 675 | ||
| 676 | [[package]] | 676 | [[package]] |
| 677 | name = "org-ssg" | 677 | name = "org-ssg" |
| 678 | version = "0.17.0" | 678 | version = "0.18.0" |
| 679 | dependencies = [ | 679 | dependencies = [ |
| 680 | "anyhow", | 680 | "anyhow", |
| 681 | "blake3", | 681 | "blake3", |
Cargo.toml +25 −1
| @@ -1,9 +1,26 @@ | |||
| 1 | [package] | 1 | [package] |
| 2 | name = "org-ssg" | 2 | name = "org-ssg" |
| 3 | version = "0.17.0" | 3 | version = "0.18.0" |
| 4 | edition = "2021" | 4 | edition = "2021" |
| 5 | description = "Org-mode static site generator that renders the org element tree straight to HTML" | 5 | description = "Org-mode static site generator that renders the org element tree straight to HTML" |
| 6 | license = "MIT" | 6 | license = "MIT" |
| 7 | readme = "README.md" | ||
| 8 | keywords = ["org-mode", "static-site-generator", "emacs", "html", "blog"] | ||
| 9 | categories = ["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/" | ||
| 13 | |||
| 14 | # The compiler floor, checked in CI rather than assumed. org-ssg's own code needs 1.82 | ||
| 15 | # (`Option::is_none_or`); the floor is 1.88 because dependencies in Cargo.lock declare it | ||
| 16 | # — `plist` and `time`, both by way of syntect. Raising this is a minor-version change, | ||
| 17 | # never a patch. | ||
| 18 | rust-version = "1.88" | ||
| 19 | |||
| 20 | # Published crates carry the source, the fixtures the tests need, and nothing else. The | ||
| 21 | # documentation site is 14 org files plus its build output, which nobody installing a | ||
| 22 | # binary wants to download. | ||
| 23 | exclude = ["docs/", "target/", "/.github/"] | ||
| 7 | 24 | ||
| 8 | [lib] | 25 | [lib] |
| 9 | name = "org_ssg" | 26 | name = "org_ssg" |
| @@ -37,3 +54,10 @@ tiny_http = "0.12.0" | |||
| 37 | 54 | ||
| 38 | [dev-dependencies] | 55 | [dev-dependencies] |
| 39 | insta = { version = "1", features = ["json"] } | 56 | insta = { version = "1", features = ["json"] } |
| 57 | |||
| 58 | # Release binaries are downloaded by people evaluating the tool, so they are built to be | ||
| 59 | # small and quick to start rather than quick to compile. Thin LTO keeps CI build times | ||
| 60 | # reasonable; full LTO bought a few percent for minutes per job. | ||
| 61 | [profile.release] | ||
| 62 | lto = "thin" | ||
| 63 | strip = "symbols" | ||
LICENSE added +21
| @@ -0,0 +1,21 @@ | |||
| 1 | MIT License | ||
| 2 | |||
| 3 | Copyright (c) 2026 Christian Cleberg | ||
| 4 | |||
| 5 | Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| 6 | of this software and associated documentation files (the "Software"), to deal | ||
| 7 | in the Software without restriction, including without limitation the rights | ||
| 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| 9 | copies of the Software, and to permit persons to whom the Software is | ||
| 10 | furnished to do so, subject to the following conditions: | ||
| 11 | |||
| 12 | The above copyright notice and this permission notice shall be included in all | ||
| 13 | copies or substantial portions of the Software. | ||
| 14 | |||
| 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| 21 | SOFTWARE. | ||
README.md +28 −2
| @@ -387,7 +387,7 @@ all-of-org. Phase 0 checked this line against a real 179-file corpus and found i | |||
| 387 | | **19** | **Export parity: relative heading levels, special strings, sub/superscript, caption numbering, checkbox and counter markup, table marker columns, special blocks** | **done** | | 387 | | **19** | **Export parity: relative heading levels, special strings, sub/superscript, caption numbering, checkbox and counter markup, table marker columns, special blocks** | **done** | |
| 388 | | **20** | **Correctness debt: org's entity table, table captions, a reported `#+INCLUDE:`, and an oracle that separates deliberate divergence from defects** | **done** | | 388 | | **20** | **Correctness debt: org's entity table, table captions, a reported `#+INCLUDE:`, and an oracle that separates deliberate divergence from defects** | **done** | |
| 389 | | **21** | **Extra asset roots; per-template hashing so one layout edit does not re-render the site** | **done** | | 389 | | **21** | **Extra asset roots; per-template hashing so one layout edit does not re-render the site** | **done** | |
| 390 | | 22 | Release engineering: CI, MSRV, published binaries, changelog, a written compatibility promise | 1.0 | | 390 | | **22** | **Release engineering: CI on both platforms, a checked MSRV, release binaries, a changelog, and a written compatibility promise** | **done** | |
| 391 | 391 | ||
| 392 | ### v0.2 in / out | 392 | ### v0.2 in / out |
| 393 | 393 | ||
| @@ -688,6 +688,32 @@ anchored — `:CUSTOM_ID:`/`:ID:` else a slug of its text) and trailing tags; pa | |||
| 688 | plain lists (unordered + ordered) with checkboxes; source blocks; inline markup (`*bold*`, | 688 | plain lists (unordered + ordered) with checkboxes; source blocks; inline markup (`*bold*`, |
| 689 | `/italic/`, `_underline_`, `+strike+`, `=verbatim=`, `~code~`); links and bare URLs. | 689 | `/italic/`, `_underline_`, `+strike+`, `=verbatim=`, `~code~`); links and bare URLs. |
| 690 | 690 | ||
| 691 | ## Compatibility | ||
| 692 | |||
| 693 | Versions mean something as of 1.0. The **stable surface** — changing incompatibly requires | ||
| 694 | a major version — is what you actually build a site against: | ||
| 695 | |||
| 696 | | Stable | Detail | | ||
| 697 | |---|---| | ||
| 698 | | `org-ssg.toml` keys | Names, types and meaning. New keys are minor releases; removing one is major. | | ||
| 699 | | Template context | `page`, `site`, `nav`, `root`, `pages`, `group`, `groups`, `paginator`, `stylesheet`, and the `absolute` / `rfc822` / `truncate` filters. | | ||
| 700 | | CLI | Command names, flags, and exit codes. | | ||
| 701 | | URLs | How a source path becomes an output path, including `#+SLUG:`. A generator that moves your URLs breaks every link anyone has to you. | | ||
| 702 | |||
| 703 | Explicitly **not stable**, so that the above can be: | ||
| 704 | |||
| 705 | - **The incremental cache.** Versioned, discarded on mismatch, never a correctness | ||
| 706 | 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 | ||
| 708 | closing a gap changes markup. Changes that affect output are called out in | ||
| 709 | [CHANGELOG.md](CHANGELOG.md) — the class names the documentation names (`post-list`, | ||
| 710 | `figure-number`, `section-number-N`, `footnote-ref`) are the ones to write CSS against. | ||
| 711 | - **The Rust API.** The crate is published so the binary can be installed with | ||
| 712 | `cargo install`; the library exists to serve it, and its types move as the tool does. | ||
| 713 | |||
| 714 | The **MSRV is 1.88**, checked in CI on every change. org-ssg's own code compiles on | ||
| 715 | 1.82; the floor comes from dependencies. Raising it is a minor version, never a patch. | ||
| 716 | |||
| 691 | ## Dependencies | 717 | ## Dependencies |
| 692 | 718 | ||
| 693 | Parser is hand-written recursive descent (not `nom`/`chumsky`/`pest` — org is | 719 | Parser is hand-written recursive descent (not `nom`/`chumsky`/`pest` — org is |
| @@ -702,7 +728,7 @@ development server), `toml` (config), `chrono`, `camino`, `walkdir`, `clap`, `an | |||
| 702 | 728 | ||
| 703 | ``` | 729 | ``` |
| 704 | cargo build | 730 | cargo build |
| 705 | cargo test # 156 tests | 731 | cargo test # 191 tests |
| 706 | cargo run -- init my-site # scaffold a new site | 732 | cargo run -- init my-site # scaffold a new site |
| 707 | cargo run -- build fixtures/minimal.org -o minimal.html # single file | 733 | cargo run -- build fixtures/minimal.org -o minimal.html # single file |
| 708 | cargo run -- build fixtures/site -o _site # whole site (incremental) | 734 | cargo run -- build fixtures/site -o _site # whole site (incremental) |
RELEASING.md added +78
| @@ -0,0 +1,78 @@ | |||
| 1 | # Releasing | ||
| 2 | |||
| 3 | A release is three things that must agree: a version in `Cargo.toml`, a git tag, and a | ||
| 4 | changelog entry. The release workflow checks the first two against each other and refuses | ||
| 5 | to build if they differ, because a release tagged `v0.18.0` containing a binary that | ||
| 6 | reports `0.17.0` is the kind of mistake nobody notices for months. | ||
| 7 | |||
| 8 | ## Before the first publish | ||
| 9 | |||
| 10 | `repository` in `Cargo.toml` is commented out, because a wrong URL on a crates.io page is | ||
| 11 | worse than none. Set it, then: | ||
| 12 | |||
| 13 | ```bash | ||
| 14 | cargo login # a crates.io token, once per machine | ||
| 15 | cargo publish --dry-run | ||
| 16 | ``` | ||
| 17 | |||
| 18 | ## Every release | ||
| 19 | |||
| 20 | 1. **Write the changelog entry first.** [CHANGELOG.md](CHANGELOG.md) names behaviour, not | ||
| 21 | commits — someone reading it wants to know what their next build will do differently. | ||
| 22 | Anything that changes rendered HTML gets said out loud. | ||
| 23 | |||
| 24 | 2. **Bump the version** in `Cargo.toml`, and build once so `Cargo.lock` follows. | ||
| 25 | |||
| 26 | Patch for fixes that change nothing about the stable surface. Minor for new config | ||
| 27 | keys, new template variables, an MSRV bump, or output that changes to track Emacs more | ||
| 28 | closely. Major for anything that breaks the promises in the README's Compatibility | ||
| 29 | section — config keys, template context, CLI, or URLs. | ||
| 30 | |||
| 31 | 3. **Check it.** | ||
| 32 | |||
| 33 | ```bash | ||
| 34 | cargo test | ||
| 35 | cargo clippy --all-targets -- -D warnings | ||
| 36 | cargo run -- build docs -o docs/_site --strict | ||
| 37 | cargo package | ||
| 38 | ``` | ||
| 39 | |||
| 40 | `cargo package` is the one people forget: it builds the crate exactly as crates.io will | ||
| 41 | receive it, and catches a file the `exclude` list should not have removed. | ||
| 42 | |||
| 43 | 4. **Verify against a real corpus.** The test suite says the code does what it did; a | ||
| 44 | corpus says the *site* does. Build a site you know with `--no-cache` and diff the | ||
| 45 | output against the previous version's. A release that quietly changes 200 pages should | ||
| 46 | do so on purpose. | ||
| 47 | |||
| 48 | 5. **Commit, tag, push.** | ||
| 49 | |||
| 50 | ```bash | ||
| 51 | git commit -am "0.18: <what changed>" | ||
| 52 | git tag -a v0.18.0 -m "0.18.0" | ||
| 53 | git push && git push --tags | ||
| 54 | ``` | ||
| 55 | |||
| 56 | 6. **Publish the crate.** | ||
| 57 | |||
| 58 | ```bash | ||
| 59 | cargo publish | ||
| 60 | ``` | ||
| 61 | |||
| 62 | This is irreversible: a published version can be yanked but never replaced. | ||
| 63 | |||
| 64 | 7. **Finish the GitHub release.** Pushing the tag builds binaries for macOS (arm64 and | ||
| 65 | x86_64) and Linux (gnu and musl) and opens a *draft* release with them attached. Paste | ||
| 66 | the changelog entry in and publish it. The draft is deliberate — a release that | ||
| 67 | publishes itself before anyone has read it cannot be edited quietly. | ||
| 68 | |||
| 69 | ## If a release goes wrong | ||
| 70 | |||
| 71 | Yank rather than delete, and ship a fix as a new version: | ||
| 72 | |||
| 73 | ```bash | ||
| 74 | cargo yank --version 0.18.0 | ||
| 75 | ``` | ||
| 76 | |||
| 77 | Yanking stops new dependents from selecting it; anyone who already has it keeps working. | ||
| 78 | Then release `0.18.1` with the fix and a changelog entry that says what happened. | ||
docs/guide/10-deploying.org +7
| @@ -112,3 +112,10 @@ built 182 page(s) (182 rendered, 0 cached), copied 3 asset(s) from content -> _s | |||
| 112 | Both zeros matter. Unresolved links are internal links pointing at nothing; diagnostics | 112 | Both zeros matter. Unresolved links are internal links pointing at nothing; diagnostics |
| 113 | are malformed org that degraded rather than failing. With =--strict= neither can reach | 113 | are malformed org that degraded rather than failing. With =--strict= neither can reach |
| 114 | this line, because either would have failed the build. | 114 | this line, because either would have failed the build. |
| 115 | |||
| 116 | * After an upgrade | ||
| 117 | |||
| 118 | The first build on a new version is worth running with =--no-cache=, so you compare the | ||
| 119 | new output to the old rather than to a cache written by both. What a version number | ||
| 120 | promises — and what it does not — is in [[file:11-versioning.org][Versioning and | ||
| 121 | upgrades]]. | ||
docs/guide/11-versioning.org added +72
| @@ -0,0 +1,72 @@ | |||
| 1 | #+TITLE: Versioning and upgrades | ||
| 2 | #+DESCRIPTION: What a version number promises, what it does not, and how to upgrade safely. | ||
| 3 | #+LEDE: The stable surface is what you build a site against, not what the code happens to do. | ||
| 4 | |||
| 5 | A generator you point at ten years of writing needs to be boring about compatibility. | ||
| 6 | This page says exactly what is promised. | ||
| 7 | |||
| 8 | * The stable surface | ||
| 9 | |||
| 10 | Changing any of this incompatibly requires a major version. | ||
| 11 | |||
| 12 | | Stable | What that covers | | ||
| 13 | |--------+------------------| | ||
| 14 | | =org-ssg.toml= keys | Their names, types and meaning. | | ||
| 15 | | Template context | =page=, =site=, =nav=, =root=, =pages=, =group=, =groups=, =paginator=, =stylesheet=, and the =absolute=, =rfc822= and =truncate= filters. | | ||
| 16 | | The CLI | Command names, flags and exit codes. | | ||
| 17 | | URLs | How a source path becomes an output path, =#+SLUG:= included. | | ||
| 18 | |||
| 19 | *URLs are on that list deliberately.* A generator that quietly moves your pages breaks | ||
| 20 | every link anyone has ever made to you, and no upgrade note fixes an inbound link. | ||
| 21 | |||
| 22 | Adding things — a new config key, a new template variable — is a minor release. Nothing | ||
| 23 | you already wrote stops working. | ||
| 24 | |||
| 25 | * What is not stable | ||
| 26 | |||
| 27 | Three things move freely, so the list above can hold still. | ||
| 28 | |||
| 29 | ** The incremental cache | ||
| 30 | |||
| 31 | =<output>/.org-ssg-cache.json= is versioned and discards itself on a mismatch. A cache | ||
| 32 | format bump means one full rebuild, and nothing else. It is never a correctness | ||
| 33 | dependency: a missing, stale or corrupt cache produces exactly the same site, more slowly. | ||
| 34 | |||
| 35 | ** Rendered HTML details | ||
| 36 | |||
| 37 | org-ssg aims at what Emacs exports from the same file, and closing a gap changes markup. | ||
| 38 | That is the product working rather than a regression — but it is called out in the | ||
| 39 | changelog every time, because your stylesheet is downstream of it. | ||
| 40 | |||
| 41 | The class names the documentation names are the ones to write CSS against: | ||
| 42 | =post-list=, =post-list-item=, =figure-number=, =table-number=, =section-number-N=, | ||
| 43 | =footnote-ref=, =verbatim=, and the =on=/=off=/=trans= classes on checkbox items. | ||
| 44 | |||
| 45 | ** The Rust API | ||
| 46 | |||
| 47 | The crate is on crates.io so the binary can be installed with =cargo install=. The library | ||
| 48 | exists to serve the binary, and its types move as the tool does. | ||
| 49 | |||
| 50 | * The compiler floor | ||
| 51 | |||
| 52 | The MSRV is *1.88*, checked in CI on every change rather than assumed — which is how it | ||
| 53 | came to be 1.88 rather than the 1.82 org-ssg's own code needs. The floor is set by | ||
| 54 | dependencies, and a dependency raising its own is invisible until someone on an older | ||
| 55 | compiler tries to build. | ||
| 56 | |||
| 57 | Raising it is a minor version, never a patch. | ||
| 58 | |||
| 59 | * Upgrading | ||
| 60 | |||
| 61 | #+BEGIN_SRC sh | ||
| 62 | cargo install org-ssg # or download a release binary | ||
| 63 | org-ssg build content -o _site --no-cache --strict | ||
| 64 | #+END_SRC | ||
| 65 | |||
| 66 | =--no-cache= makes the first build after an upgrade a full one, so you are comparing the | ||
| 67 | new version's output to the old version's output rather than to a cache written by a | ||
| 68 | mixture of both. =--strict= turns a link that stopped resolving into a failure. | ||
| 69 | |||
| 70 | If you keep your built site in version control, the diff after that command *is* the | ||
| 71 | upgrade report — which is the most useful review a generator can give you, and the reason | ||
| 72 | the changelog names behaviour rather than commits. | ||