starting new job...
→ running command: cd "/etc/byggsteg/job-clone//orgwebalchemy/trunk" && git reset --hard && git pull
HEAD is now at b27085d feat: Add figure and name support
Already up to date.
→ running command: cd /etc/byggsteg/job-clone/orgwebalchemy/trunk && uname -a
Linux wolk-jjba 7.1.5-gnu #1 SMP PREEMPT_DYNAMIC 1 x86_64 GNU/Linux
→ running command: cd /etc/byggsteg/job-clone/orgwebalchemy/trunk && git log --name-status HEAD^..HEAD
commit b27085d4b18fdf37e6bb76b582ef97bbe645cfeb
Author: Josep Bigorra <jjbigorra@gmail.com>
Date: Wed Oct 14 14:48:29 2026 +0200
feat: Add figure and name support
M README.org
M src/orgwebalchemy/html.scm
M src/orgwebalchemy/markdown.scm
M src/orgwebalchemy/peg.scm
M test/fixtures/sample-org-features.org
M test/snapshots/sample-org-features-markdown.snap
M test/veritas/benchmark/scope.scm
M test/veritas/unit/html-spec.scm
M test/veritas/unit/markdown-spec.scm
M test/veritas/unit/peg-spec.scm
→ running command: cd /etc/byggsteg/job-clone/orgwebalchemy/trunk && maak test
scanning directories for unit tests...
scanning present working directory for unit tests...
shuffled the files with seed 821816614 (replay with VERITAS_SEED=821816614)
found unit tests: (peg-spec.scm html-spec.scm markdown-spec.scm)
running unit tests from file: peg-spec.scm, entrypoint: spec
[info] shuffled with seed 821816614 (replay with VERITAS_SEED=821816614)
------------------------------------------
[suite] Org link targets
[test] a scheme link keeps its scheme
[pass] got: //example.org/a
[pass] got: https
[pass] got: https://example.org/a
[test] a Windows drive letter is not a scheme
[pass] got: C:/tmp/x.txt
[pass] got: file
[test] a ::search suffix is kept apart from the path
[pass] got: notes.org
[pass] got: #f
[pass] got: *Intro
[pass] got: notes.org
[test] image links are recognised by extension
[pass] got: #t
[pass] got: #t
[pass] got: #t
[pass] got: #f
[pass] got: #t
[pass] got: #f
[test] custom ids and heading links become fragments
[pass] got: custom-id
[pass] got: heading
[pass] got: internal
[pass] got: My Section
[pass] got: #my-section
[pass] got: #custom-id
[test] mailto and tel links keep their scheme
[pass] got: mailto
[pass] got: tel
[pass] got: mailto:me@example.org
[test] file links and bare paths are local files
[pass] got: notes.org
[pass] got: notes.org
[pass] got: file
[pass] got: file
[pass] got: images/a.png
------------------------------------------
[suite] Named elements
[test] a named element reports its name and itself
[pass] got: (code (src-block (lang scheme) (src-content (x)
)))
[test] a prefixed cross-reference needs no rewriting
[pass] got: #tbl:data
[pass] got: #fig:arch
[test] a caption before a table keeps its place, in document order
[pass] got: ((keyword (keyword-key CAPTION) (keyword-value c)) (named (element-name n) (paragraph (line-content t))))
[test] no names in scope leaves every target as written
[pass] got: (inline-link-no-desc (link-target-content diagram))
[test] a target that says what it is is left alone
[pass] got: (inline-link-no-desc (link-target-content https://x/diagram))
[pass] got: (inline-link-no-desc (link-target-content diagram::*H))
[pass] got: (inline-link-no-desc (link-target-content #diagram))
[pass] got: (inline-link-no-desc (link-target-content nowhere))
[pass] got: (inline-link-no-desc (link-target-content *diagram))
[test] the names of a document are the names its elements carry
[pass] got: ()
[pass] got: (tbl:data diagram)
[test] a name in front of nothing stays a keyword
[pass] got: (keyword (keyword-key NAME) (keyword-value lonely))
[pass] got: ((keyword (keyword-key NAME) (keyword-value c)) (comment-line (comment-content x)))
[test] a named element still names the image below it a figure
[pass] got: (figure (figure-name diagram) (figure-image (figure-target file:d.png)))
[test] a name becomes the id of the element under it
[pass] got: (named (element-name tbl:data) (table (table-row (table-cell a ))))
[pass] got: (named (element-name q) (quote-block (quote-content x
)))
[test] a name wins over the file path it would otherwise be
[pass] got: (inline-link-no-desc (link-target-content #notes.org))
[test] a bare target naming an element becomes a fragment link
[pass] got: (inline-link-with-desc (link-target-content #diagram) (link-description-content it))
[pass] got: (inline-link-no-desc (link-target-content #diagram))
------------------------------------------
[suite] Inline constructs
[test] links with and without a description
[pass] got: (inline-link-with-desc (link-target-content u) (link-description-content d))
[pass] got: (inline-link-no-desc (link-target-content u))
[test] escaped markup characters are recognised
[pass] got: (inline-escape-char *)
[pass] got: (inline-escape-char +)
[test] raw urls carry their scheme
[pass] got: (inline-raw-url (url-scheme https://) x.org)
[test] a forced line break before end of line
[pass] got: (inline-line-break \\)
[test] footnote reference, definition and inline form
[pass] got: (inline-footnote-ref (footnote-label 1))
[pass] got: (inline-footnote-inline (footnote-content note))
[pass] got: (inline-footnote-def (footnote-label 1) (footnote-content note))
[test] superscript and subscript use their braced form
[pass] got: ((inline-text H) (inline-subscript-content 2) (inline-text O))
[pass] got: ((inline-text x) (inline-superscript-content 2))
------------------------------------------
[suite] Inline markup nodes
[test] +strike+ parses
[pass] got: (inline-strike-content strike)
[test] =verbatim= parses
[pass] got: (inline-verbatim-content verbatim)
[test] /italic/ parses
[pass] got: (inline-italic-content italic)
[test] ~code~ parses
[pass] got: (inline-code-content code)
[test] _underline_ parses
[pass] got: (inline-underline-content underline)
[test] *bold* parses
[pass] got: (inline-bold-content bold)
------------------------------------------
[suite] Mixed and nested lists
[test] an ordered list nests under an unordered item
[pass] got: (org-list (unordered-item (line-content outer) (nested-list (ordered-item (line-content n1)) (ordered-item (line-content n2)))))
[test] a list keeps every kind of item in one run
[pass] got: (org-list (unordered-item (line-content u)) (ordered-item (line-content o)) (desc-item (desc-key k) (line-content v)))
[test] a description list nests under an unordered item
[pass] got: (org-list (unordered-item (line-content term) (nested-list (desc-item (desc-key k) (line-content v)))))
[test] two levels of mixed nesting
[pass] got: (org-list (unordered-item (line-content a) (nested-list (unordered-item (line-content b) (nested-list (ordered-item (line-content c)))) (unordered-item (line-content d)))))
[test] a nested list may mix item kinds
[pass] got: (org-list (unordered-item (line-content a) (nested-list (unordered-item (line-content b)) (ordered-item (line-content c)) (unordered-item (line-content d)))))
------------------------------------------
[suite] Heading metadata
[test] a plain heading keeps its simple shape
[pass] got: (heading ** (line-content Title))
[test] a title ending in a colon is not mistaken for tags
[pass] got: (heading * (line-content Note:))
[test] a TODO keyword without a priority
[pass] got: (heading * (heading-todo DONE) (line-content Done))
[test] TODO keyword, priority and tags are extracted
[pass] got: (heading * (heading-todo TODO) (heading-priority A) (line-content Task) (heading-tags work urgent))
------------------------------------------
[suite] Document elements recognise their construct
[test] comment parses as comment-line
[pass] got: comment-line
[test] keyword parses as keyword
[pass] got: keyword
[test] verse block parses as verse-block
[pass] got: verse-block
[test] comment block parses as comment-block
[pass] got: comment-block
[test] export html parses as export-html-block
[pass] got: export-html-block
[test] mixed sibling list parses as org-list
[pass] got: org-list
[test] heading parses as heading
[pass] got: heading
[test] ordered list parses as org-list
[pass] got: org-list
[test] center block parses as center-block
[pass] got: center-block
[test] separator parses as separator
[pass] got: separator
[test] example block parses as example-block
[pass] got: example-block
[test] description list parses as org-list
[pass] got: org-list
[test] quote block parses as quote-block
[pass] got: quote-block
[test] paragraph parses as paragraph
[pass] got: paragraph
[test] table parses as table
[pass] got: table
[test] unordered list parses as org-list
[pass] got: org-list
[test] fixed width parses as fixed-width
[pass] got: fixed-width
[test] src block parses as src-block
[pass] got: src-block
------------------------------------------
[suite] Whole-document parsing
[test] the feature fixture is parsed all the way to the end
[pass] got: ((keyword (keyword-key title) (keyword-value Feature Showcase)) (keyword (keyword-key author) (keyword-value OrgWebAlchemy)) (comment-line (comment-content A single comment line.)) (heading * (line-content Introduction)) (paragraph (line-content This paragraph shows *bold*, /italic/, ~code~, =verbatim=, _underline_, +strikethrough+, x^{2}, H_{2}O.)) (paragraph (line-content A forced break here:\\)) (paragraph (line-content and then the continuation.)) (paragraph (line-content See [[https://gnu.org][GNU]] or a raw link https://example.org plus a)) (paragraph (line-content footnote reference[fn:one] and an inline footnote[fn::like this].)) (heading * (line-content Links and images)) (paragraph (line-content An external [[https://example.org/docs][documentation link]], a bare [[notes.org]],)) (paragraph (line-content an [[file:photo.png][inline image]], a bare [[images/diagram.svg]] and a jump)) (paragraph (line-content [[file:notes.org::*Introduction][into another file]].)) (paragraph (line-content A local anchor [[#custom-id][custom id]] and a heading link [[*Introduction][up top]].)) (paragraph (line-content Mail [[mailto:hello@example.org][the author]] or dial [[tel:+123456]].)) (paragraph (line-content A figure cross-reference [[fig:showcase]].)) (figure (figure-name fig:showcase) (figure-caption The showcase diagram) (figure-image (figure-target images/showcase.svg) (figure-description a showcase diagram))) (paragraph (line-content [fn:one] The footnote definition.)) (heading * (line-content Named elements)) (named (element-name tbl:metrics) (table (table-header-row (table-cell Metric ) (table-cell Value )) table-separator-row (table-row (table-cell Users ) (table-cell 1024 )))) (named (element-name feature-list) (org-list (unordered-item (line-content one)) (unordered-item (line-content two)))) (paragraph (line-content A table cross-reference [[tbl:metrics]] and one to [[feature-list]].)) (paragraph (line-content The figure [[fig:showcase]] is named too.)) (heading * (heading-todo TODO) (heading-priority A) (line-content A Task) (heading-tags work urgent)) (org-list (unordered-item (line-content [ ] unchecked item)) (unordered-item (line-content [X] checked item) (nested-list (unordered-item (line-content nested item))))) (heading * (line-content Lists)) (org-list (ordered-item (line-content first)) (ordered-item (line-content second))) (org-list (desc-item (desc-key Term) (line-content Definition))) (org-list (unordered-item (line-content outer) (nested-list (ordered-item (line-content nested ordered)) (ordered-item (line-content another)))) (desc-item (desc-key Tail) (line-content detail))) (heading * (line-content Blocks)) (src-block (lang scheme) (src-content (display "hi")
)) (example-block (example-content *literal*
)) (quote-block (quote-content Quoted *text*
)) (verse-block (verse-content line one
line two
)) (center-block (center-content centered line
)) (export-html-block (export-html-content <div class="raw">Raw HTML</div>
)) (comment-block (comment-block-content hidden comment
)) (fixed-width (fixed-width-content a fixed-width line)) (separator ---------) (table (table-header-row (table-cell Name ) (table-cell Age )) table-separator-row (table-row (table-cell Ada ) (table-cell 36 ))))
[pass] got: ((keyword (keyword-key title) (keyword-value Feature Showcase)) (keyword (keyword-key author) (keyword-value OrgWebAlchemy)) (comment-line (comment-content A single comment line.)) (heading * (line-content Introduction)) (paragraph (line-content This paragraph shows *bold*, /italic/, ~code~, =verbatim=, _underline_, +strikethrough+, x^{2}, H_{2}O.)) (paragraph (line-content A forced break here:\\)) (paragraph (line-content and then the continuation.)) (paragraph (line-content See [[https://gnu.org][GNU]] or a raw link https://example.org plus a)) (paragraph (line-content footnote reference[fn:one] and an inline footnote[fn::like this].)) (heading * (line-content Links and images)) (paragraph (line-content An external [[https://example.org/docs][documentation link]], a bare [[notes.org]],)) (paragraph (line-content an [[file:photo.png][inline image]], a bare [[images/diagram.svg]] and a jump)) (paragraph (line-content [[file:notes.org::*Introduction][into another file]].)) (paragraph (line-content A local anchor [[#custom-id][custom id]] and a heading link [[*Introduction][up top]].)) (paragraph (line-content Mail [[mailto:hello@example.org][the author]] or dial [[tel:+123456]].)) (paragraph (line-content A figure cross-reference [[fig:showcase]].)) (figure (figure-name fig:showcase) (figure-caption The showcase diagram) (figure-image (figure-target images/showcase.svg) (figure-description a showcase diagram))) (paragraph (line-content [fn:one] The footnote definition.)) (heading * (line-content Named elements)) (named (element-name tbl:metrics) (table (table-header-row (table-cell Metric ) (table-cell Value )) table-separator-row (table-row (table-cell Users ) (table-cell 1024 )))) (named (element-name feature-list) (org-list (unordered-item (line-content one)) (unordered-item (line-content two)))) (paragraph (line-content A table cross-reference [[tbl:metrics]] and one to [[feature-list]].)) (paragraph (line-content The figure [[fig:showcase]] is named too.)) (heading * (heading-todo TODO) (heading-priority A) (line-content A Task) (heading-tags work urgent)) (org-list (unordered-item (line-content [ ] unchecked item)) (unordered-item (line-content [X] checked item) (nested-list (unordered-item (line-content nested item))))) (heading * (line-content Lists)) (org-list (ordered-item (line-content first)) (ordered-item (line-content second))) (org-list (desc-item (desc-key Term) (line-content Definition))) (org-list (unordered-item (line-content outer) (nested-list (ordered-item (line-content nested ordered)) (ordered-item (line-content another)))) (desc-item (desc-key Tail) (line-content detail))) (heading * (line-content Blocks)) (src-block (lang scheme) (src-content (display "hi")
)) (example-block (example-content *literal*
)) (quote-block (quote-content Quoted *text*
)) (verse-block (verse-content line one
line two
)) (center-block (center-content centered line
)) (export-html-block (export-html-content <div class="raw">Raw HTML</div>
)) (comment-block (comment-block-content hidden comment
)) (fixed-width (fixed-width-content a fixed-width line)) (separator ---------) (table (table-header-row (table-cell Name ) (table-cell Age )) table-separator-row (table-row (table-cell Ada ) (table-cell 36 ))))
[pass] got: keyword
[test] the project's own README starts with a keyword and is long
[pass] got: 484
[pass] got: ((keyword (keyword-key title) (keyword-value OrgWebAlchemy)) (keyword (keyword-key author) (keyword-value Josep Bigorra)) (keyword (keyword-key email) (keyword-value jjbigorra@gmail.com)) (keyword (keyword-key language) (keyword-value en)) (keyword (keyword-key options) (keyword-value toc:nil num:nil)) (paragraph (line-content [[https://jointhefreeworld.org/ggg/dist/scheme-guile.svg]] [[https://jointhefreeworld.org/ggg/dist/gnu-guix.svg]])) (paragraph (line-content [[https://jointhefreeworld.org/ggg/dist/license-lgpl3+.svg]] [[https://jointhefreeworld.org/ggg/dist/license-fdl13+.svg]])) (paragraph (line-content Parse Org-mode documents and convert them with a flexible Lisp (Guile Scheme) library.)) (paragraph (line-content If you like my work, please support me by [[https://bmc.link/jjbigorra][buying me a cup of coffee ???]] so I can continue with a lot of motivation.)) (paragraph (line-content You can find the technical Guile Scheme API documentation for OrgWebAlchemy here:)) (paragraph (line-content https://jointhefreeworld.org/api-docs/orgwebalchemy/API.html)) (separator ------------------------) (heading * (line-content What OrgWebAlchemy gives you)) (paragraph (line-content This package provides among others the ~org->html~ and ~org->markdown~ functions,)) (paragraph (line-content turning Org-mode into either HTML or Markdown.)) (paragraph (line-content See the [[file:test/][tests of this library]] to understand better how to use it. This library also exposes multiple parameters which can be used to customize behavior and final result (CSS classes for example).)) (paragraph (line-content In a simplified way:)) (src-block (lang scheme) (src-content (use-modules (orgwebalchemy html))
(org->html "This is ~test~ code.")
;; results in:
;; "This is <code>test</code> code."
)) (paragraph (line-content And for Markdown:)) (src-block (lang scheme) (src-content (use-modules (orgwebalchemy markdown))
(org->markdown "This is ~test~ code.")
;; results in:
;; "This is `test` code."
)) (separator ---------------------------------) (paragraph (line-content [[https://jointhefreeworld.org/static-assets/orgwebalchemy/screen-2026-09-24-11-21-12.png]])) (separator ---------------------------------) (heading ** (line-content What is a PEG? (Parsing Expression Grammars))) (paragraph (line-content Read a [[https://en.wikipedia.org/wiki/Parsing_expression_grammar][nice explanation of PEG on Wikipedia]] or dive directly into the Guile Scheme ~(ice-9 peg)~ module and accompanying [[https://doc.guix.gnu.org/guile/3.0.9/en/html_node/PEG-Parsing.html][fantastic tutorial by GNU]].)) (paragraph (line-content Org mode looks deceptively simple. At first glance, parsing Org seems like a job for a handful of regular expressions:)) (example-block (example-content ^\\*+ +.*$
^- +.*$
^#+BEGIN_SRC
^#+END_SRC
)) (paragraph (line-content For a tiny subset of Org, that can work reasonably well. The thing is, real Org documents are not just collections of lines matching regular expressions.)) (paragraph (line-content They contain nested structures, different kinds of lists, source blocks, descriptions, links, inline markup, escaping rules, and syntax where the meaning of a piece of text depends on its surrounding context.)) (paragraph (line-content This is where =orgwebalchemy= uses *Parsing Expression Grammars (PEGs)* and their power to parse correctly and convert powerfully to an AST for later rendering (to different formats).)) (heading ** (line-content PEG instead of regex soup)) (paragraph (line-content A parser built from increasingly large regular expressions tends to become a collection of special cases. Eventually the regular expressions stop describing the language and start describing the bugs in the parser.)) (example-block (example-content if this matches, but not when that follows it,
unless we are inside a source block,
except when it is a description,
and don't consume the newline,
unless the previous line was a list item...
)) (paragraph (line-content PEG gives us a different approach.)) (paragraph (line-content Instead of asking:)) (quote-block (quote-content "Can I find a string that looks vaguely like this?"
)) (paragraph (line-content we describe the structure of the language:)) (quote-block (quote-content "An Org document consists of elements. An element can be a heading, a
paragraph, a list item, a source block, or another construct."
)) (paragraph (line-content The grammar becomes executable and testable documentation. This is also thanks to Guile Scheme's great ~(ice-9 peg)~ implementation, which supports traditional formal PEG syntax, but also PEG as S-expressions. Similar things can be achieved in other technologies and implementations, but I argue that not in such elegant fashion.)) (paragraph (line-content For example, a simplified grammar can express the structure directly:)) (src-block (lang scheme) (src-content (define-peg-pattern element body
(or empty-line
heading
src-block
unordered-item
ordered-item
paragraph))
)) (paragraph (line-content And a list item can be described in terms of its components:)) (src-block (lang scheme) (src-content (define-peg-pattern unordered-item all
(and list-bullet
ws
item-content
nl))
)) (paragraph (line-content This is much closer to the actual language than a regular expression trying to match an entire document.)) (heading ** (line-content PEG gives structure)) (paragraph (line-content One of the most important advantages of PEG is that parsing can produce a structured representation of the input.)) (paragraph (line-content For example, this:)) (example-block (example-content - name :: Joe
)) (paragraph (line-content can become something conceptually similar to:)) (src-block (lang scheme) (src-content (org-list
(desc-item
(desc-key "name")
(line-content "Joe")))
)) (paragraph (line-content The parser is therefore not merely answering:)) (example-block (example-content "Does this line match?"
)) (paragraph (line-content It is answering:)) (example-block (example-content "What is this thing, and what are its parts?"
)) (paragraph (line-content Once we have an AST, rendering to HTML, Markdown or others becomes a separate, much simpler problem:)) (example-block (example-content Org text
|
v
PEG grammar
|
v
AST
| |
v v
HTML Markdown
renderer renderer
)) (paragraph (line-content This separation keeps the parser concerned with *syntax* and the renderer concerned with *presentation*.)) (heading ** (line-content Context matters)) (paragraph (line-content Real-world Org syntax contains constructs that cannot safely be parsed by looking at one regular expression in isolation.)) (paragraph (line-content For example, a description list (also, think about nested lists and how that would be a nightmare with regex):)) (example-block (example-content - name :: Josep
- project :: orgwebalchemy
- language :: Scheme
)) (paragraph (line-content must not accidentally consume the following list item as part of the description key.)) (paragraph (line-content Similarly, a source block has different parsing rules from ordinary paragraph text:)) (example-block (example-content #+BEGIN_SRC scheme
(display "Hello")
#+END_SRC
)) (paragraph (line-content PEG lets us express these boundaries explicitly.)) (heading ** (line-content PEG is composable)) (paragraph (line-content Another reason PEG works particularly well and scales is that grammars can be built from smaller grammars.)) (paragraph (line-content We can have one level describing the document:)) (example-block (example-content document
-> elements
-> heading
-> paragraph
-> list
-> source block
)) (paragraph (line-content And another level describing inline markup:)) (example-block (example-content inline content
-> text
-> *bold*
-> /italic/
-> ~code~
-> =verbatim=
-> _underline_
-> [[links]]
-> escaped characters
)) (paragraph (line-content The block parser does not need to understand every inline construct.)) (paragraph (line-content Likewise, the inline parser does not need to understand headings or lists.)) (paragraph (line-content This gives us small, composable grammars instead of one enormous regular)) (paragraph (line-content expression.)) (heading ** (line-content PEG is not magic)) (paragraph (line-content Using PEG does not mean that Org mode suddenly becomes easy to parse.)) (paragraph (line-content Org is a large language with many historical conventions, and =orgwebalchemy= does its best to comply.)) (paragraph (line-content The important difference is that when the grammar grows, we can extend the grammar itself rather than adding another pile of increasingly fragile regular-expression substitutions.)) (separator ---------) (heading ** (line-content Why not just use regex?)) (paragraph (line-content Regular expressions are still useful.)) (paragraph (line-content They are excellent for small, local lexical problems:)) (org-list (unordered-item (line-content recognizing a simple token;)) (unordered-item (line-content validating a small piece of text;)) (unordered-item (line-content finding a simple pattern;)) (unordered-item (line-content processing already-tokenized data.))) (paragraph (line-content The problem starts when regular expressions are used as a substitute for a)) (paragraph (line-content parser.)) (paragraph (line-content Once the language contains nesting, alternatives, boundaries, context,)) (paragraph (line-content escaping, and multiple interacting constructs, a collection of regexes)) (paragraph (line-content quickly becomes difficult to reason about.)) (paragraph (line-content PEG gives us:)) (org-list (unordered-item (line-content *explicit grammar* instead of scattered matching rules;)) (unordered-item (line-content *ordered alternatives* instead of ambiguous regex chains;)) (unordered-item (line-content *composition* instead of one giant expression;)) (unordered-item (line-content *boundaries* instead of accidental over-consumption;)) (unordered-item (line-content *structured AST output* instead of strings that need to be reparsed;)) (unordered-item (line-content *maintainability* instead of an ever-growing collection of special cases.))) (paragraph (line-content In short:)) (quote-block (quote-content Regexes are great at recognizing pieces of text.
PEGs are great at describing a language.
)) (paragraph (line-content And Org mode is a language. That is why =orgwebalchemy= uses PEG.)) (separator --------------) (heading * (line-content The library API)) (paragraph (line-content OrgWebAlchemy is three small modules. Each builds on the one before it, so you)) (paragraph (line-content only pull in what you need.)) (heading ** (line-content ~(orgwebalchemy peg)~ ??? the parser)) (paragraph (line-content The parser turns Org text into an AST and knows nothing about HTML or Markdown.)) (org-list (desc-item (desc-key ~(org->ast str)~) (line-content parse a whole document. A single top-level element comes))) (paragraph (line-content back as that element's node; several come back as a list of nodes.)) (org-list (desc-item (desc-key ~(inline-org->ast str)~) (line-content parse one line of inline markup.))) (paragraph (line-content The parser also owns everything that is true about an Org *link target*, so)) (paragraph (line-content that the HTML and Markdown renderers never have to guess:)) (org-list (desc-item (desc-key ~(org-link-parts target)~) (line-content ~(protocol . path)~ for a link target.)) (unordered-item (line-content ~(org-link-protocol target)~, ~(org-link-path target)~,))) (paragraph (line-content ~(org-link-search target)~ :: the pieces of that pair.)) (org-list (desc-item (desc-key ~(org-image-link? target)~) (line-content is this target an image file or a ~data:image~ URI?)) (desc-item (desc-key ~(org-link-href target)~) (line-content the href the target should point at.)) (desc-item (desc-key ~(org-link-display target)~) (line-content the text Org shows for a link with no description.)) (desc-item (desc-key ~(org-anchor text)~) (line-content the URL fragment a heading (and a ~*Heading~ link) uses.)) (desc-item (desc-key ~(figure-parts node)~) (line-content ~(name caption target description)~ for a figure))) (paragraph (line-content node, with ~#f~ for every part the figure does not have.)) (org-list (desc-item (desc-key ~(named-parts node)~) (line-content ~(name element)~ for an element a ~#+NAME:~ line))) (paragraph (line-content names.)) (org-list (desc-item (desc-key ~(document-names ast)~) (line-content the names the elements of a parsed document carry,))) (paragraph (line-content which is what a link is resolved against.)) (org-list (desc-item (desc-key ~(element-names names)~) (line-content the parameter holding those names while a document))) (paragraph (line-content is parsed, which ~inline-org->ast~ resolves a bare link target against. The)) (paragraph (line-content two renderers bind it for the length of a document, so the resolution belongs)) (paragraph (line-content to the parser and neither renderer has to know a name list exists.)) (heading ** (line-content ~(orgwebalchemy html)~ ??? rendering to HTML)) (org-list (desc-item (desc-key ~(org->html str)~) (line-content Org text to an HTML fragment.)) (desc-item (desc-key ~(org->sxml str)~, ~(org-ast->sxml ast)~) (line-content Org to SXML, when you want to))) (paragraph (line-content post-process the tree with ~(sxml simple)~.)) (org-list (desc-item (desc-key ~(org-ast->html ast)~) (line-content a parsed document AST to an HTML fragment.)) (desc-item (desc-key ~(inline-org->html ast)~, ~(inline-org->sxml ast)~) (line-content inline markup only.)) (desc-item (desc-key ~(sxml->html-string sxml)~) (line-content the small SXML serialiser the library uses.)) (desc-item (desc-key ~(heading-id text)~) (line-content the ~((id ...))~ attributes a heading title gets, or))) (paragraph (line-content ~()~ when the title holds nothing an anchor could be made of.)) (heading ** (line-content ~(orgwebalchemy markdown)~ ??? rendering to Markdown)) (org-list (desc-item (desc-key ~(org->markdown str)~) (line-content Org text to Markdown.)) (desc-item (desc-key ~(org-ast->markdown ast)~) (line-content a parsed document AST to Markdown.)) (desc-item (desc-key ~(inline-org->markdown ast)~) (line-content inline markup only.))) (heading ** (line-content Customising the HTML output)) (paragraph (line-content Every element the HTML renderer produces reads its CSS classes from a Guile)) (paragraph (line-content parameter, so one document can be styled for any design system without touching)) (paragraph (line-content the parser:)) (src-block (lang scheme) (src-content (use-modules (orgwebalchemy html))
(parameterize ((heading-classes
(lambda (level)
(case level
((1) '("text-4xl" "font-bold"))
((2) '("text-2xl" "font-semibold"))
(else '("text-base")))))
(paragraph-classes '("leading-relaxed" "mb-4"))
(link-classes '("underline" "text-blue-600")))
(org->html "This paragraph is *bold*.\n"))
)) (paragraph (line-content The parameters are ~link-classes~, ~image-classes~, ~figure-classes~,)) (paragraph (line-content ~figure-caption-classes~, ~code-classes~,)) (paragraph (line-content ~code-block-classes~,)) (paragraph (line-content ~quote-block-classes~, ~example-block-classes~, ~italic-classes~,)) (paragraph (line-content ~bold-classes~, ~underline-classes~, ~strike-classes~, ~superscript-classes~,)) (paragraph (line-content ~subscript-classes~, ~separator-classes~, ~paragraph-classes~,)) (paragraph (line-content ~table-classes~ and its ~-header-row~, ~-header-cell~, ~-row~ and ~-cell~)) (paragraph (line-content variants, ~description-list-classes~ with ~description-term-classes~ and)) (paragraph (line-content ~description-detail-classes~, ~unordered-list-classes~ with)) (paragraph (line-content ~unordered-list-item-classes~, ~ordered-list-classes~ with)) (paragraph (line-content ~ordered-list-item-classes~, ~checkbox-classes~, ~keyword-classes~,)) (paragraph (line-content ~fixed-width-classes~, ~verse-block-classes~, ~center-block-classes~ and)) (paragraph (line-content ~heading-classes~, which is special: it is a procedure of the heading level)) (paragraph (line-content rather than a list, so each level can be styled on its own.)) (paragraph (line-content The Markdown renderer is deliberately not configurable: Markdown has no styling)) (paragraph (line-content of its own.)) (separator --------------) (heading * (line-content Supported Org features)) (org-list (unordered-item (line-content Headings (lines starting by n ~*~), including an optional TODO keyword,))) (paragraph (line-content priority (~[#A]~) and tags (~:tag1:tag2:~))) (org-list (unordered-item (line-content Paragraphs (any "non-special" text))) (unordered-item (line-content Unordered, Ordered and Description lists (with any level of nesting), with))) (paragraph (line-content checkbox items (~- [ ]~ and ~- [X]~). The three kinds may be mixed freely:)) (paragraph (line-content one list may change its bullet type part-way through, and a nested list may)) (paragraph (line-content use a different kind from its parent)) (org-list (unordered-item (line-content Italic, Bold, Underline, Strikethrough, Inline Code and Verbatim)) (unordered-item (line-content Superscript and subscript in their braced form (~x^{2}~, ~H_{2}O~))) (unordered-item (line-content Forced line breaks (~\\~ at the end of a line) and escaped markup (~\*~))) (unordered-item (line-content Links with and without description (with nested parsing) and bare URLs.))) (paragraph (line-content Targets are classified once, in the parser, so ~file:~, ~#custom-id~ and)) (paragraph (line-content ~*Heading~ links all point somewhere real, and image links render as images)) (paragraph (line-content in both renderers (see *Links and images* below))) (org-list (unordered-item (line-content Figures: a ~#+CAPTION:~ or ~#+NAME:~ line before an image link makes one))) (paragraph (line-content figure, which HTML renders as ~<figure>~ with a ~<figcaption>~ and Markdown)) (paragraph (line-content as an image with its caption, and the name is the id a ~[[fig:name]]~)) (paragraph (line-content cross-reference link resolves to (see *Figures* below))) (org-list (unordered-item (line-content Named elements: a ~#+NAME:~ line before a table, a block, a list, a separator))) (paragraph (line-content or a paragraph is that element's id, and the ~[[name]]~ and ~[[tbl:name]]~)) (paragraph (line-content links to it resolve to it in both renderers (see *Named elements* below))) (org-list (unordered-item (line-content Footnotes: references (~[fn:label]~), definitions (~[fn:label: text]~) and))) (paragraph (line-content inline footnotes (~[fn::text]~))) (org-list (unordered-item (line-content Keywords and in-buffer settings (~#+TITLE: ...~, ~#+AUTHOR: ...~, ...))) (unordered-item (line-content Comments: single lines beginning with ~#~ and ~#+begin_comment~ blocks)) (unordered-item (line-content Fixed-width lines (~: text~))) (unordered-item (line-content Horizontal separators (~--------~) five or more dashes)) (unordered-item (line-content Tables)) (unordered-item (line-content ~#+begin_src~)) (unordered-item (line-content ~#+begin_example~)) (unordered-item (line-content ~#+begin_quote~ (with nested parsing))) (unordered-item (line-content ~#+begin_verse~ and ~#+begin_center~)) (unordered-item (line-content ~#+begin_export html~ : Org syntax is parsed by your PEG grammar, but raw HTML export blocks bypass the Org inline parser and are emitted as trusted literal output.))) (heading ** (line-content Links and images)) (paragraph (line-content An Org link is not just a URL in brackets: its target says what kind of link)) (paragraph (line-content it is, and both renderers have to agree on what to do with each kind. Instead)) (paragraph (line-content of letting each renderer re-read the target, =orgwebalchemy= classifies the)) (paragraph (line-content target once, in the parser:)) (src-block (lang scheme) (src-content (use-modules (orgwebalchemy peg))
(org-link-parts "https://example.org/a") ;; => (https . "//example.org/a")
(org-link-parts "file:photo.png") ;; => (file . "photo.png")
(org-link-parts "notes.org") ;; => (file . "notes.org")
(org-link-parts "#custom-id") ;; => (internal . "custom-id")
(org-link-parts "*My Section") ;; => (heading . "My Section")
(org-link-protocol "mailto:me@example.org") ;; => mailto
(org-link-path "file://images/a.png") ;; => "images/a.png"
(org-link-href "*My Section") ;; => "#my-section"
(org-link-display "#custom-id") ;; => "custom-id"
(org-link-search "notes.org::*Heading") ;; => "*Heading"
)) (paragraph (line-content The protocol is the URL scheme when the target has one of its own, ~file~ for a)) (paragraph (line-content ~file:~ link or a bare path, ~internal~ for a ~#custom-id~ link, and ~heading~)) (paragraph (line-content for a ~*Heading~ link. ~org-image-link?~ then answers the question the image)) (paragraph (line-content rendering needs:)) (org-list (unordered-item (line-content a ~file:~ link (or bare path) is an image when its extension is one of ~png~,))) (paragraph (line-content ~jpg~, ~jpeg~, ~gif~, ~svg~, ~webp~, ~avif~, ~bmp~, ~tiff~, ~tif~ or ~ico~,)) (paragraph (line-content matched case-insensitively and with any query string ignored, so)) (paragraph (line-content ~photo.jpeg?raw=1~ counts;)) (org-list (unordered-item (line-content a ~data:image/...~ URI counts as well;)) (unordered-item (line-content a ~::search~ suffix (~file:notes.org::*Heading~) is kept out of the path, so))) (paragraph (line-content the href that comes out is still a valid file name, and ~org-link-search~)) (paragraph (line-content hands back the part that was set aside;)) (org-list (unordered-item (line-content a Windows path such as ~C:/tmp/x.txt~ is not mistaken for a link with a scheme))) (paragraph (line-content called ~c~, because a scheme has to be lower-case.)) (paragraph (line-content Both renderers then follow that single answer:)) (table (table-header-row (table-cell Org ) (table-cell HTML ) (table-cell Markdown )) table-separator-row (table-row (table-cell ~[[file:photo.png][a photo]]~ ) (table-cell ~<img src="photo.png" alt="a photo" />~ ) (table-cell ~~ )) (table-row (table-cell ~[[images/diagram.svg]]~ ) (table-cell ~<img src="images/diagram.svg" alt="..." />~ ) (table-cell ~~ )) (table-row (table-cell ~[[file:notes.org][notes]]~ ) (table-cell ~<a href="notes.org">notes</a>~ ) (table-cell ~[notes](notes.org)~ )) (table-row (table-cell ~[[#custom-id][there]]~ ) (table-cell ~<a href="#custom-id">there</a>~ ) (table-cell ~[there](#custom-id)~ )) (table-row (table-cell ~[[*My Section][up]]~ ) (table-cell ~<a href="#my-section">up</a>~ ) (table-cell ~[up](#my-section)~ )) (table-row (table-cell ~[[https://gnu.org][GNU]]~ ) (table-cell ~<a href="https://gnu.org">GNU</a>~ ) (table-cell ~[GNU](https://gnu.org)~ ))) (paragraph (line-content A heading link needs somewhere to land, so every heading carries its anchor as)) (paragraph (line-content an ~id~:)) (src-block (lang scheme) (src-content (use-modules (orgwebalchemy html))
(org->html "* My First Section\nSee [[*My First Section][up there]].\n")
;; "<h1 id=\"my-first-section\">My First Section</h1><p>See <a href=\"#my-first-section\">up there</a>.</p>"
)) (paragraph (line-content The heading's id and the link's fragment are both ~org-anchor~ of the same)) (paragraph (line-content title, so they cannot drift apart. ~heading-id~ builds the attribute, and)) (paragraph (line-content ~image-classes~ is the parameter that styles what an ~<img>~ gets, like every)) (paragraph (line-content other element class.)) (heading ** (line-content Figures)) (paragraph (line-content Org gives the element that follows a ~#+CAPTION:~ or ~#+NAME:~ keyword a)) (paragraph (line-content caption and a name. When that element is a link to an image, the caption, the)) (paragraph (line-content name and the image are one thing: a *figure*. The parser folds them together,)) (paragraph (line-content so both renderers are handed a single element, and the name a ~[[fig:name]]~)) (paragraph (line-content link points at is the very id the figure carries:)) (src-block (lang scheme) (src-content (use-modules (orgwebalchemy html))
(use-modules (orgwebalchemy markdown))
(org->html "#+NAME: fig:arch\n#+CAPTION: The architecture\n[[file:diagram.png][a diagram]]\n")
;; "<figure id=\"fig:arch\"><img src=\"diagram.png\" alt=\"a diagram\" /><figcaption>The architecture</figcaption></figure>"
(org->markdown "#+NAME: fig:arch\n#+CAPTION: The architecture\n[[file:diagram.png][a diagram]]\n")
;; "<a id=\"fig:arch\"></a>\n\n\n\n*The architecture*\n\n"
)) (paragraph (line-content A ~[[fig:arch]]~ link is an internal link, so it becomes the fragment)) (paragraph (line-content ~#fig:arch~ and lands on the id the figure carries:)) (src-block (lang scheme) (src-content (org->html "#+NAME: fig:arch\n#+CAPTION: The arch\n[[file:a.png]]\n\nSee [[fig:arch]].\n")
;; "<figure id=\"fig:arch\"><img src=\"a.png\" alt=\"The arch\" /><figcaption>The arch</figcaption></figure><p>See <a href=\"#fig:arch\">fig:arch</a>.</p>"
)) (paragraph (line-content Markdown has no ~figure~ element, so a figure becomes its image, its caption as)) (paragraph (line-content an emphasised line, and -- when it has a name -- an explicit anchor for the)) (paragraph (line-content cross-references, which is what the example above shows.)) (paragraph (line-content What counts as a figure is deliberately narrow: the keywords have to come)) (paragraph (line-content immediately before a paragraph holding exactly one image link and nothing else.)) (paragraph (line-content A caption before a table, before a non-image link, before a paragraph with any)) (paragraph (line-content other text, or before a link to an image elsewhere (~https://.../a.png~) stays)) (paragraph (line-content an ordinary keyword line -- a ~#+CAPTION:~ in front of one of those describes an)) (paragraph (line-content element this parser does not caption yet, while the ~#+NAME:~ in front of it is)) (paragraph (line-content the *Named elements* case below. Several ~#+CAPTION:~ lines join in document order,)) (paragraph (line-content one ~#+NAME:~ line gives the figure its id, and ~figure-parts~ returns)) (paragraph (line-content ~(name caption target description)~ with ~#f~ for every part the figure does not)) (org-list (ordered-item (line-content ~figure-classes~ and ~figure-caption-classes~ style the element.))) (heading ** (line-content Named elements)) (paragraph (line-content A figure is the image case of a more general rule: ~#+NAME:~ names the element)) (paragraph (line-content that follows it. Every element that renders as something a link can land on)) (paragraph (line-content can be named -- a table, a source or example block, a quote, verse or center)) (paragraph (line-content block, an export block, a list, a separator and a paragraph:)) (src-block (lang scheme) (src-content (use-modules (orgwebalchemy html))
(use-modules (orgwebalchemy markdown))
(org->html "#+NAME: tbl:users\n| Users | 1024 |\n")
;; "<table id=\"tbl:users\"><tbody><tr><td> Users </td><td> 1024 </td></tr></tbody></table>"
(org->markdown "#+NAME: tbl:users\n| Users | 1024 |\n")
;; "<a id=\"tbl:users\"></a>\n\n| Users | 1024 |\n| --- | --- |\n\n"
)) (paragraph (line-content HTML has an element to put the id on, so the id goes on it; Markdown has none)) (paragraph (line-content on a table or a block, so the same name becomes an explicit anchor, exactly as)) (paragraph (line-content it does for a figure. The id is the name, verbatim, so the name does not have)) (paragraph (line-content to follow any convention for the link to work.)) (paragraph (line-content A link to a named element resolves in either of the two ways Org resolves a)) (paragraph (line-content cross-reference:)) (org-list (unordered-item (line-content a target that already *is* a fragment -- ~#custom-id~, a ~fig:~, ~tbl:~,))) (paragraph (line-content ~tab:~, ~lst:~ or ~eq:~ name -- is classified as one by the link classifier,)) (paragraph (line-content and lands on the element's id;)) (org-list (unordered-item (line-content a bare target that is exactly the name of an element of the same document is))) (paragraph (line-content resolved when the document's inline text is parsed, so ~[[tbl:users]]~ and)) (paragraph (line-content ~[[notes]]~ both point at the element they name:)) (src-block (lang scheme) (src-content (org->html "#+NAME: tbl:users\n| Users | 1024 |\n\nSee [[tbl:users]].\n")
;; "<table id=\"tbl:users\"><tbody><tr><td> Users </td><td> 1024 </td></tr></tbody></table><p>See <a href=\"#tbl:users\">tbl:users</a>.</p>"
(org->html "#+NAME: notes\n#+BEGIN_QUOTE\n(x)\n#+END_QUOTE\n\nSee [[notes]].\n")
;; "<blockquote id=\"notes\">(x)</blockquote><p>See <a href=\"#notes\">notes</a>.</p>"
)) (paragraph (line-content The same holds for a source block, a list, a separator or a paragraph, and)) (paragraph (line-content Markdown gets an anchor in front of each of them:)) (src-block (lang scheme) (src-content (org->markdown "#+NAME: q\n#+BEGIN_QUOTE\n(x)\n#+END_QUOTE\n")
;; "<a id=\"q\"></a>\n\n> (x)\n\n"
)) (paragraph (line-content Only a bare target is resolved this way, and only when the document really does)) (paragraph (line-content carry that name: a target with a scheme of its own (~https:...~), a ~file:~)) (paragraph (line-content prefix, a leading ~#~ or ~*~, or a ~::search~ suffix keeps the meaning it wrote.)) (paragraph (line-content A name does win over the file path it would otherwise be, since a link that is)) (paragraph (line-content exactly a name in the same document is a cross-reference -- so name an element)) (paragraph (line-content something that is not also a file the document links to, and a document that)) (paragraph (line-content names nothing leaves every link as it was written.)) (paragraph (line-content What is namable is deliberately narrow, for the same reason figures are: the)) (paragraph (line-content ~#+NAME:~ line has to come immediately before the element (a blank line between)) (paragraph (line-content them ends the run), and the element has to be one that can carry an id. A name)) (paragraph (line-content in front of a heading, a comment, an empty line, another keyword line or the end)) (paragraph (line-content of the document stays an ordinary keyword line, so the parser never invents a)) (paragraph (line-content name that resolves to nothing. ~(named-parts node)~ hands back ~(name element)~.)) (paragraph (line-content A ~#+CAPTION:~ line in front of a named element is *not* folded into it the way)) (paragraph (line-content a figure's caption is: captions on tables and blocks are not rendered yet, so)) (paragraph (line-content the line stays an ordinary keyword line, in its own place:)) (src-block (lang scheme) (src-content (org->html "#+CAPTION: A table\n#+NAME: t\n| a |\n")
;; "<div>CAPTION: A table</div><table id=\"t\"><tbody><tr><td> a </td></tr></tbody></table>"
)) (heading ** (line-content Exporting to Markdown)) (paragraph (line-content The ~(orgwebalchemy markdown)~ module renders the same AST as the HTML module.)) (paragraph (line-content It exports ~org->markdown~ (an Org string), ~org-ast->markdown~ (a parsed AST))) (paragraph (line-content and ~inline-org->markdown~ (an inline AST). Markdown has no equivalent for)) (paragraph (line-content some Org constructs, so those fall back to inline HTML where that is the only)) (paragraph (line-content way to keep their meaning:)) (org-list (unordered-item (line-content underlines become ~<u>...</u>~)) (unordered-item (line-content superscript and subscript become ~<sup>...</sup>~ and ~<sub>...</sub>~)) (unordered-item (line-content strikethrough becomes ~~~text~~~)) (unordered-item (line-content description lists use the definition-list indentation convention)) (unordered-item (line-content footnotes use the ~[^label]~ and ~^[note]~ conventions))) (src-block (lang scheme) (src-content (use-modules (orgwebalchemy markdown))
(org->markdown "* Title\nSome *bold* text.\n")
;; results in:
;; "# Title\n\nSome **bold** text.\n\n"
)) (separator --------------------) (heading * (line-content Benchmarks)) (paragraph (line-content The parser and the two renderers are benchmarked with)) (paragraph (line-content [[https://codeberg.org/jjba23/veritas][Veritas]], the same test framework)) (paragraph (line-content used for the unit suite. The benchmarks live in)) (paragraph (line-content [[file:test/veritas/benchmark/scope.scm]] and are deliberately kept out of the)) (paragraph (line-content normal test run: they are selected by the test type, so nothing slows down)) (paragraph (line-content ~maak test~.)) (paragraph (line-content Run them with:)) (src-block (lang shell) (src-content VERITAS_TEST_TYPE=benchmark \
guix shell -m manifest.scm -- guile -L ./src -L ./test \
-c '((@ (veritas runner) run-tests))'
)) (paragraph (line-content Veritas profiles every workload with Guile's ~statprof~, and the suite then)) (paragraph (line-content prints the three tables below, ready to paste into this file. Each workload is)) (paragraph (line-content run five times and the fastest run is reported, because a single run is noisy)) (paragraph (line-content enough to move a figure by ten percent. On an AMD Ryzen 5 7640U, under Guile)) (paragraph (line-content 3.0.9 inside the project's Guix shell:)) (table (table-header-row (table-cell Workload ) (table-cell Iterations ) (table-cell Total (ms) ) (table-cell Per operation (??s) )) table-separator-row (table-row (table-cell Parse the feature fixture ) (table-cell 2000 ) (table-cell 617.82 ) (table-cell 308.91 )) (table-row (table-cell Parse README.org ) (table-cell 200 ) (table-cell 869.72 ) (table-cell 4348.60 )) (table-row (table-cell Render the feature fixture to HTML ) (table-cell 2000 ) (table-cell 3752.72 ) (table-cell 1876.36 )) (table-row (table-cell Render the feature fixture to Markdown ) (table-cell 2000 ) (table-cell 3393.53 ) (table-cell 1696.76 )) (table-row (table-cell Parse a 50-clause inline paragraph ) (table-cell 2000 ) (table-cell 4345.74 ) (table-cell 2172.87 ))) (paragraph (line-content Rendering is a separate pass over the AST, and HTML and Markdown cost about the)) (paragraph (line-content same, as one would expect from two renderers walking the same tree. Inline)) (paragraph (line-content parsing is still the most expensive stage per byte: a 50-clause paragraph has)) (paragraph (line-content many alternatives to try at every position.)) (heading ** (line-content Allocation and GC pressure)) (paragraph (line-content The same run measures what the work does to the heap. The heap is collected)) (paragraph (line-content before each workload, and what is reported is the allocation the workload asked)) (paragraph (line-content for -- including the garbage it immediately dropped -- so it measures)) (paragraph (line-content allocation pressure, not the memory a result keeps.)) (table (table-header-row (table-cell Workload ) (table-cell Allocated per operation ) (table-cell GC runs per 1000 ops ) (table-cell GC time per operation )) table-separator-row (table-row (table-cell Parse the feature fixture ) (table-cell 228.61 KiB ) (table-cell 28.50 ) (table-cell 0.41 ms )) (table-row (table-cell Parse README.org ) (table-cell 3226.31 KiB ) (table-cell 395.00 ) (table-cell 5.59 ms )) (table-row (table-cell Render the feature fixture to HTML ) (table-cell 1359.97 KiB ) (table-cell 168.00 ) (table-cell 2.45 ms )) (table-row (table-cell Render the feature fixture to Markdown ) (table-cell 1362.75 KiB ) (table-cell 168.00 ) (table-cell 2.38 ms )) (table-row (table-cell Parse a 50-clause inline paragraph ) (table-cell 1691.04 KiB ) (table-cell 213.00 ) (table-cell 3.03 ms ))) (paragraph (line-content The last column is Guile's own ~gc-time-taken~, which is collector *CPU* time:)) (paragraph (line-content Guile sums the time of its GC marker threads, so on a multi-core machine a)) (paragraph (line-content single full collection of a large heap is reported as several times its)) (paragraph (line-content wall-clock cost. It says how much collector work a workload causes, not what)) (paragraph (line-content share of the clock it takes. Allocation and collection counts are exact and)) (paragraph (line-content are the stable half of this table.)) (paragraph (line-content What the numbers say:)) (org-list (unordered-item (line-content Parsing ~README.org~ allocates about 3.2 MB for 45 KB of input, roughly 74))) (paragraph (line-content bytes per input byte. The collection counts say what that means in practice:)) (paragraph (line-content at this size a parse is still likely to trigger a collection of its own, but)) (paragraph (line-content the parse used to ask for twice as much and a document twice this size now)) (paragraph (line-content fits where it used to collect.)) (org-list (unordered-item (line-content What that allocation is spent on has changed twice. It used to be the text:))) (paragraph (line-content every character of ordinary prose became a one-character string and a cons)) (paragraph (line-content cell before the tree was collapsed, which is why plain prose used to allocate)) (paragraph (line-content five times as much per byte as it does now. When the first scanners landed,)) (paragraph (line-content what was left was the AST and the lists above it -- and the second round of)) (paragraph (line-content scanners, which took the text of links, footnotes, markup and description)) (paragraph (line-content keys out of the character-by-character loop as well, halved it again: plain)) (paragraph (line-content prose and a long line now allocate five and a hundred times less.)) (org-list (unordered-item (line-content Rendering allocates less than parsing, since the inline parser only sees the))) (paragraph (line-content text nodes rather than the whole document.)) (heading ** (line-content Fragments and the collapse pass)) (paragraph (line-content A parse first produces a tree of *fragments*, which a collapse pass then merges)) (paragraph (line-content into the strings they spell. The scanners now hand that pass one fragment per)) (paragraph (line-content run of text instead of one per character, so the third table the suite prints)) (paragraph (line-content counts the fragments a parse produces and times the merge:)) (table (table-header-row (table-cell Document ) (table-cell Fragments handed to the collapse pass ) (table-cell Collapse (ms) )) table-separator-row (table-row (table-cell Feature fixture (1.4 KB) ) (table-cell 82 ) (table-cell 0.046 )) (table-row (table-cell README.org ) (table-cell 922 ) (table-cell 0.460 )) (table-row (table-cell Plain prose (32 KB) ) (table-cell 400 ) (table-cell 0.216 )) (table-row (table-cell One 45 KB line ) (table-cell 1 ) (table-cell 0.007 ))) (paragraph (line-content The counts are what the scanners produced, before the merge, so they say)) (paragraph (line-content nothing about how the collapse pass is written. 32 KB of plain prose arrives)) (paragraph (line-content as 400 fragments -- one per line -- where it used to arrive as 38,400, one per)) (paragraph (line-content character, and a single 45 KB line arrives as one. What is left is the reading)) (paragraph (line-content and writing of the tree's own nodes, which is why the merge is now well under a)) (paragraph (line-content millisecond even for a document the size of this one.)) (heading ** (line-content Before and after the parser optimisation)) (paragraph (line-content Parsing used to spend most of its time deciding, character by character,)) (paragraph (line-content whether the next character could start something special, and then merging the)) (paragraph (line-content resulting per-character fragments back into strings. The scanners now consume)) (paragraph (line-content a whole run at a time: the end of a run of ordinary text is found with one)) (paragraph (line-content C-level search, and the run reaches the tree as one string.)) (table (table-header-row (table-cell Input ) (table-cell Before (ms) ) (table-cell After (ms) ) (table-cell Speedup )) table-separator-row (table-row (table-cell ~README.org~ (this file) ) (table-cell 18.341 ) (table-cell 3.892 ) (table-cell 4.71x )) (table-row (table-cell 4 KB of plain prose ) (table-cell 1.270 ) (table-cell 0.243 ) (table-cell 5.22x )) (table-row (table-cell 32 KB of plain prose ) (table-cell 10.745 ) (table-cell 2.073 ) (table-cell 5.18x )) (table-row (table-cell One 45 KB line ) (table-cell 2301.937 ) (table-cell 0.198 ) (table-cell 11632x )) (table-row (table-cell Inline markup corpus (7 KB) ) (table-cell 2.427 ) (table-cell 0.208 ) (table-cell 11.67x )) (table-row (table-cell Tables (6 KB) ) (table-cell 2.217 ) (table-cell 1.120 ) (table-cell 1.98x )) (table-row (table-cell Mixed document (8 KB) ) (table-cell 3.427 ) (table-cell 1.946 ) (table-cell 1.76x )) (table-row (table-cell ~sample-org-text.org~ (0.3 KB) ) (table-cell 0.129 ) (table-cell 0.061 ) (table-cell 2.13x )) (table-row (table-cell ~sample-org-blocks.org~ (0.3 KB) ) (table-cell 0.097 ) (table-cell 0.047 ) (table-cell 2.05x )) (table-row (table-cell ~sample-org-table.org~ (0.1 KB) ) (table-cell 0.029 ) (table-cell 0.020 ) (table-cell 1.48x ))) (paragraph (line-content The one long line is not a typo, it is the point: merging the fragments of a)) (paragraph (line-content long line was quadratic in their number, so 45 KB of single-line text cost 2.3)) (paragraph (line-content seconds on its own, for a document the parser now reads in a fifth of a)) (org-list (ordered-item (line-content Allocation came down with it -- 32 KB of prose asks for 1.9 MB))) (paragraph (line-content instead of 10.3 MB, and a single 45 KB line for 102 KB instead of 10.6 MB.)) (paragraph (line-content Allocation fell further than the clock did, which is the point of doing this)) (paragraph (line-content work in the scanner rather than in the collapse pass. Against the same parser)) (paragraph (line-content before its second round of scanners -- the same features, the same inputs, the)) (paragraph (line-content same measurement -- this file asks for 3.2 MB instead of 14.0 MB, plain prose)) (paragraph (line-content for five times less, a single 45 KB line for ninety-seven times less, and the)) (paragraph (line-content inline markup corpus for eight times less; description lists come down five and)) (paragraph (line-content a half times and a document that is mostly footnotes and links four times,)) (paragraph (line-content while the two least improved, tables and a mixed document, halve. Nothing)) (paragraph (line-content about the output changed: the same inputs parse to the same AST before and)) (paragraph (line-content after, checked input by input.)) (table (table-header-row (table-cell Input ) (table-cell Fragments before ) (table-cell Fragments after ) (table-cell Collapse before (ms) ) (table-cell Collapse after (ms) )) table-separator-row (table-row (table-cell ~README.org~ (this file) ) (table-cell 42152 ) (table-cell 922 ) (table-cell 8.066 ) (table-cell 0.430 )) (table-row (table-cell 4 KB of plain prose ) (table-cell 4800 ) (table-cell 50 ) (table-cell 0.704 ) (table-cell 0.023 )) (table-row (table-cell 32 KB of plain prose ) (table-cell 38400 ) (table-cell 400 ) (table-cell 6.084 ) (table-cell 0.186 )) (table-row (table-cell One 45 KB line ) (table-cell 48000 ) (table-cell 1 ) (table-cell 2370.666 ) (table-cell 0.004 )) (table-row (table-cell Inline markup corpus (7 KB) ) (table-cell 7400 ) (table-cell 40 ) (table-cell 1.720 ) (table-cell 0.020 )) (table-row (table-cell Tables (6 KB) ) (table-cell 5540 ) (table-cell 723 ) (table-cell 0.634 ) (table-cell 0.253 )) (table-row (table-cell Mixed document (8 KB) ) (table-cell 6060 ) (table-cell 660 ) (table-cell 0.942 ) (table-cell 0.309 ))) (paragraph (line-content These are not two runs of two programs: the parser as published (~git HEAD~))) (paragraph (line-content and the parser now are loaded into one process, given the same inputs, and)) (paragraph (line-content checked for identical output before any timing starts. All ten inputs above)) (paragraph (line-content parse to the same AST under both, so the speedup is not bought by parsing)) (paragraph (line-content something else. The inputs are deliberately figure-free, because figures are a)) (paragraph (line-content feature the published parser does not have -- the examples in this file are)) (paragraph (line-content inside source blocks, where both parsers read them as text -- and equivalence)) (paragraph (line-content for the change alone was checked separately, as *Testing* describes. The)) (paragraph (line-content ~README.org~ row is this file at the length it had when the table was measured,)) (paragraph (line-content which is also why it is the one row that moves when the documentation grows.)) (paragraph (line-content What changed:)) (org-list (unordered-item (line-content The run scanners -- the patterns that walk every line, every table cell,))) (paragraph (line-content every run of inline text, a link's target and description, a footnote's label)) (paragraph (line-content and text, the body of a markup construct, the key of a description item and)) (paragraph (line-content the body of a raw URL -- are no longer written as ~(* ...)~ or ~(+ ...)~)) (org-list (ordered-item (line-content They are hand-written scanners that find a whole run with one))) (paragraph (line-content ~string-index~ search and hand it to the tree as one string, so the number of)) (paragraph (line-content fragments a parse produces is now proportional to the document's structure)) (paragraph (line-content rather than to its length.)) (org-list (unordered-item (line-content A scanner may only stop at a character that *can* begin a construct, and it))) (paragraph (line-content stops only where the construct really begins, so ~foohttps://x~ is still a)) (paragraph (line-content link glued to a word and a stray ~*~ in prose is still just an asterisk. Where)) (paragraph (line-content the grammar decided something by looking ahead at a character that does not)) (paragraph (line-content end the run -- a description key stops only where a ~ :: ~ really separates it)) (paragraph (line-content from its detail, a markup body stops at a delimiter only when that delimiter)) (paragraph (line-content closes it -- the scanner asks the pattern itself, so the two cannot drift.)) (org-list (unordered-item (line-content The collapse pass no longer uses ~(ice-9 peg)~'s ~string-collapse~, whose))) (paragraph (line-content ~string-group~ builds each run of adjacent strings by consing onto the run it)) (paragraph (line-content has built so far and is quadratic in the length of that run. ~collapse-tree~)) (paragraph (line-content implements the same contract in a single pass.)) (heading ** (line-content Where the time goes now)) (org-list (unordered-item (line-content For ~README.org~, the collapse pass is 0.4 ms of the 3.9 ms the parse takes,))) (paragraph (line-content measured by timing the stage on its own. In the published parser it was 8.1)) (paragraph (line-content ms of 18.3 ms, and it was the part that grew faster than the input.)) (org-list (unordered-item (line-content ~(ice-9 peg)~ already memoises: every pattern defined with))) (paragraph (line-content ~define-peg-pattern~ is wrapped by ~cg-cached-parser~ in a fixed 512-entry)) (paragraph (line-content direct-mapped cache keyed on the position. That cache is not what these)) (paragraph (line-content documents wait for -- the cost was inside the ~*~ and ~+~ scanners, which are)) (paragraph (line-content compiled inline, are not separately cached, and allocated one string per)) (paragraph (line-content character matched. The cache size is inlined when each pattern is compiled,)) (paragraph (line-content so raising ~*cache-size*~ at run time does not affect patterns that are)) (paragraph (line-content already defined; measuring it that way was inconclusive rather than)) (paragraph (line-content informative.)) (org-list (unordered-item (line-content What is left is allocation: ~README.org~ asks for about 74 bytes per input))) (paragraph (line-content byte, and most of that is the AST and the lists it is built from rather than)) (paragraph (line-content text fragments, so the next win would be in how the tree is built -- the)) (paragraph (line-content ~cons~-and-reverse the pattern compiler emits for every node -- not in how)) (paragraph (line-content the text is scanned.)) (paragraph (line-content These are indicative numbers, not guarantees. Wall-clock figures move by ten to)) (paragraph (line-content twenty percent between runs -- these come from a laptop that was compiling and)) (paragraph (line-content browsing while they ran -- while the allocation figures stay within a few)) (paragraph (line-content percent and the fragment counts are exact. Reproduce everything with the)) (paragraph (line-content command above.)) (heading ** (line-content What the benchmarks do not cover)) (paragraph (line-content The benchmarks measure this implementation on its own. They do not compare it)) (paragraph (line-content against other Org implementations, and they do not measure the incremental use)) (paragraph (line-content case where a document is re-parsed after a small edit. Both would be welcome)) (paragraph (line-content additions; see the OrgWebAlchemy Project section at the end of this document.)) (heading * (line-content Testing)) (paragraph (line-content The project is tested with [[https://codeberg.org/jjba23/veritas][Veritas]], a)) (paragraph (line-content testing framework for Guile. Suites live under [[file:test/veritas/unit/]] and)) (paragraph (line-content cover the grammar ([[file:test/veritas/unit/peg-spec.scm]]), the HTML renderer)) (paragraph (line-content ([[file:test/veritas/unit/html-spec.scm]]) and the Markdown renderer)) (paragraph (line-content ([[file:test/veritas/unit/markdown-spec.scm]]). They use table-driven)) (paragraph (line-content ~test-each~ and ~suite-each~ cases, snapshot testing for whole documents, and a)) (paragraph (line-content range of assertions (~assert-equal~, ~assert-contains~, ~assert-matches~,)) (paragraph (line-content ~assert-length~, ~assert-no-error~, ...). The parser suite covers link target)) (paragraph (line-content classification, heading anchors, figures, named elements and the run scanners,)) (paragraph (line-content and each renderer has its own *Links and images*, *Figures* and *Named)) (paragraph (line-content elements* suites for the output it produces from them.)) (paragraph (line-content Two checks the suites cannot express live beside them, because they compare a)) (paragraph (line-content parser against a *previous* one rather than against an expectation. Both were)) (paragraph (line-content run when the scanners were rewritten and are worth repeating after any change)) (paragraph (line-content to the grammar:)) (org-list (unordered-item (line-content *Equivalence.* The parser as it was and the parser being changed are loaded))) (paragraph (line-content into one process -- the earlier one under a renamed module, compiled from)) (paragraph (line-content ~git show REV:src/orgwebalchemy/peg.scm~ -- given the same inputs, and every)) (paragraph (line-content whole-document and inline AST compared. The run-scanner change was checked)) (paragraph (line-content this way on 70,858 inputs: every pair of interesting characters (printable)) (paragraph (line-content ASCII, tab, newline, carriage return, the other control characters and two)) (paragraph (line-content multi-byte characters) as documents, as inline text, and inside a link, a)) (paragraph (line-content footnote and a description list, this README, hand-written edge cases (markup)) (paragraph (line-content at every position, tables with empty cells, lines without a newline) and 5,800)) (paragraph (line-content randomly generated documents. Zero differences -- with one deliberate)) (paragraph (line-content exception, below.)) (org-list (unordered-item (line-content *The stop set.* A run scanner may only leap over characters it knows cannot))) (paragraph (line-content begin a construct, so ~inline-stop-chars~ is checked against the grammar)) (paragraph (line-content itself: at every position where ~inline-special~ really matches, the)) (paragraph (line-content character that begins it has to be in the set. Sweeping 391,094 positions of)) (paragraph (line-content the README, the fixtures, every pair of bytes and random markup -- 6,024)) (paragraph (line-content constructs -- found no gap.)) (paragraph (line-content The one deliberate difference: a run of two or more ~#+CAPTION:~ or ~#+NAME:~)) (paragraph (line-content lines in front of an element that is not a figure used to come out of the)) (paragraph (line-content folder *reversed*. It now keeps document order, which is what the surrounding)) (paragraph (line-content documentation always claimed and what a keyword run should do; the equivalence)) (paragraph (line-content run reports it as a deliberate change rather than a difference, and a unit test)) (paragraph (line-content pins it down. Everything else about the two rounds of scanner work is)) (paragraph (line-content output-identical, and the new ~#+NAME:~ feature is a change to the language, so)) (paragraph (line-content it is covered by tests rather than by equivalence.)) (paragraph (line-content One trap when adding tests: ~veritas~ collects a test's assertions by filtering)) (paragraph (line-content the test body for procedures, so assertions wrapped in a ~let~, ~when~ or)) (paragraph (line-content ~parameterize~ are silently reduced to the last one. Keep every assertion at)) (paragraph (line-content the top level of the test body, and call a helper when a parameterization is)) (paragraph (line-content needed (the class-parameter tests show how).)) (paragraph (line-content Run the unit suite with:)) (src-block (lang shell) (src-content guix shell -m manifest.scm -- guile -L ./src -L ./test \
-c '((@ (veritas runner) run-tests))'
)) (paragraph (line-content or with ~maak test~. Fixtures under [[file:test/fixtures/]] are shared between)) (paragraph (line-content the suites and the benchmarks, so both stay honest about the same documents.)) (heading * (line-content Projects that use Orgwebalchemy)) (org-list (desc-item (desc-key LucidPlan) (line-content https://codeberg.org/jjba23/lucidplan)) (desc-item (desc-key WikiMusic) (line-content https://codeberg.org/jjba23/wikimusic))) (separator --------------) (heading * (line-content Licensing)) (paragraph (line-content orgwebalchemy and all of its source code are free software, licensed under the GNU Lesser General Public License v3 (or newer at your convenience).)) (paragraph (line-content https://www.gnu.org/licenses/lgpl-3.0.html)) (paragraph (line-content The documentation and examples, including this document, which are provided with orgwebalchemy, are all licensed under the GNU Free Documentation License v1.3 (or newer at your convenience).)) (paragraph (line-content https://www.gnu.org/licenses/fdl-1.3.html)) (separator ---------) (heading * (line-content Installing and using)) (paragraph (line-content orgwebalchemy is on its way to be packaged via the Guix package manager officially, as ~guile-orgwebalchemy~. It is recommended you use Guix to work on your Guile Scheme projects, and install orgwebalchemy with it (in your manifest.scm for example).)) (paragraph (line-content To install it you can download the source code files and add them directly to your ~GUILE_LOAD_PATH~ or even better, use a Guix shell ~manifest.scm~ and add the package with something like this:)) (src-block (lang scheme) (src-content (define-public guile-orgwebalchemy
(package
(name "guile-orgwebalchemy")
(version "x.x.x")
(source
(origin
(method git-fetch)
(uri (git-reference
(url "https://codeberg.org/jjba23/orgwebalchemy.git")
(commit (string-append "v" version))))
(file-name (git-file-name name version))
(sha256
(base32 "<depends on version>"))))
(build-system guile-build-system)
(native-inputs (list guile-3.0))
(arguments
(list
#:source-directory "src"))
(home-page "https://codeberg.org/jjba23/orgwebalchemy")
(synopsis
"Convert Org markup to pretty HTML with a Lisp (Guile Scheme) library")
(description
"Convert Org markup to pretty HTML with a Lisp (Guile Scheme) library")
(license license:lgpl3+)))
)) (separator -------------) (heading * (line-content Code of conduct)) (paragraph (line-content This project adheres to the jointhefreeworld code of conduct. Find it here:)) (paragraph (line-content https://jointhefreeworld.org/blog/articles/personal/jointhefreeworld-code-of-conduct/index.html)) (paragraph (line-content In summary, we foster an inclusive, respectful, and cooperative environment for all contributors and users of this free software project. Inspired by the ideals of the GNU Project, we strive to uphold freedom, equality, and community as guiding principles. We believe that collaboration in a community of mutual respect is essential to creating excellent free software.)) (separator -------) (heading * (line-content OrgWebAlchemy Project)) (paragraph (line-content Contributing to free software is a uniquely beautiful act because it embodies principles of generosity, collaboration, and empowerment.)) (paragraph (line-content We welcome everyone to feel invited to the orgwebalchemy Project, and encourage active contribution in all forms, to improve it and/or suggest improvements, brainstorm with me, make it more modular/flexible, etc, feel free to contact me <jjbigorra@gmail.com> to chat, discuss or report feedback.)) (paragraph (line-content Find here the Backlog and Kanban boards for orgwebalchemy: https://lucidplan.jointhefreeworld.org/tickets/orgwebalchemy)) (separator -------) (paragraph (line-content [[https://jointhefreeworld.org/static-assets/orgwebalchemy/orgwebalchemy-small.png]])) (separator ---------) (heading * (line-content What people are saying about OrgWebAlchemy)) (org-list (unordered-item (line-content https://jointhefreeworld.org/blog/articles/lisps/parsing-expression-grammar-lisp-org-convert-to-html/index.html)) (unordered-item (line-content https://www.reddit.com/r/lisp/comments/1wox8h4/parsing_org_mode_with_parsing_expression_grammars/))))
[pass] got: keyword
------------------------------------------
[suite] Run scanners
[test] a run ends where an inline construct really begins
[pass] got: ((inline-text a) (inline-bold-content b))
[pass] got: ((inline-text see) (inline-raw-url (url-scheme https://) example.org/a))
[test] a plain inline run is one string, not one per character
[pass] got: (inline-text the h f s w g t m i r letters)
[pass] got: (inline-text plain text here)
[test] a tab and non-ASCII text are ordinary line content
[pass] got: (paragraph (line-content a b))
[pass] got: (paragraph (line-content caf? ??))
[test] a lone carriage return is content, as it always was
b )))pass] got: (table (table-row (table-cell a
[pass] got: (table (table-row (table-cell a
[pass] got: (paragraph (line-content a))
[test] the last line of a document without a newline parses
[pass] got: (paragraph (line-content text))
[test] a table row needs no trailing newline
[pass] got: (table (table-row (table-cell a ) table-cell))
[test] a line ends at a newline, and a table cell at its separator
[pass] got: (table (table-row (table-cell )))
[pass] got: (table (table-row (table-cell a ) (table-cell b )))
[test] a long line is one string, not one fragment per character
[pass] got: 10000
------------------------------------------
[suite] Heading anchors
[test] a heading link uses the anchor of its title
[pass] got: #my-section
[test] titles become URL fragments
[pass] got:
[pass] got: spaced-punct
[pass] got: a-b
[pass] got: my-first-section
------------------------------------------
[suite] Figures
[test] a figure reports its parts, missing ones as #f
[pass] got: (#f A caption file:a.png #f)
[pass] got: (fig:arch The architecture file:diagram.png a diagram)
[test] a name, a caption and an image become one figure
[pass] got: (figure (figure-name fig:arch) (figure-caption The architecture) (figure-image (figure-target file:diagram.png) (figure-description a diagram)))
[test] a caption alone is still a figure
[pass] got: (figure (figure-caption A caption) (figure-image (figure-target file:a.png) (figure-description alt)))
[test] a figure keeps its place among the paragraphs around it
[pass] got: ((paragraph (line-content before)) (figure (figure-caption c) (figure-image (figure-target file:a.png))) (paragraph (line-content after)))
[test] a name alone is still a figure
[pass] got: (figure (figure-name fig:x) (figure-image (figure-target images/x.svg)))
[test] an empty name is no name
[pass] got: (keyword (keyword-key NAME) keyword-value)
[test] a link to an image elsewhere is no local figure
[pass] got: (keyword (keyword-key CAPTION) (keyword-value c))
[test] several captions join in document order
[pass] got: (figure (figure-caption One two) (figure-image (figure-target file:a.png)))
[test] a caption describing something else stays a keyword
[pass] got: (keyword (keyword-key CAPTION) (keyword-value Not a figure))
[pass] got: (keyword (keyword-key CAPTION) (keyword-value A table))
[info] ran 11 tasks in: 0 seconds, 37816 microseconds
running unit tests from file: html-spec.scm, entrypoint: spec
[info] shuffled with seed 821816614 (replay with VERITAS_SEED=821816614)
------------------------------------------
[suite] Mixed and nested list types
[test] a nested list may mix item kinds
[pass] got: <ul><li>a<ul><li>b</li></ul><ol><li>c</li></ol><ul><li>d</li></ul></li></ul>
[test] consecutive kinds become separate list elements
[pass] got: <ul><li>u</li></ul><ol><li>o</li></ol><dl><dt>k</dt><dd>v</dd></dl>
[test] a description list nests under an unordered item
[pass] got: <ul><li>term<dl><dt>k</dt><dd>v</dd></dl></li></ul>
[test] an ordered list nests under an unordered item
[pass] got: <ul><li>outer<ol><li>n1</li><li>n2</li></ol></li></ul>
------------------------------------------
[suite] Feature fixture
[test] the whole fixture renders without error
[pass] success running without errors
[test] tables still render their header
[pass] got: <div>title: Feature Showcase</div><div>author: OrgWebAlchemy</div><!-- A single comment line. --><h1 id="introduction">Introduction</h1><p>This paragraph shows <strong>bold</strong>, <em>italic</em>, <code>code</code>, <code>verbatim</code>, <u>underline</u>, <del>strikethrough</del>, x<sup>2</sup>, H<sub>2</sub>O.</p><p>A forced break here:<br /></p><p>and then the continuation.</p><p>See <a href="https://gnu.org">GNU</a> or a raw link <a href="https://example.org">https://example.org</a> plus a</p><p>footnote reference<sup class="footnote-ref"><a href="#fn-one" id="fnref-one">one</a></sup> and an inline footnote<sup class="footnote-inline">like this</sup>.</p><h1 id="links-and-images">Links and images</h1><p>An external <a href="https://example.org/docs">documentation link</a>, a bare <a href="notes.org">notes.org</a>,</p><p>an <img src="photo.png" alt="inline image" />, a bare <img src="images/diagram.svg" alt="images/diagram.svg" /> and a jump</p><p><a href="notes.org">into another file</a>.</p><p>A local anchor <a href="#custom-id">custom id</a> and a heading link <a href="#introduction">up top</a>.</p><p>Mail <a href="mailto:hello@example.org">the author</a> or dial <a href="tel:+123456">tel:+123456</a>.</p><p>A figure cross-reference <a href="#fig:showcase">fig:showcase</a>.</p><figure id="fig:showcase"><img src="images/showcase.svg" alt="a showcase diagram" /><figcaption>The showcase diagram</figcaption></figure><p><sup class="footnote-ref"><a href="#fn-one" id="fnref-one">one</a></sup> The footnote definition.</p><h1 id="named-elements">Named elements</h1><table id="tbl:metrics"><thead><tr><th> Metric </th><th> Value </th></tr></thead><tbody><tr><td> Users </td><td> 1024 </td></tr></tbody></table><ul id="feature-list"><li>one</li><li>two</li></ul><p>A table cross-reference <a href="#tbl:metrics">tbl:metrics</a> and one to <a href="#feature-list">feature-list</a>.</p><p>The figure <a href="#fig:showcase">fig:showcase</a> is named too.</p><h1 class="org-todo org-todo-todo" id="a-task"><span class="org-todo">TODO</span><span class="org-priority">[#A]</span>A Task<span class="org-tags"><span class="org-tag">work</span><span class="org-tag">urgent</span></span></h1><ul><li><input type="checkbox" disabled="disabled" />unchecked item</li><li><input type="checkbox" disabled="disabled" checked="checked" />checked item<ul><li>nested item</li></ul></li></ul><h1 id="lists">Lists</h1><ol><li>first</li><li>second</li></ol><dl><dt>Term</dt><dd>Definition</dd></dl><ul><li>outer<ol><li>nested ordered</li><li>another</li></ol></li></ul><dl><dt>Tail</dt><dd>detail</dd></dl><h1 id="blocks">Blocks</h1><pre class="scheme">(display "hi")
</pre><pre>*literal*
</pre><blockquote>Quoted <strong>text</strong></blockquote><p>line one<br />line two</p><div><p>centered line</p></div><div class="raw">Raw HTML</div>
<!-- hidden comment
--><pre>a fixed-width line
</pre><hr /><table><thead><tr><th> Name </th><th> Age </th></tr></thead><tbody><tr><td> Ada </td><td> 36 </td></tr></tbody></table>
[test] raw HTML export passes through untouched
[pass] got: <div class="raw">Raw HTML</div>
------------------------------------------
[suite] Figures
[test] a named figure carries its name as its id
[pass] got: <figure id="fig:arch"><img src="diagram.png" alt="diagram.png" /></figure>
[test] a captioned image renders as a figure with its caption
[pass] got: <figure><img src="diagram.png" alt="a diagram" /><figcaption>The architecture</figcaption></figure>
[test] figure classes can be customised
[pass] got: <figure class="numbered" id="fig:x"><img src="a.png" alt="c" /><figcaption class="caption">c</figcaption></figure>
[test] a caption names an image with no alt text of its own
[pass] got: <figure><img src="a.png" alt="A caption" /><figcaption>A caption</figcaption></figure>
[test] a cross-reference link points at the figure
[pass] got: <figure id="fig:arch">
[pass] got: <a href="#fig:arch">fig:arch</a>
------------------------------------------
[suite] Extended block elements
[test] keywords and comments
[pass] got: <!-- hidden
-->
[pass] got: <div>TITLE: Hello</div>
[pass] got: <!-- a comment -->
[test] checkbox list items
[pass] got: <ul><li><input type="checkbox" disabled="disabled" />todo</li><li><input type="checkbox" disabled="disabled" checked="checked" />done</li></ul>
[test] fixed-width, verse and center blocks
[pass] got: <pre>fixed
</pre>
[pass] got: <p>one<br />two</p>
[pass] got: <div><p>mid</p></div>
------------------------------------------
[suite] SXML to HTML String Rendering
[test] rendering single element vs list of elements
[pass] got: <h1>Title</h1><p>Body</p>
[pass] got: <span>text</span>
[test] rendering raw HTML nodes verbatim without escaping
[pass] got: <span>Raw</span>
[test] rendering *TOP* wrapped node tree
[pass] got: <p>Hello</p><p>World</p>
------------------------------------------
[suite] Named elements
[test] a named table carries its name as its id
[pass] got: <table id="tbl:data"><tbody><tr><td> a </td></tr></tbody></table>
[test] a prefixed cross-reference link points at the element
[pass] got: <a href="#tbl:data">tbl:data</a>
[test] the id joins the classes an element already has
[pass] got: <pre id="code" class="scheme">(x)
</pre>
[test] a name the document does not carry is left a file link
[pass] got: <a href="nowhere">nowhere</a>
[test] a bare link to a name points at the element as well
[pass] got: <a href="#diagram">diagram</a>
[pass] got: <figure id="diagram">
------------------------------------------
[suite] List transformations
[test] unordered and ordered lists transformation
[pass] got: <ul><li>Item 1</li><li>Item 2</li></ul>
[pass] got: <ol><li>First</li><li>Second</li></ol>
[test] nested description list transformation
[pass] got: <dl><dt>Term</dt><dd>Definition text<dl><dt>Term2</dt><dd>Def2</dd></dl></dd></dl>
[test] description list transformation
[pass] got: <dl><dt>Term</dt><dd>Definition text</dd></dl>
[test] nested unordered and nested ordered lists transformation
[pass] got: <ol><li>Item 1<ol><li>Item 1.1</li><li>Item 1.2</li></ol></li></ol>
[pass] got: <ul><li>Item 1<ul><li>Item 1.1</li><li>Item 1.2</li></ul></li></ul>
------------------------------------------
[suite] Links and images
[test] a heading link points at the heading anchor
[pass] got: <h1 id="notes">Notes</h1><p>See <a href="#notes">there</a>.</p>
[test] file links lose their file: prefix
[pass] got: <p><a href="notes.org">notes.org</a></p>
[pass] got: <p><a href="notes.org">notes</a></p>
[test] a ::search suffix is not part of the href
[pass] got: <p><a href="notes.org">notes</a></p>
[test] an image link renders as an img
[pass] got: <p><img src="photo.png" alt="a photo" /></p>
[pass] got: <p><img src="images/diagram.svg" alt="images/diagram.svg" /></p>
[test] image classes can be customised
[pass] got: <p><img class="rounded" src="photo.png" alt="photo.png" /></p>
[test] a custom id link stays a fragment
[pass] got: <p><a href="#custom-id">there</a></p>
------------------------------------------
[suite] Heading metadata
[test] plain headings carry an anchor id
[pass] got: <h1 id="plain">Plain</h1>
[test] TODO keyword, priority and tags render
[pass] got: <h1 class="org-todo org-todo-todo" id="a-task"><span class="org-todo">TODO</span><span class="org-priority">[#A]</span>A Task<span class="org-tags"><span class="org-tag">work</span><span class="org-tag">urgent</span></span></h1>
------------------------------------------
[suite] Org Table Transformation
[test] table error resilience
[pass] success running without errors
[test] table rendering with header and body
[pass] got: <table><thead><tr><th> Name </th><th> Age </th><th> City </th></tr></thead><tbody><tr><td> Alice </td><td> 30 </td><td> London </td></tr></tbody><tbody><tr><td> Bob </td><td> 25 </td><td> Berlin </td></tr></tbody></table>
------------------------------------------
[suite] Extended inline markup
[test] forced line break and escaped markup
[pass] got: +
[pass] got: line<br />
[test] footnote reference, definition and inline form
[pass] got: <span class="footnote-def" id="fn-1"><sup class="footnote-label">1</sup> note</span>
[pass] got: <sup class="footnote-inline">note</sup>
[pass] got: see<sup class="footnote-ref"><a href="#fn-1" id="fnref-1">1</a></sup>
[test] strikethrough, superscript and subscript
[pass] got: x<sup>2</sup>
[pass] got: <del>struck</del>
[pass] got: H<sub>2</sub>O
------------------------------------------
[suite] Block Org AST & Document Parsing
[test] headings AST transformation
[pass] got: <h2 id="level-2">Level 2</h2>
[pass] got: <h3 id="level-3">Level 3</h3>
[pass] got: <h1 id="level-1">Level 1</h1>
[test] paragraph transformation
[pass] got: <p>A simple paragraph with <strong>bold</strong> text.</p>
[test] special blocks: quote, example, src, export, separator
[pass] got: <hr />
[pass] got: <pre class="scheme">(define x 10)
</pre>
[pass] got: <blockquote>Quoted <strong>text</strong></blockquote>
[pass] got: <pre>*literal* code
</pre>
[pass] got: <div>Raw content</div>
------------------------------------------
[suite] Class Parameter Customization
[test] customizing heading classes dynamically
[pass] got: <h2 class="text-xl" id="section">Section</h2>
[pass] got: <h1 class="text-3xl font-bold" id="main-title">Main Title</h1>
[test] customizing block element classes
[pass] got: <p class="leading-relaxed mb-4">Paragraph text.</p>
[pass] got: <pre class="python bg-gray-900 p-4">print(1)
</pre>
[pass] got: <ul class="list-disc ml-6"><li>item</li></ul>
[test] customizing inline markup classes
[pass] got: <a class="underline text-blue-600" href="/path">link</a>
[pass] got: <strong class="font-bold text-red-500">styled bold</strong>
------------------------------------------
[suite] Full Org-to-HTML Integration
[test] sample org text parsing without errors
[pass] success running without errors
[test] sample org blocks parsing without errors
[pass] success running without errors
------------------------------------------
[suite] Inline Org Transformation
[test] nested inline markup
[pass] got: <strong>bold with <em>italic</em></strong>
[pass] got: <u>underlined <a href="https://gnu.org">link</a></u>
[test] basic inline attributes
[pass] got: <a href="https://gnu.org">GNU</a>
[pass] got: <em>italic</em>
[pass] got: <u>underlined</u>
[pass] got: <a href="https://gnu.org">https://gnu.org</a>
[pass] got: <code>verbatim</code>
[pass] got: <code>code</code>
[pass] got: <strong>bold</strong>
[pass] got: cron job / shepherd timer that does git fetch --all for work and <code>~/hacking</code>
[pass] got: Unsure if Option A/B
------------------------------------------
[suite] make-sxml-attrs
[test] constructs a class attribute when classes are supplied
[pass] got: ((@ (class heading)))
[pass] got: ((@ (class btn btn-primary active)))
[test] returns empty list when no classes or attributes provided
[pass] got: ()
[pass] got: ()
[pass] got: ()
[test] constructs a class attribute when classes and attributes are supplied
[pass] got: ((@ (class heading) (href /about)))
[pass] got: ((@ (class btn btn-primary active) (href /about)))
[test] constructs attributes when they are supplied
[pass] got: ((@ (id main-header) (role banner)))
[pass] got: ((@ (href /home)))
------------------------------------------
[suite] Inline Raw URL Transformation
[test] supports WebSocket protocols
[pass] got: <a href="ws://example.org/socket">ws://example.org/socket</a>
[pass] got: <a href="wss://example.org/socket">wss://example.org/socket</a>
[test] supports remote access and network protocols
[pass] got: <a href="git://example.org/project.git">git://example.org/project.git</a>
[pass] got: <a href="ssh://git@example.org/repo">ssh://git@example.org/repo</a>
[pass] got: <a href="gopher://example.org/1/hello">gopher://example.org/1/hello</a>
[pass] got: <a href="sftp://example.org/path/to/file">sftp://example.org/path/to/file</a>
[test] supports URLs with paths, queries, fragments and ports
[pass] got: <a href="https://example.org/page#section">https://example.org/page#section</a>
[pass] got: <a href="https://example.org:8443/path/to/page">https://example.org:8443/path/to/page</a>
[pass] got: <a href="https://example.org/search?q=org-mode&lang=en">https://example.org/search?q=org-mode&lang=en</a>
[test] URLs can appear inside ordinary prose
[pass] got: Visit <a href="https://example.org">https://example.org</a> for more information.
[pass] got: Clone <a href="git://example.org/project.git">git://example.org/project.git</a> and build it.
[test] supports all HTTP and file transfer protocols
[pass] got: <a href="http://example.org">http://example.org</a>
[pass] got: <a href="ftps://example.org/file.txt">ftps://example.org/file.txt</a>
[pass] got: <a href="https://example.org">https://example.org</a>
[pass] got: <a href="file:///home/user/document.org">file:///home/user/document.org</a>
[pass] got: <a href="ftp://example.org/file.txt">ftp://example.org/file.txt</a>
[test] supports communication protocols
[pass] got: <a href="ircs://irc.example.org/#emacs">ircs://irc.example.org/#emacs</a>
[pass] got: <a href="tel:+31612345678">tel:+31612345678</a>
[pass] got: <a href="mailto:jjbigorra@gmail.com">mailto:jjbigorra@gmail.com</a>
[pass] got: <a href="irc://irc.example.org/#emacs">irc://irc.example.org/#emacs</a>
[test] URLs coexist with inline Org markup
[pass] got: <strong>Visit <a href="https://example.org">https://example.org</a></strong>
[pass] got: See <a href="https://example.org">https://example.org</a> and <strong>bold</strong> text.
[test] URLs stop at a newline
[pass] got: <a href="https://example.org">https://example.org</a>
[test] URLs stop at whitespace
[pass] got: See <a href="https://example.org">https://example.org</a> for details.
[pass] got: See <a href="https://example.org">https://example.org</a> here.
[test] raw URLs and Org links coexist
[pass] got: Visit <a href="https://example.org">https://example.org</a> or <a href="https://gnu.org">GNU</a>.
[test] does not treat unsupported schemes as raw URLs
[pass] got: javascript:alert(1)
[pass] got: custom://example.org
[info] ran 17 tasks in: 0 seconds, 63419 microseconds
running unit tests from file: markdown-spec.scm, entrypoint: spec
[info] shuffled with seed 821816614 (replay with VERITAS_SEED=821816614)
------------------------------------------
[suite] Links and images in Markdown
[test] an image link becomes a Markdown image
[pass] got: 
[pass] got: 
[test] file links lose their file: prefix
[pass] got: [notes](notes.org)
[pass] got: [notes.org](notes.org)
[test] a heading link becomes the heading slug
[pass] got: [up](#notes)
[pass] got: [there](#custom-id)
------------------------------------------
[suite] Every fixture converts to Markdown
------------------------------------------
[suite] sample-org-text.org
[test] produces a non-empty document
[pass] got: # Top Level Heading
This is a standard paragraph.
It sits right under the heading.
## A Subheading
- First list item
- Second list item
### An ordered subheading
1. yes buddy
2. another item
## Another
**key**
: val
**desc**
: conte
*In Vino Veritas* but *In Aqua Sanitas*
```scheme
(define (hello)
(display \"Hello World!\"))
```
[test] converts without error
[pass] success running without errors
------------------------------------------
[suite] sample-org-blocks.org
[test] produces a non-empty document
[pass] got: ```
* Verbatim content
- Do not parse
```
> IN VINO VERITAS IN AQUA SANITAS
---
<strong><p>HELLO WORLD</p></strong>
")
(define sample-org-table
"
| Name | Age | City |
| --- | --- | --- |
| Alice | 30 | London |
| Bob | 25 | Berlin |
[test] converts without error
[pass] success running without errors
------------------------------------------
[suite] sample-org-table.org
[test] produces a non-empty document
[pass] got: | Name | Age | City |
| --- | --- | --- |
| Alice | 30 | London |
| Bob | 25 | Berlin |
[test] converts without error
[pass] success running without errors
------------------------------------------
[suite] sample-org-features.org
[test] produces a non-empty document
[pass] got: title: Feature Showcase
author: OrgWebAlchemy
<!-- A single comment line. -->
# Introduction
This paragraph shows **bold**, *italic*, `code`, `verbatim`, <u>underline</u>, ~~strikethrough~~, x<sup>2</sup>, H<sub>2</sub>O.
A forced break here:
and then the continuation.
See [GNU](https://gnu.org) or a raw link <https://example.org> plus a
footnote reference[^one] and an inline footnote^[like this].
# Links and images
An external [documentation link](https://example.org/docs), a bare [notes.org](notes.org),
an , a bare  and a jump
[into another file](notes.org).
A local anchor [custom id](#custom-id) and a heading link [up top](#introduction).
Mail [the author](mailto:hello@example.org) or dial [tel:+123456](tel:+123456).
A figure cross-reference [fig:showcase](#fig:showcase).
<a id="fig:showcase"></a>

*The showcase diagram*
[^one] The footnote definition.
# Named elements
<a id="tbl:metrics"></a>
| Metric | Value |
| --- | --- |
| Users | 1024 |
<a id="feature-list"></a>
- one
- two
A table cross-reference [tbl:metrics](#tbl:metrics) and one to [feature-list](#feature-list).
The figure [fig:showcase](#fig:showcase) is named too.
# TODO [#A] A Task :work:urgent:
- [ ] unchecked item
- [x] checked item
- nested item
# Lists
1. first
2. second
**Term**
: Definition
- outer
1. nested ordered
2. another
**Tail**
: detail
# Blocks
```scheme
(display "hi")
```
```
*literal*
```
> Quoted *text*
line one
line two
<div align="center">
centered line
</div>
<div class="raw">Raw HTML</div>
<!-- hidden comment
-->
a fixed-width line
---
| Name | Age |
| --- | --- |
| Ada | 36 |
[test] converts without error
[pass] success running without errors
------------------------------------------
[suite] Nested Markdown structures
[test] nested ordered lists are indented
[pass] got: 1. a
1. b
[test] a checkbox nested inside a list
[pass] got: - a
- [ ] b
[test] nested unordered lists are indented
[pass] got: - a
- b
------------------------------------------
[suite] Block Org renders to Markdown
[test] example block renders
[pass] got: ```
x
```
[test] unordered list renders
[pass] got: - a
- b
[test] keyword renders
[pass] got: TITLE: Hi
[test] src block renders
[pass] got: ```scheme
(x)
```
[test] heading metadata renders
[pass] got: # TODO [#A] T :x:
[test] heading renders
[pass] got: # Title
[test] comment renders
[pass] got: <!-- a comment -->
[test] checkbox on renders
[pass] got: - [x] a
[test] ordered list renders
[pass] got: 1. a
2. b
[test] verse block renders
[pass] got: a
b
[test] fixed width renders
[pass] got: fixed
[test] paragraph renders
[pass] got: Some text
[test] checkbox off renders
[pass] got: - [ ] a
[test] separator renders
[pass] got: ---
[test] table renders
[pass] got: | Name | Age |
| --- | --- |
| Ada | 36 |
[test] quote block renders
[pass] got: > hi
[test] description list renders
[pass] got: **k**
: v
------------------------------------------
[suite] Inline Org renders to Markdown
[test] /italic/ -> *italic*
[pass] got: *italic*
[test] [fn:1] -> [^1]
[pass] got: [^1]
[test] [[u]] -> [u](u)
[pass] got: [u](u)
[test] _underline_ -> <u>underline</u>
[pass] got: <u>underline</u>
[test] +strike+ -> ~~strike~~
[pass] got: ~~strike~~
[test] =verbatim= -> `verbatim`
[pass] got: `verbatim`
[test] [[u][d]] -> [d](u)
[pass] got: [d](u)
[test] x^{2} -> x<sup>2</sup>
[pass] got: x<sup>2</sup>
[test] ~code~ -> `code`
[pass] got: `code`
[test] [fn::note] -> ^[note]
[pass] got: ^[note]
[test] *bold* -> **bold**
[pass] got: **bold**
[test] H_{2}O -> H<sub>2</sub>O
[pass] got: H<sub>2</sub>O
[test] https://x.org -> <https://x.org>
[pass] got: <https://x.org>
[test] plain -> plain
[pass] got: plain
------------------------------------------
[suite] Mixed and nested list types
[test] a mixed nested list renders each run
[pass] got: - a
- b
1. c
- d
[test] a description list nests under an unordered item
[pass] got: - term
**k**
: v
[test] consecutive kinds become separate Markdown lists
[pass] got: - u
1. o
**k**
: v
[test] an ordered list nests and keeps its indentation
[pass] got: - outer
1. n1
2. n2
------------------------------------------
[suite] Full document Markdown
[test] the fixture keeps the metadata keyword
[pass] got: title: Feature Showcase
[test] the fixture keeps inline markup
[pass] got: **bold**
[pass] got: ~~strikethrough~~
[test] the feature fixture matches its snapshot
[assert] sample-org-features markdown
[pass] matches snapshot test/snapshots/sample-org-features-markdown.snap
[test] the sample text fixture converts without error
[pass] success running without errors
[pass] got: # Top Level Heading
This is a standard paragraph.
It sits right under the heading.
## A Subheading
- First list item
- Second list item
### An ordered subheading
1. yes buddy
2. another item
## Another
**key**
: val
**desc**
: conte
*In Vino Veritas* but *In Aqua Sanitas*
```scheme
(define (hello)
(display \"Hello World!\"))
```
------------------------------------------
[suite] Figures in Markdown
[test] a link to a figure points at its anchor
[pass] got: [fig:arch](#fig:arch)
[test] a figure without a caption is just the image
[pass] got: <a id="fig:x"></a>

[test] a named figure keeps an anchor for its cross-references
[pass] got: <a id="fig:arch"></a>

*The architecture*
[test] a caption names an image with no alt text of its own
[pass] got: 
*A caption*
------------------------------------------
[suite] Named elements in Markdown
[test] a named table keeps an anchor for its cross-references
[pass] got: <a id="tbl:data"></a>
| a |
| --- |
[test] a bare link to a name points at the anchor
[pass] got: [tbl:data](#tbl:data)
[test] a named quote block keeps its anchor too
[pass] got: <a id="q"></a>
> x
[info] ran 9 tasks in: 0 seconds, 57804 microseconds
finished running tests
--------------------------------------------------------------
Maak: the infinitely extensible command runner
Loading tasks from Maak file: /etc/byggsteg/job-clone/orgwebalchemy/trunk/maak.scm
Executing Maak tasks: test
--------------------------------------------------------------
=> Running Maak task: test
Run project's tests.
→ running command: cd /etc/byggsteg/job-clone/orgwebalchemy/trunk && sudo maak deploy
;;; note: source file /etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/html.scm
;;; newer than compiled /root/.cache/guile/ccache/3.0-LE-8-4.6/etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/html.scm.go
;;; note: auto-compilation is enabled, set GUILE_AUTO_COMPILE=0
;;; or pass the --no-auto-compile argument to disable.
;;; compiling /etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/html.scm
;;; note: source file /etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/peg.scm
;;; newer than compiled /root/.cache/guile/ccache/3.0-LE-8-4.6/etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/peg.scm.go
;;; compiling /etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/peg.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.6/etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/peg.scm.go
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.6/etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/html.scm.go
;;; note: source file /etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/markdown.scm
;;; newer than compiled /root/.cache/guile/ccache/3.0-LE-8-4.6/etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/markdown.scm.go
;;; compiling /etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/markdown.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.6/etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/markdown.scm.go
API documentation saved to doc/api
index.texi: warning: must specify a title with a title command or @top
renamed './index' -> './doc/api-dist'
removed '/srv/http/jointhefreeworld.org/api-docs/orgwebalchemy/API.html'
removed '/srv/http/jointhefreeworld.org/api-docs/orgwebalchemy/orgwebalchemy_005fhtml.html'
removed '/srv/http/jointhefreeworld.org/api-docs/orgwebalchemy/orgwebalchemy_005fmarkdown.html'
removed '/srv/http/jointhefreeworld.org/api-docs/orgwebalchemy/orgwebalchemy_005fpeg.html'
removed directory '/srv/http/jointhefreeworld.org/api-docs/orgwebalchemy'
mkdir: created directory '/srv/http/jointhefreeworld.org/api-docs/orgwebalchemy'
renamed 'doc/api-dist/API.html' -> '/srv/http/jointhefreeworld.org/api-docs/orgwebalchemy/API.html'
renamed 'doc/api-dist/orgwebalchemy_005fhtml.html' -> '/srv/http/jointhefreeworld.org/api-docs/orgwebalchemy/orgwebalchemy_005fhtml.html'
renamed 'doc/api-dist/orgwebalchemy_005fmarkdown.html' -> '/srv/http/jointhefreeworld.org/api-docs/orgwebalchemy/orgwebalchemy_005fmarkdown.html'
renamed 'doc/api-dist/orgwebalchemy_005fpeg.html' -> '/srv/http/jointhefreeworld.org/api-docs/orgwebalchemy/orgwebalchemy_005fpeg.html'
removed 'doc/api/index.texi'
removed directory 'doc/api'
removed directory 'doc/api-dist'
removed directory 'doc'
--------------------------------------------------------------
Maak: the infinitely extensible command runner
Loading tasks from Maak file: /etc/byggsteg/job-clone/orgwebalchemy/trunk/maak.scm
Executing Maak tasks: deploy
--------------------------------------------------------------
=> Running Maak task: deploy
Generate documentation for the project and publish to web directory.