Skip to content
include-yyPublic

About

org export backend for W3C TR style

Resources

Stars

9 stars

Watchers

0 watching

Forks

Latest commit

 

History

887 Commits

Folders and files

Repository files navigation

ox-w3ctr — org export backend for W3C TR CSS style

Based on Org-mode’s ox-html.el and CSS of W3C Technical Report tr-design’s base.css.

Status

Emacs Version30.131.132.1
Package Version0.20.30.4
Date[2025-02-23][2026-08-24]2027?

The current ox-w3ctr has not yet completed a full refactoring of the ox-html code, though it’s already largely usable. I plan to release version 0.4 when Emacs 32.1 comes out.

When version 0.4 is released, the package should no longer introduce breaking changes, and Emacs 32.1 will be the minimum supported version. In a sense, 0.4 will be the first official release.

Install and Use

Please use Emacs 32.0.50 or above. Download this repo and add it to load-path, or use package-vc like this:

M-x package-vs-install https://github.com/include-yy/ox-w3ctr RET

After installation and load, use C-c C-e w o to export a org buffer to HTML file. See more information from docstrings and comments (at least now).

The Node helper in jstools/ is optional. It renders math for the mathml-by-mathjax and svg-by-mathjax modes and highlights code for the jstools engine. It needs node on exec-path and its packages:

cd jstools && npm ci

Code highlighting

Source blocks are coloured by an engine named in org-w3ctr-fontify-method:

  • engrave (default): Emacs font-lock, through the language’s major mode.
  • jstools: Shiki (TextMate grammars, ~350 languages) in the Node helper.
  • plain or nil: no colour.

A list tries engines in order, e.g. (jstools engrave); plain always ends it. Override the method per file with #+OPTIONS: fontify:(jstools engrave) and per block with a header argument: #+begin_src python :fontify nil.

The output is <span class“ef-SLUG”>= runs; faces map to slugs through org-w3ctr-fontify-faces (and org-w3ctr-fontify-extra-faces), following :inherit. The colours, light and dark, come from org-w3ctr-fontify-palette; after changing it, run M-x org-w3ctr-fontify-update-stylesheet to rewrite the generated block of assets/style.css. A block can recolour a slug locally with #+attr__: (style "--ef-k: red").

A language no engine knows, or an engine that fails, is warned about once per export and the block exported as plain text (see org-w3ctr-fontify-unknown-language and org-w3ctr-fontify-on-error). M-x org-w3ctr-show-fontify-report lists, for the last export, which engine took how many blocks, the languages no engine took, and the faces that found no slug. See doc/fontify.org for the details: the face table, colours, the Node engine, writing an engine of your own, and how the pipeline works.

TODO list [0/4]

  1. [ ] Totally Refactor from ox-html.
  2. [ ] Fully detailed documentation.
  3. [ ] A friendly introductive video.
  4. [ ] CI/CD support.

Roadmap

  • Export entry fast path. Measured 2026-10: ~78% of corpus export time is find-file-noselect’s per-document org-mode init, not the backend. A raw-read path (insert-file-contents + buffer-file-name + default-directory + org-export-as, no org-mode) is byte-identical (norm across the 57 docs) and 4.4x faster (9.3s → 2.1s). Apply it to the batch/corpus path (yynt’s yynt--do-export already does this); per-document the saving is ~0.1s, so it only matters at batch scale. Not the backend’s job – an Org invocation concern.
  • Options tidy-up. The :options-alist is a grab-bag: a few entries are grouped, most are not. Order every entry, consider replacing cumbersome string options with *-function ones. Chart compatibility with ox-html for each option. Known divergences to chart: :html-link-use-abs-url is unimplemented (FIXME in org-w3ctr--link-path); the legacy home/up bar emits our own <nav id“navbar”>= markup, deliberately not ox-html’s <div id“org-div-home-and-up”>=. :html-home/up-format keeps its ox-html keyword compatibility (HTML_HOME/UP_FORMAT, newline). org-w3ctr-metadata-timestamp-format drops ox-html’s %a weekday abbreviation (deliberate). org-w3ctr-home/up-format, a bare format string, is the natural *-function candidate. Special blocks: with user attributes org-w3ctr--special-block-builtin replaces the type class instead of concatenating it as ox-html does (class“user TYPE”=) — deliberate (explicit over implicit). Maximum flexibility is the rule for example and source blocks: #+attr__: / #+attr_html: replace the whole attribute list, so dropping example also drops the W3C box and its “Example N” counter; write [example foo] to keep both. A captioned src block gets class“example”= only when the author supplies no attributes (the class positions the self-link and hosts the caption box).
  • Src-block feature gaps vs ox-html. Dropped when forking (judged low-value for W3C TR output). Revisit later:
    • Line numbers and retain-labels. Coderef links exist in a reduced form: [[(label)]] points at the defining block’s id, the label text is stripped from the code (as ox-html does), and the -r=/-k= switches are not implemented.
    • :html-wrap-src-lines and :html-klipsify-src.
    • Listing number in captions.
    • example-block fontification / line numbers. The token list (org-w3ctr--fontify-dispatch) is where line numbers would be cut, without touching any engine.
  • Attr-reading machinery. The attribute helpers have grown several layers (t--read-attr__, t--make-attr__, t--make-attr__id, t--make-attr__id*, t--make-attr_html, t--make-attribute-string, t--src-block-attrs). Two syntaxes coexist (Lisp s-exprs for #+attr__, plists for #+attr_html). Candidates: a single intermediate representation or one canonical syntax.
  • SVG global font cache. svg-by-mathjax uses fontCache: 'local'. MathJax’s fontCache: 'global' shares one <defs> per document (~1.9x smaller on a 100-formula sample). Tried and reverted; revisit if SVG output is kept.
  • CSS cleanup in =assets/style.css=. Not urgent.
    • Dead rules: .org-center (never emitted).
  • Client-side highlighting. The server-side engines (engrave, jstools) are done (2026-10). A client engine is designed but not built: it would emit <code class“language-LANG”>= unchanged and add highlight.js or Prism to <head> only when a document needs it. It makes the page depend on browser JavaScript, so it waits for a concrete need.
  • Special-block custom elements: a component cookbook. The :html-special-block-custom-elements registry (:template for Declarative Shadow DOM, :src and :script for the <head> scripts) is the declared seam for authoring W3C components, and that is where the mechanism stops: no org-special-block-extras- style block DSL (a defblock macro that =read=s the block header and =eval=s raw HTML) is planned. What belongs in the docs instead is a cookbook of correct component patterns for Org’s always block-level <p> light content: style :host and reset ::slotted(p) for inline components, wrap <slot> in a flow container for block components, route named slots with #+attr_html: :slot NAME, and choose :src vs :script. Theme through CSS custom properties (--w3c-*), and, as optional sugar, map a block’s :parameters (#+begin_x-badge :variant warn) onto element attributes.
  • Caption policy across blocks. #+caption: parses on nearly every block – example-block, src-block, quote-block, verse-block, center-block, special-block, table, fixed-width, latex-environment, export-block, drawer – but only four have a consumer today: the standalone-image paragraph (<figure><figcaption>), the table (<caption>), the captioned src-block (an unstyled <span> as the first child of its .example <div>), and the drawer (the caption reused as <summary>). Everything else drops the caption silently. W3C’s base.css splits a caption in two: the label and number come from a CSS counter on the container class (figcaption::before { content: "Figure " counter(figure) ". " }, .example::before gives “Example N”, and likewise .issue::before / .note::before), while the text is content the author supplies. So the back-end picks the container/class and emits the text, and must not compute numbers in Elisp – do not port ox-html’s org-export-get-ordinal / “Listing N:” machinery. Decide a mapping {element -> container/class -> caption home}, using W3C classes only (there is no .quote / .verse / .center counter):
    • figure (image paragraph) -> <figure><figcaption> (done).
    • table -> <caption> (done).
    • example-block / src-block -> .example <div>, caption in an unstyled <span> as the first child (done 2026-10-18). A bare span because .caption=/=figcaption carry the figure counter; the span renders exactly like bare text but leaves a styling hook. The .example class stays as before: always on example-block, added to a captioned src-block.
    • the rest (quote, verse, center, special, fixed-width, latex-environment, export-block) have no W3C home; do not invent one – leave the caption dropped and say so.

    Pin the invariants while unifying: is the counter class applied only when captioned (src-block’s rule) or always (example-block’s)? And is the self-link anchor emitted on every captioned block or only src-block? Decided 2026-10-17: a coderef target block gets an id but no ¶ self-link for now (the coderef link needs only the id); see the self-link TODO entry below for the deferred decision – note that .caption=/=figcaption carry the figure counter, so an example caption cannot use them.

  • Self-links on literal blocks (deferred). Headlines and captioned src blocks emit <a class“self-link”>=; example blocks do not, and the coderef-target rule (2026-10-17) deliberately skips it. The open question is the gate. The block’s id (a #+name: that becomes an id, or an explicit attribute id) is the only condition that is both explicit and free of invented syntax; reusing a begin-line switch (-n, -k) would make a coderef or line-number switch control presentation. Placement is the .example > a.self-link slot (top right), as in a captioned src block; do not use .marker, because the automatic .example::before counter would duplicate the example number.
  • Arbitrary keyword support (consider, weigh carefully). Every backend drops an unknown #+KEY: line; org-w3ctr-keyword only handles H/HTML, E, D and TOC. Emitting arbitrary keywords by default is the wrong default: keywords are metadata, not content (the corpus carries #+TMP: in 55 documents), and there is no W3C element or class for them. The explicit hatches already exist: #+HTML_HEAD: / #+HTML_HEAD_EXTRA: for the head (including <meta>), #+HTML: and export blocks for raw body markup, #+E: / #+D: for computed output. If a repeated need appears, the only shape that keeps “explicit over implicit” is an opt-in alist (key -> format string or function) consulted after the built-in branches, with a test for mapped/unmapped/built-in precedence.
  • Second check of the Lesser elements (after v0.2.19). The whole part was reviewed function by function, then edited by the coderef, caption, eval-gate and source-block passes and reordered (code references and example moved before the fontify pipeline). Re-read it as one unit: docstrings, `declare’ (ftype, important-return-value, pure), `t–pget’/`t–pput’, `t-error’, tests (coverage, section order, mutations), no stale REFINE/FIXME, and the backend transcoder / options / defcustom order matching the new source order. Another model does the first read, then pi goes over its findings. Shared gate: 320 ERT tests, strict compile, checkdoc, order-check (only the known Headline/CC disorder), test-map (293 functions, 20 uncovered), corpus (57 documents, 0 errors).
  • LaTeX / Math follow-ups.
    • org-w3ctr--format-latex keeps its ((or `nil `verbatim) nil) branch deliberately (decided 2026-10-18: “seen, not handled”). Both modes belong to ox.el, which prunes math under nil (org-export--skip-p) and expands it into plain text under verbatim (org-export--remove-uninterpreted-data) before the transcoders run, so only a direct call reaches the branch; there, nil is the honest answer, because the export drops the math. It used to return frag, which contradicted “drop the math” and, on verbatim, diverged from ox.el’s HTML-escaped plain-text path.
    • Client-side mathjax mode emits \(...\) / \[...\] without HTML escaping, so a raw < or & in TeX (e.g. $a <b > c$) can be parsed as an HTML tag before MathJax runs; pass through org-w3ctr--encode-plain-text.
    • org-w3ctr--latex-rpc wraps $...$ into \(...\) only for jstools/lib/math.js’s unwrap to regex-strip it back and infer display, and unwrap’s regex lacks ^...$ anchors (an inline \(...\) inside a \begin{equation} environment matches the inner span and drops the environment). Anchor unwrap or pass (:tex ... :display ...) explicitly.
    • org-w3ctr-latex-environment ignores #+name: and #+attr__=/=#+attr_html (the equation-link item below drops the link path, after which a #+name: on an environment is dead metadata: keep ignoring it or reject it).
    • Equation links: drop them (decided 2026-10-18). A link that resolves to a latex-environment is broken in every mode, so the back-end should stop pretending to support it. Measured:
      • mathjax=/=t: org-w3ctr--link-equation emits a dangling \eqref{...} – the back-end never injects the matching \label, so MathJax renders “??”; with prefer-user-labels off, org-w3ctr--reference falls back to a random orgXXXXXXX, so even the label is wrong.
      • nil / verbatim: ox.el prunes the element (org-export--skip-p) or expands it into plain text (org-export--remove-uninterpreted-data) before the transcoders run, and a #+name:-carrying equation is gone before org-export-resolve-fuzzy-link, so Org aborts with “Unable to resolve link: NAME” – the back-end never sees the link.

      Supported instead (measured in all three modes): an explicit target next to the equation (<<eq>> emits <span id“eq”>=), a #+name: on a wrapping block (<div id“eq” class=”…”>=), or MathJax-native \label{...} plus $\eqref{...}$ (mathjax mode only; no Org link involved). Change set: replace the latex-environment branch in org-w3ctr--link-dispatch with a t-error carrying the source line and the alternatives; delete org-w3ctr--link-equation; delete org-w3ctr-equation-reference-format with its :html-equation-reference-format option-alist entry, its org-w3ctr--oinfo-cache-props key (the source-scanning test iterates that list) and the Link section’s option comment; turn the t--link-equation test into an org-w3ctr-error assertion. Open decisions: one rule for every latex-environment (recommended) or math only; delete the option or keep it unread; and let org-w3ctr-latex-environment keep ignoring #+name: (not recommended: wrapping raw TeX in <div id> is presentation, and nil=/=verbatim lose the target anyway).

    • org-w3ctr-mathjax-config loads MathJax v3 from the CDN while jstools uses MathJax v4 (and output.font: 'mathjax-modern' is a v4 option); update the client config to v4.

Tasks

Small items, found while refining a function and usually finished in the same session. Larger or planned work is in the TODO list above.

  • =org-w3ctr-check-element-values= expects its values in reverse. The fixture push-accumulates org-w3ctr-test-values, so EXPECTED is in reverse call order (a test pins this). Either document it or nreverse it in the fixture; the latter flips nearly every EXPECTED list in the suite.
  • The OINFO source-scanning tests hardcode =info=. Both org-w3ctr--oinfo-props-are-looked-up and org-w3ctr--oinfo-props-go-through-pget match the literal variable info; a differently named INFO plist would false-fail. Loosen to [^ \t\n()]+ if that ever changes.
  • checkdoc leftovers outside the REFINE passes. Emacs 31.1’s checkdoc reports 17 warnings; 10 sit in the Link REFINE: section and its pass clears them. Seven have no owner: t-creator-string’s docstring first line is not a complete sentence (it ends at the %c placeholder); the t-export-as-html / t-export-to-html docstrings never mention ASYNC (the argument is used – it goes to org-export-to-buffer / org-export-to-file – so the docstring should say so, not the name mangled); two docstrings name the bare word org-publish where checkdoc wants it quoted; and two say jsonrpc-process-connection without “class” or “symbol” in front of it. The ox-w3ctr-lint skill’s scripts/checkdoc.el lists them.
  • =org-w3ctr–read-attr=’s error message prints the property keyword as is. Invalid attribute #+%s interpolates the keyword, so the production message reads Invalid attribute #+:attr__: ... while the Org keyword is spelled #+attr__: (the colon sits differently). A test pins the current message; if the formatting is ever fixed, flip that expectation.
  • =org-w3ctr–make-attr__id= recognizes an explicit id only as a list (=(id \u2026)=). A bare id atom in #+attr__: is not detected, so the reference id is prepended and the atom emitted as well: #+name:1 with #+attr__: id gives ~ id=”1” id~, a malformed attribute string. A test pins it; recognizing the atom form too would flip the expectation. The mirror problem on the :attr_html side: org-w3ctr--make-attr_html keys its suppression off plist-member, which sees the key regardless of value, so an explicit :id nil suppresses the auto id and emits no id at all.
  • Attribute input is not validated. In org-w3ctr--make-attr only the values go through org-w3ctr--encode-plain-text*; a name is just downcased, so ("<x>" v) gives ~ <x>=”v”~ and (1) gives ~ 1~. org-w3ctr--make-attribute-string has the same asymmetry: keys go out as is ((:<x> "v") gives <x>="v"). A dotted element falls through to a primitive wrong-type-argument (reachable as #+attr__: (a . b), which aborts the export) instead of org-w3ctr-error with context. Duplicate keys are not merged either: :class a :class b gives ~ class=”a” class=”b”~. Tests pin the current behavior; a checker that accepts only HTML attribute names and proper lists (and signals org-w3ctr-error otherwise) would flip them.
  • Silent drops in S-exp rendering and attribute formatting. org-w3ctr--sexp2html renders a non symbol/string/number child as nothing, org-w3ctr--make-attr returns nil for an attribute whose name is not convertible ((nil 1)), and org-w3ctr--read-attr__ maps a nil element of a [...] vector to an empty contribution that keeps its separator ([a nil b] gives ("class" "a b"), via mapconcat over org-w3ctr--2str with " "). Tests pin all three. They are the ;;; Basic utilities exceptions to fail-loudly: either keep them documented as deliberate (the docstrings say so now) or signal org-w3ctr-error and flip the tests.
  • Docstring & layout leftovers (from the tidy pass).
    • Add (declare (ftype …)) to the one function that still lacks it (excluding defsubst, interactive, and end-user commands): t-preamble-default-function.
  • Shorthand symbol names in docstrings and comments. They are string literals and get no read-symbol-shorthands; write the full org-w3ctr-* name. Fix per section in the passes. grep `t- in ox-w3ctr.el finds, outside the REFINE: sections, only t-style / t-style-file (<head>). The rest sit inside the Link section, which its pass will fix: t-inline-image-rules, t-inline-image-p, t-link, t--link-path, t--link-target, t--link-equation (Link).
  • Three test sections sit out of source order. order-check.el flags Link, Headline and CC license badges. Reorder them to the source function order when those sections are next touched.
  • Other transcoders take CONTENTS as =string= with no nil guard. ox.el prunes math under tex:nil, so a paragraph whose only content was pruned arrives with nil CONTENTS – org-w3ctr-paragraph used to crash in org-w3ctr--trim; fixed in the LaTeX pass (nil becomes “”, as org-w3ctr-special-block already did). The remaining transcoders declare CONTENTS as string; guard one when a nil-contents case shows up.
  • order-check cannot see a test filed under the wrong section. It checks order within a section and the order of sections only: the org-w3ctr--normalize-latex and org-w3ctr--format-latex tests sat in the Math config section while the functions live in the source LaTeX section, and nothing flagged it. Cross-section membership is checked by eye; done for the LaTeX pass.
  • Link leftovers. The refactor is done, but a few spots are still weak or suspect. Each of these carries a FIXME in the source: the cross-file ID fragment is built from t--link-path’s output instead of the raw path (t--link-to-file); :html-link-home / :html-link-use-abs-url are not implemented (t--link-path); and LaTeX equation links are to be dropped – see the Equation links item in the TODO list. One more divergence has no marker: .org.gpg files are not rewritten to .html (t--link-org-files-as-html), though ox-html does rewrite them. t--link-broken looks unreachable (Org handles broken links before the transcoder); it is FIXME-marked. (t--math-environment-p, likewise FIXME-marked, was removed – its ordinal purpose is long gone.) t--link-to-file and t--link-broken still have no tests.
  • De-engrave the naming: names describe function, provenance goes in a comment. The fontify engine borrowed its ideas (and ~5% of its lines) from engrave-faces v0.3.1, but its names say otherwise. Rename in one pass:
    • the 11 t--engrave-* functions and t--engrave-css-weights to t--fontify-*, unifying the whole subsystem under one prefix;
    • the section ;;;; Engrave-faces subset to ;;;; Fontify: font-lock engine;
    • the engine engrave to font-lock (org-w3ctr-fontify-engines, org-w3ctr-fontify-method, the :engrave key of org-w3ctr-fontify-lang-alist), so the shipped engines font-lock / jstools / plain each name their mechanism;
    • the CSS prefix ef- to tok- (--ef-*, .ef-*, the renderer, the palette, assets/style.css, the tests and doc/fontify.org).

    Then credit engrave-faces v0.3.1 in the section comment, noting only org-w3ctr--fontify-next-face-change is lifted (from htmlize, via engrave-faces). Breaking (the engine name and every highlighted span’s class), but the package has one user, so the cheapest time is now, right after 0.2.18.

  • =npm audit= flags =@xmldom/xmldom= in jstools. It comes with mathjax 4.0.0-beta.7, pinned in package-lock.json (MathJax 4.1.x is out); Shiki adds no finding. Upgrade MathJax deliberately, re-run npm test and the math export tests, rather than npm audit fix.
  • Fontify subsystem polish (from the 0.2.18 review; none urgent).
    • org-w3ctr--fontify-stylesheet lacks the proper-list-p check that org-w3ctr--fontify-face-table has, so a dotted palette slips into a primitive wrong-type-argument instead of an org-w3ctr-error with context – the most worth fixing of these.
    • org-w3ctr--fontify-engine does not validate the engine plist: an engine missing :fontify or :available fails with funcall nil rather than an org-w3ctr-error naming the engine.
    • org-w3ctr--fontify-report-text is side-effect free and signal-free, so it qualifies for (pure t), but is not marked; org-w3ctr--fontify-merge and org-w3ctr--fontify-render are.
    • The unknown-language predicate in org-w3ctr--fontify-dispatch ((and (> tried 0) (= tried declined) (eq (car result) 'plain))) is correct but opaque; a comment or a clearer formulation helps.
    • org-w3ctr--fontify-failure has an explicit warn-once guard while org-w3ctr--fontify-unavailable relies on the broken marking; the asymmetry reads as a possible oversight.
    • Efficiency, only if profiling flags the engrave engine: memoize org-w3ctr--engrave-face-slug per export (the state already has the (cache . jstools-languages) pattern); and org-w3ctr--fontify-merge builds merged text with repeated concat, O(n^2) in a pathological many-tiny-token case.
    • Efficiency, same caveat: consider reusing one scratch buffer per major mode across blocks (stashed in the fontify state) instead of a fresh with-temp-buffer + (funcall mode) per block; delay-mode-hooks already removes the dominant cost, so only if profiling shows the mode body itself as hot – and it must erase + insert + font-lock-ensure
      • collect, minding buffer-local state leaking between blocks.
  • Unnamed elements get a fresh random id on every export. With no explicit label, t--reference falls back to org-export-get-reference, which mints an orgXXXXXXX id from a randomly seeded counter. Four of the 57 corpus documents hold such ids (verify-corpus counts them in refs), and an anchor into one is not stable across exports. Never diff raw export hashes – compare the normalized norm (see the verification skill). Either the author gives every referenced element an explicit CUSTOM_ID, or the back-end derives a stable id – see Non-goals.
  • Docstring symbol quoting: the literal-apostrophe form where backquote-apostrophe is meant. The elisp-docstring rules quote a Lisp symbol as `sym'; a few docstrings write \\‘sym= instead, and the help buffer shows 'sym. Offenders: =org-w3ctr--format-latex’s MODE values (verbatim, mathjax, custom), org-w3ctr--get-info-timezone-offset / org-w3ctr--get-info-export-timezone-offset (local), org-w3ctr--get-markup-format (bold), and in the tests the t-parse1 harness docstring. org-w3ctr--eval-lisp (TYPE values) was fixed in the Lesser elements second pass; the rest sit in sections for later passes – fix per section. The literal-apostrophe use around the attribute example in org-w3ctr--make-attr__ is correct and stays.
  • An export snippet without a backend leaks a primitive error. A synthetic (export-snippet (:value \u2026)) with no :back-end dies inside org-export-snippet-backend’s (intern nil) with wrong-type-argument stringp nil instead of org-w3ctr-error. Unreachable from documents (the parser always fills :back-end; @@:x@@ is not a snippet at all), so left alone deliberately: guarding would mean reading :back-end behind the translation lookup the tests pin. Probed in scratch/_probes/lesser-export-snippet-probe.el.
  • Export blocks take no attributes or ids (deferred). Raw passthrough is the contract: a #+name: on an export block emits no anchor, so a link to it dangles (measured: [[x]] renders <a href“#x”>= with nothing to land on; a random id with org-w3ctr-prefer-user-labels off). Decided 2026-10 to leave it; the Link pass should know before touching element references. Fixed-width blocks show the mirror case (measured 2026-10-18, scratch/_probes/lesser-fixed-width-probe2.el): with named-only ids and org-w3ctr-prefer-user-labels off, [[t]] to a #+name: t block renders <a href“#orgXXXX”>= while the <pre> gets no id, and an explicit #+attr__: (id custom) leaves the link pointing at orgXXXX – both dangle. One anchor/consistency decision covers all of it.
  • Consider dropping the =:textarea= source-block form (lean: drop). org-w3ctr--textarea-block keeps ox-html’s #+attr_html: :textarea behaviour: the block renders as a <textarea> instead of <pre><code>. It is a compatibility relic with no W3C TR rationale – a form control in a spec, unstyled by base.css, and a silent output-shape switch. It is the lone survivor of the dropped ox-html src-block features (line numbers, coderef, retain-labels, …). Delete it and add it to the Roadmap’s dropped-features list; the only reason to keep it is ox-html compatibility, which this package does not promise. If kept instead, record the decision like the Export blocks entry does.

Non-goals

Explicitly out of scope for now.

  • Dependency analysis. Charting how far ox-w3ctr leans on Org and mapping the internal t-* call graph. Final global-optimization and cleanup task.
  • Cross-file link resolution (crossrefs). Resolving [[file:other.org::*Heading]] / ::#custom-id to an anchor in the target document, with a project-scoped persistent cache. Not now.
  • Drop the =ox-publish= dependency. Replace org-publish-file-relative-name, org-publish-resolve-external-link, and org-publish-org-to with local implementations. Not now.
  • Option validation. Checking user-supplied export options and customization variables for well-formedness. Options are trusted as given; the back-end verifies explicit markup only (attributes, block parameters), where malformed input fails loudly. Not now.
  • Own export framework: streaming renderer / half-parser. Replace org-export-data’s bottom-up string building and org-element parsing with a single-pass, head-to-tail renderer over a partial parser. It means dropping the Org export contract – filters, :exported-data memoization, string-returning transcoders, re-entrant call sites – not optimising org-export-data. The ceiling is small: the backend is only ~12% of export time (see next bullet). Not now.
  • Backend micro-optimization. Don’t over-optimize: the bottleneck is not the backend. Measured 2026-10 over the 57-document corpus (time, not allocation): find-file + org-mode init ~78% (avoidable with raw read + org-export-as, no find-file), org-export--annotate-info keyword/macro preprocessing ~10%, parse-tree ~4%, backend transcode walk + transcoders + fontification ~12%. ox-w3ctr is already faster than ox-html.

Scratch

Notes about using pi (the coding agent) with this repo, kept here as material for a future blog post. Not part of the package.

Skills are code the agent runs

A skill is a directory with SKILL.md (frontmatter + instructions) plus optional scripts/, references/, assets/. pi advertises only the name, description, and path at startup; the full instructions load only when the task matches. The description is therefore routing information, not documentation.

  • Write the description in three parts: what it does, when it applies (concrete triggers), and when it does not (negative routing). A “Helps with X” description has no routing value.
  • The skill-maker skill (in .agents/skills/) captures the pi spec and the create/review checklists.

Shell choice: Git Bash over MSYS2

pi runs each bash command in a fresh non-interactive shell, configured by shellPath and shellCommandPrefix in ~/.pi/agent/settings.json.

This checkout started on standalone MSYS2 (UCRT64) and moved to Git Bash for one reason: Python wheels. MSYS2’s python is a mingw build whose platform tag is mingw_x86_64_ucrt_gnu; PyPI ships rpds-py only for win_amd64 and manylinux, so pip fell back to compiling Rust from source and hung. Git Bash exposes the official Windows Python (win_amd64), and the same pip install downloads a wheel and finishes in seconds.

  • Do not activate a venv through shellCommandPrefix: the prefix runs before every command, including the env-detect probe, which must see the system Python. Keep the venv opt-in per task.
  • Machine facts live in .agents/env.md (gitignored); project facts (the venv, its deps) live in AGENTS.md, with deps pinned in requirements.txt when any exist.

env-detect: facts, fallbacks, and never overwriting a choice

The env-detect skill probes the machine and writes .agents/env.md. The script decides the values, never the agent. The probe chain for a path is: an env override (ORG_SOURCES, EMACS) -> the value recorded in env.md -> a derived default (the Org bundled with Emacs) -> FAIL.

When the recorded value is gone, the script emits org_previous and falls back; the agent must report the change rather than silently overwrite the user’s choice. Only a value the user never set falls back automatically.

About

org export backend for W3C TR style

Resources

Stars

9 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages