orgwebalchemy-0e3d3302-d033 (retry f5128b5c)

job succeeded

time taken
2 seconds
started at
Wed Oct 14 19:54:17 2026
updated at
Wed Oct 14 19:54:19 2026
log uuid
f5128b5c-52f6-4035-9969-1f5c6ce228d1
inspect the job's lambda expression
(lambda()
  `((task . ("maak-test" "maak-deploy"))
    (project . "orgwebalchemy")
    (branch-name . "trunk")            
    (clone-url . "https://codeberg.org/jjba23/orgwebalchemy.git")
    (env . ((VERITAS_REPORTER . monotone-reporter)))))
    
starting new job...


→ running command: cd "/etc/byggsteg/job-clone//orgwebalchemy/trunk" && git reset --hard && git pull

HEAD is now at 13155f2 feat: Support table empty row
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 13155f23d5af391ac7d930b0f37a0e845cb50298
Author: Josep Bigorra <jjbigorra@gmail.com>
Date:   Wed Oct 14 16:54:45 2026 +0200

    feat: Support table empty row

M       README.org
M       src/orgwebalchemy/html.scm
M       src/orgwebalchemy/markdown.scm
M       src/orgwebalchemy/peg.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 105624515 (replay with VERITAS_SEED=105624515)

found unit tests: (markdown-spec.scm peg-spec.scm html-spec.scm)


running unit tests from file: markdown-spec.scm, entrypoint: spec


[info] shuffled with seed 105624515 (replay with VERITAS_SEED=105624515)
------------------------------------------
[suite] Full document Markdown
  [test] the sample text fixture converts without error
    [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!\"))
```


    [pass] success running without errors
  [test] the fixture keeps inline markup
    [pass] got: **bold**
    [pass] got: ~~strikethrough~~
  [test] the fixture keeps the metadata keyword
    [pass] got: title: Feature Showcase
  [test] the feature fixture matches its snapshot
    [assert] sample-org-features markdown
    [pass] matches snapshot test/snapshots/sample-org-features-markdown.snap
------------------------------------------
[suite] Every fixture converts to Markdown
------------------------------------------
[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 ![inline image](photo.png), a bare ![images/diagram.svg](images/diagram.svg) 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>

![a showcase diagram](images/showcase.svg)

*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] 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-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] Links and images in Markdown
  [test] file links lose their file: prefix
    [pass] got: [notes.org](notes.org)


    [pass] got: [notes](notes.org)


  [test] an image link becomes a Markdown image
    [pass] got: ![a photo](photo.png)


    [pass] got: ![images/diagram.svg](images/diagram.svg)


  [test] a heading link becomes the heading slug
    [pass] got: [there](#custom-id)


    [pass] got: [up](#notes)


------------------------------------------
[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 named quote block keeps its anchor too
    [pass] got: <a id="q"></a>

> x


  [test] a bare link to a name points at the anchor
    [pass] got: [tbl:data](#tbl:data)
------------------------------------------
[suite] Mixed and nested list types
  [test] a description list nests under an unordered item
    [pass] got: - term
  **k**
  :   v


  [test] a mixed nested list renders each run
    [pass] got: - a
  - b
  1. c
  - d


  [test] an ordered list nests and keeps its indentation
    [pass] got: - outer
  1. n1
  2. n2


  [test] consecutive kinds become separate Markdown lists
    [pass] got: - u
1. o
**k**
:   v


------------------------------------------
[suite] Block Org renders to Markdown
  [test] separator renders
    [pass] got: ---


  [test] fixed width renders
    [pass] got:     fixed


  [test] checkbox off renders
    [pass] got: - [ ] a


  [test] verse block renders
    [pass] got: a  
b


  [test] heading metadata renders
    [pass] got: # TODO [#A] T :x:


  [test] paragraph renders
    [pass] got: Some text


  [test] description list renders
    [pass] got: **k**
:   v


  [test] unordered list renders
    [pass] got: - a
- b


  [test] ordered list renders
    [pass] got: 1. a
2. b


  [test] comment renders
    [pass] got: <!-- a comment -->


  [test] heading renders
    [pass] got: # Title


  [test] example block renders
    [pass] got: ```
x
```


  [test] src block renders
    [pass] got: ```scheme
(x)
```


  [test] quote block renders
    [pass] got: > hi


  [test] keyword renders
    [pass] got: TITLE: Hi


  [test] table renders
    [pass] got: | Name | Age |
| --- | --- |
| Ada | 36 |


  [test] checkbox on renders
    [pass] got: - [x] a


------------------------------------------
[suite] Figures in Markdown
  [test] a figure without a caption is just the image
    [pass] got: <a id="fig:x"></a>

![x.svg](x.svg)


  [test] a link to a figure points at its anchor
    [pass] got: [fig:arch](#fig:arch)
  [test] a named figure keeps an anchor for its cross-references
    [pass] got: <a id="fig:arch"></a>

![a diagram](diagram.png)

*The architecture*


  [test] a caption names an image with no alt text of its own
    [pass] got: ![A caption](a.png)

*A caption*


------------------------------------------
[suite] Nested Markdown structures
  [test] a checkbox nested inside a list
    [pass] got: - a
  - [ ] b


  [test] nested ordered lists are indented
    [pass] got: 1. a
   1. b


  [test] nested unordered lists are indented
    [pass] got: - a
  - b


------------------------------------------
[suite] Inline Org renders to Markdown
  [test] ~code~ -> `code`
    [pass] got: `code`
  [test] _underline_ -> <u>underline</u>
    [pass] got: <u>underline</u>
  [test] +strike+ -> ~~strike~~
    [pass] got: ~~strike~~
  [test] plain -> plain
    [pass] got: plain
  [test] x^{2} -> x<sup>2</sup>
    [pass] got: x<sup>2</sup>
  [test] https://x.org -> <https://x.org>
    [pass] got: <https://x.org>
  [test] [fn::note] -> ^[note]
    [pass] got: ^[note]
  [test] [fn:1] -> [^1]
    [pass] got: [^1]
  [test] =verbatim= -> `verbatim`
    [pass] got: `verbatim`
  [test] H_{2}O -> H<sub>2</sub>O
    [pass] got: H<sub>2</sub>O
  [test] [[u][d]] -> [d](u)
    [pass] got: [d](u)
  [test] *bold* -> **bold**
    [pass] got: **bold**
  [test] [[u]] -> [u](u)
    [pass] got: [u](u)
  [test] /italic/ -> *italic*
    [pass] got: *italic*
------------------------------------------
[suite] Empty values
  [test] an empty table cell keeps its column
    [pass] got: | a |  | b |
| --- | --- | --- |


  [test] an empty keyword value still renders its line
    [pass] got: KEY: 


  [test] a source block written without a language still renders
    [pass] got: ```
(x)
```


  [test] a line that is only its closing bar holds one empty cell
    [pass] got: |  |
| --- |


  [test] an empty block still renders as the block it is
    [pass] got: > 


    [pass] got: ```
```


[info] ran 10 tasks in: 0 seconds, 27793 microseconds

running unit tests from file: peg-spec.scm, entrypoint: spec


[info] shuffled with seed 105624515 (replay with VERITAS_SEED=105624515)
------------------------------------------
[suite] Inline constructs
  [test] a forced line break before end of line
    [pass] got: (inline-line-break \\)
  [test] escaped markup characters are recognised
    [pass] got: (inline-escape-char +)
    [pass] got: (inline-escape-char *)
  [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] superscript and subscript use their braced form
    [pass] got: ((inline-text x) (inline-superscript-content 2))
    [pass] got: ((inline-text H) (inline-subscript-content 2) (inline-text O))
  [test] raw urls carry their scheme
    [pass] got: (inline-raw-url (url-scheme https://) x.org)
  [test] footnote reference, definition and inline form
    [pass] got: (inline-footnote-inline (footnote-content note))
    [pass] got: (inline-footnote-ref (footnote-label 1))
    [pass] got: (inline-footnote-def (footnote-label 1) (footnote-content  note))
------------------------------------------
[suite] Run scanners
  [test] a table line that is only its closing bar holds one empty cell
    [pass] got: (table (table-row (table-cell )))
    [pass] got: (table (table-row (table-cell  a ) (table-cell )))
    [pass] got: (table (table-row (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] the last line of a document without a newline parses
    [pass] got: (paragraph (line-content text))
  [test] an empty value is text, not a gap in the tree
    [pass] got: (quote-block (quote-content ))
    [pass] got: (table (table-row (table-cell a) (table-cell ) (table-cell b)))
    [pass] got: (keyword (keyword-key KEY) (keyword-value ))
    [pass] got: (src-block (lang ) (src-content (x)
))
  [test] a tab and non-ASCII text are ordinary line content
    [pass] got: (paragraph (line-content café 中文))
    [pass] got: (paragraph (line-content a      b))
  [test] a table row needs no trailing newline, and gains no extra cell
    [pass] got: (table (table-row (table-cell  a )))
    [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
  [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 lone carriage return is content, as it always was
    [pass] got: (paragraph (line-content a))
b )))pass] got: (table (table-row (table-cell  a
  [test] a plain inline run is one string, not one per character
    [pass] got: (inline-text plain text here)
    [pass] got: (inline-text the h f s w g t m i r letters)
------------------------------------------
[suite] Heading anchors
  [test] titles become URL fragments
    [pass] got: my-first-section
    [pass] got: a-b
    [pass] got: spaced-punct
    [pass] got: 
  [test] a heading link uses the anchor of its title
    [pass] got: #my-section
------------------------------------------
[suite] Mixed and nested lists
  [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 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] 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)))))
  [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)))
------------------------------------------
[suite] Heading metadata
  [test] a plain heading keeps its simple shape
    [pass] got: (heading ** (line-content Title))
  [test] TODO keyword, priority and tags are extracted
    [pass] got: (heading * (heading-todo TODO) (heading-priority A) (line-content Task) (heading-tags work urgent))
  [test] a TODO keyword without a priority
    [pass] got: (heading * (heading-todo DONE) (line-content Done))
  [test] a title ending in a colon is not mistaken for tags
    [pass] got: (heading * (line-content Note:))
------------------------------------------
[suite] Figures
  [test] a figure reports its parts, missing ones as #f
    [pass] got: (fig:arch The architecture file:diagram.png a diagram)
    [pass] got: (#f A caption file:a.png #f)
  [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 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 describing something else stays a keyword
    [pass] got: (keyword (keyword-key CAPTION) (keyword-value A table))
    [pass] got: (keyword (keyword-key CAPTION) (keyword-value Not a figure))
  [test] several captions join in document order
    [pass] got: (figure (figure-caption One two) (figure-image (figure-target file:a.png)))
  [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] 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)))
------------------------------------------
[suite] Whole-document parsing
  [test] the project's own README starts with a keyword and is long
    [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, including empty cells: ~| a || b |~ is a row of three cells in both))) (paragraph (line-content   renderers, the middle one empty, and a line that is nothing but its closing)) (paragraph (line-content   bar (~|~ on its own) is a row of one empty cell)) (org-list (unordered-item (line-content ~#+begin_src~, with or without a language (a block written without one still))) (paragraph (line-content   renders as a source block, just without a language class))) (org-list (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  ~![a photo](photo.png)~                        )) (table-row (table-cell  ~[[images/diagram.svg]]~      ) (table-cell  ~<img src="images/diagram.svg" alt="..." />~ ) (table-cell  ~![images/diagram.svg](images/diagram.svg)~    )) (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![a diagram](diagram.png)\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 used)) (paragraph (line-content 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      592.82 ) (table-cell              296.41 )) (table-row (table-cell  Parse README.org                       ) (table-cell         200 ) (table-cell      674.73 ) (table-cell             3373.65 )) (table-row (table-cell  Render the feature fixture to HTML     ) (table-cell        2000 ) (table-cell     3576.71 ) (table-cell             1788.35 )) (table-row (table-cell  Render the feature fixture to Markdown ) (table-cell        2000 ) (table-cell     3296.28 ) (table-cell             1648.14 )) (table-row (table-cell  Parse a 50-clause inline paragraph     ) (table-cell        2000 ) (table-cell     4204.41 ) (table-cell             2102.21 ))) (paragraph (line-content That is a parser that reads a 1.4 KB Org document in 0.3 ms and this 39 KB file)) (paragraph (line-content in 3.4 ms, and two renderers that walk the resulting tree at the same speed as)) (paragraph (line-content each other.  A 50-clause paragraph of inline markup -- fifty links, bold runs)) (paragraph (line-content and code spans in one line -- takes 2.1 ms; inline parsing is the most expensive)) (paragraph (line-content stage per byte, and deliberately so, because a line that carries that many)) (paragraph (line-content constructs has to try every alternative at every position of it.)) (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.67 KiB ) (table-cell                 28.50 ) (table-cell                 0.40 ms )) (table-row (table-cell  Parse README.org                       ) (table-cell              2630.23 KiB ) (table-cell                315.00 ) (table-cell                 4.67 ms )) (table-row (table-cell  Render the feature fixture to HTML     ) (table-cell              1360.62 KiB ) (table-cell                166.00 ) (table-cell                 2.43 ms )) (table-row (table-cell  Render the feature fixture to Markdown ) (table-cell              1362.94 KiB ) (table-cell                167.00 ) (table-cell                 2.42 ms )) (table-row (table-cell  Parse a 50-clause inline paragraph     ) (table-cell              1691.05 KiB ) (table-cell                208.50 ) (table-cell                 3.01 ms ))) (paragraph (line-content Parsing this file asks for about 2.7 MB for 39 KB of input, roughly 70 bytes per)) (paragraph (line-content input byte, and the collection counts say a parse of this size is still likely)) (paragraph (line-content to trigger a collection of its own.  Rendering allocates less than parsing,)) (paragraph (line-content since the inline parser only ever sees the text nodes rather than the whole)) (paragraph (line-content document.)) (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.)) (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 run scanners hand that pass one fragment per)) (paragraph (line-content run of text rather than 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.043 )) (table-row (table-cell  README.org               ) (table-cell                                   768 ) (table-cell          0.385 )) (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.005 ))) (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 -- and a single 45 KB line as one, which is)) (paragraph (line-content why the merge is well under a millisecond even for a document the size of this)) (org-list (ordered-item (line-content What is left is the reading and writing of the tree's own nodes, and the))) (paragraph (line-content allocation that goes with it: the ~cons~-and-reverse the pattern compiler emits)) (paragraph (line-content for every node, rather than the scanning of the text.)) (paragraph (line-content The ~README.org~ rows are this file at the length it had when the tables were)) (paragraph (line-content measured, so they move a little when the documentation grows or shrinks; the)) (paragraph (line-content other rows do not.)) (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*, *Named elements*)) (paragraph (line-content and *Empty values* 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 and the two renderers as they were and as they))) (paragraph (line-content   are changed are loaded into one process -- the earlier ones under renamed)) (paragraph (line-content   modules, taken from ~git show REV:src/orgwebalchemy/peg.scm~ and its)) (paragraph (line-content   neighbours -- given the same inputs, and six results compared for every one)) (paragraph (line-content   of them: the whole-document AST, the inline AST, and the HTML and the)) (paragraph (line-content   Markdown that each renderer writes.  The run-scanner change was checked this)) (paragraph (line-content   way on 70,858 inputs, and the empty-value change again on 38,412: every pair)) (paragraph (line-content   of interesting characters (printable ASCII, tab, newline, carriage return,)) (paragraph (line-content   the other control characters and two multi-byte characters) as documents, as)) (paragraph (line-content   inline text, and inside a link, a footnote and a description list, this)) (paragraph (line-content   README, the fixtures, hand-written edge cases (markup at every position,)) (paragraph (line-content   tables with empty cells, lines without a newline) and 5,800 randomly)) (paragraph (line-content   generated documents.  The last run reported 232 differences, every one of)) (paragraph (line-content   them the empty-value change below, and no input at all that raised an error on)) (paragraph (line-content   the new side, where the code before it raised on 21, each of them a table row)) (paragraph (line-content   with no cells in it.  The same check after the table change below, on 38,250)) (paragraph (line-content   of those inputs, found 16 that parse differently: every one of the 16 a table)) (paragraph (line-content   line whose last bar is followed by nothing but the end of the line.)) (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 Three deliberate differences have come out of these runs.  A run of two or more)) (paragraph (line-content ~#+CAPTION:~ or ~#+NAME:~ lines in front of an element that is not a figure used)) (paragraph (line-content to come out of the folder *reversed*; it now keeps document order, which is what)) (paragraph (line-content the surrounding documentation always claimed and what a keyword run should do,)) (paragraph (line-content and a unit test pins it down.  And the text of something that is empty is now)) (paragraph (line-content the empty string, rather than the bare tag symbol an empty `(* ...)' match)) (paragraph (line-content leaves behind: an empty table cell keeps its column, a source block written)) (paragraph (line-content without a language still renders as a source block, and an empty keyword,)) (paragraph (line-content comment, fixed-width line, footnote or block stays in the document instead of)) (paragraph (line-content vanishing from it.  And a table row is now read the way Org reads one, by the)) (paragraph (line-content bars that separate its cells: a bar closes a row when nothing but the end of the)) (paragraph (line-content line follows it, which makes a line that is only its closing bar (~|~ on its)) (org-list (ordered-item (line-content a row of one empty cell rather than a row with no cells in it, makes))) (paragraph (line-content ~| a ||~ a row of two cells rather than a paragraph, and stops a row that ends)) (paragraph (line-content the input from gaining an empty cell for the newline it does not have.  Each of)) (paragraph (line-content those shapes has a test.  Everything else about the parser is output-identical,)) (paragraph (line-content and the ~#+NAME:~ feature is a change to the language, so it is covered by tests)) (paragraph (line-content 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
    [pass] got: 430
  [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
    [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  ))))
------------------------------------------
[suite] Org link targets
  [test] image links are recognised by extension
    [pass] got: #t
    [pass] got: #t
    [pass] got: #f
    [pass] got: #f
    [pass] got: #t
    [pass] got: #t
  [test] custom ids and heading links become fragments
    [pass] got: #my-section
    [pass] got: My Section
    [pass] got: #custom-id
    [pass] got: heading
    [pass] got: custom-id
    [pass] got: internal
  [test] file links and bare paths are local files
    [pass] got: file
    [pass] got: file
    [pass] got: notes.org
    [pass] got: notes.org
    [pass] got: images/a.png
  [test] mailto and tel links keep their scheme
    [pass] got: tel
    [pass] got: mailto
    [pass] got: mailto:me@example.org
  [test] a scheme link keeps its scheme
    [pass] got: https
    [pass] got: //example.org/a
    [pass] got: https://example.org/a
  [test] a ::search suffix is kept apart from the path
    [pass] got: notes.org
    [pass] got: *Intro
    [pass] got: #f
    [pass] got: notes.org
  [test] a Windows drive letter is not a scheme
    [pass] got: C:/tmp/x.txt
    [pass] got: file
------------------------------------------
[suite] Inline markup nodes
  [test] +strike+ parses
    [pass] got: (inline-strike-content strike)
  [test] ~code~ parses
    [pass] got: (inline-code-content code)
  [test] /italic/ parses
    [pass] got: (inline-italic-content italic)
  [test] =verbatim= parses
    [pass] got: (inline-verbatim-content verbatim)
  [test] _underline_ parses
    [pass] got: (inline-underline-content underline)
  [test] *bold* parses
    [pass] got: (inline-bold-content bold)
------------------------------------------
[suite] Document elements recognise their construct
  [test] export html parses as export-html-block
    [pass] got: export-html-block
  [test] center block parses as center-block
    [pass] got: center-block
  [test] mixed sibling list parses as org-list
    [pass] got: org-list
  [test] comment parses as comment-line
    [pass] got: comment-line
  [test] quote block parses as quote-block
    [pass] got: quote-block
  [test] separator parses as separator
    [pass] got: separator
  [test] ordered list parses as org-list
    [pass] got: org-list
  [test] description list parses as org-list
    [pass] got: org-list
  [test] example block parses as example-block
    [pass] got: example-block
  [test] fixed width parses as fixed-width
    [pass] got: fixed-width
  [test] src block parses as src-block
    [pass] got: src-block
  [test] heading parses as heading
    [pass] got: heading
  [test] keyword parses as keyword
    [pass] got: keyword
  [test] comment block parses as comment-block
    [pass] got: comment-block
  [test] unordered list parses as org-list
    [pass] got: org-list
  [test] table parses as table
    [pass] got: table
  [test] paragraph parses as paragraph
    [pass] got: paragraph
  [test] verse block parses as verse-block
    [pass] got: verse-block
------------------------------------------
[suite] Named elements
  [test] the names of a document are the names its elements carry
    [pass] got: ()
    [pass] got: (tbl:data diagram)
  [test] a named element reports its name and itself
    [pass] got: (code (src-block (lang scheme) (src-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] no names in scope leaves every target as written
    [pass] got: (inline-link-no-desc (link-target-content diagram))
  [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] 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 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 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))
  [test] a target that says what it is is left alone
    [pass] got: (inline-link-no-desc (link-target-content #diagram))
    [pass] got: (inline-link-no-desc (link-target-content https://x/diagram))
    [pass] got: (inline-link-no-desc (link-target-content nowhere))
    [pass] got: (inline-link-no-desc (link-target-content *diagram))
    [pass] got: (inline-link-no-desc (link-target-content diagram::*H))
  [test] a prefixed cross-reference needs no rewriting
    [pass] got: #tbl:data
    [pass] got: #fig:arch
[info] ran 11 tasks in: 0 seconds, 35750 microseconds

running unit tests from file: html-spec.scm, entrypoint: spec


[info] shuffled with seed 105624515 (replay with VERITAS_SEED=105624515)
------------------------------------------
[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] Class Parameter Customization
  [test] customizing heading classes dynamically
    [pass] got: <h1 class="text-3xl font-bold" id="main-title">Main Title</h1>
    [pass] got: <h2 class="text-xl" id="section">Section</h2>
  [test] customizing block element classes
    [pass] got: <ul class="list-disc ml-6"><li>item</li></ul>
    [pass] got: <p class="leading-relaxed mb-4">Paragraph text.</p>
    [pass] got: <pre class="python bg-gray-900 p-4">print(1)
</pre>
  [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] 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 &quot;hi&quot;)
</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] Links and images
  [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] 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] a ::search suffix is not part of the href
    [pass] got: <p><a href="notes.org">notes</a></p>
  [test] a custom id link stays a fragment
    [pass] got: <p><a href="#custom-id">there</a></p>
  [test] image classes can be customised
    [pass] got: <p><img class="rounded" src="photo.png" alt="photo.png" /></p>
  [test] a heading link points at the heading anchor
    [pass] got: <h1 id="notes">Notes</h1><p>See <a href="#notes">there</a>.</p>
------------------------------------------
[suite] Inline Raw URL Transformation
  [test] supports communication protocols
    [pass] got: <a href="irc://irc.example.org/#emacs">irc://irc.example.org/#emacs</a>
    [pass] got: <a href="mailto:jjbigorra@gmail.com">mailto:jjbigorra@gmail.com</a>
    [pass] got: <a href="tel:+31612345678">tel:+31612345678</a>
    [pass] got: <a href="ircs://irc.example.org/#emacs">ircs://irc.example.org/#emacs</a>
  [test] supports all HTTP and file transfer protocols
    [pass] got: <a href="https://example.org">https://example.org</a>
    [pass] got: <a href="ftp://example.org/file.txt">ftp://example.org/file.txt</a>
    [pass] got: <a href="ftps://example.org/file.txt">ftps://example.org/file.txt</a>
    [pass] got: <a href="http://example.org">http://example.org</a>
    [pass] got: <a href="file:///home/user/document.org">file:///home/user/document.org</a>
  [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] 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] 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> here.
    [pass] got: See <a href="https://example.org">https://example.org</a>       for details.
  [test] does not treat unsupported schemes as raw URLs
    [pass] got: javascript:alert(1)
    [pass] got: custom://example.org
  [test] supports remote access and network protocols
    [pass] got: <a href="gopher://example.org/1/hello">gopher://example.org/1/hello</a>
    [pass] got: <a href="ssh://git@example.org/repo">ssh://git@example.org/repo</a>
    [pass] got: <a href="sftp://example.org/path/to/file">sftp://example.org/path/to/file</a>
    [pass] got: <a href="git://example.org/project.git">git://example.org/project.git</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 URLs with paths, queries, fragments and ports
    [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/page#section">https://example.org/page#section</a>
    [pass] got: <a href="https://example.org/search?q=org-mode&amp;lang=en">https://example.org/search?q=org-mode&amp;lang=en</a>
------------------------------------------
[suite] Empty values
  [test] a line that is only its closing bar holds one empty cell
    [pass] got: <table><tbody><tr><td> </td></tr></tbody></table>
    [pass] got: <table><tbody><tr><td /></tr></tbody></table>
  [test] an empty keyword value still renders its line
    [pass] got: <div>KEY: </div>
  [test] an empty table cell keeps its column
    [pass] got: <table><tbody><tr><td>a</td><td /><td>b</td></tr></tbody></table>
  [test] a source block written without a language still renders
    [pass] got: <pre>(x)
</pre>
  [test] an empty block still renders as the block it is
    [pass] got: <pre></pre>
    [pass] got: <blockquote />
------------------------------------------
[suite] Extended block elements
  [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>
  [test] keywords and comments
    [pass] got: <div>TITLE: Hello</div>
    [pass] got: <!-- a comment -->
    [pass] got: <!-- hidden
-->
  [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>
------------------------------------------
[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: <em>italic</em>
    [pass] got: <a href="https://gnu.org">GNU</a>
    [pass] got: <a href="https://gnu.org">https://gnu.org</a>
    [pass] got: <strong>bold</strong>
    [pass] got: <u>underlined</u>
    [pass] got: <code>code</code>
    [pass] got: Unsure if Option A/B
    [pass] got: <code>verbatim</code>
    [pass] got: cron job / shepherd timer that does git fetch --all for work and <code>~/hacking</code>
------------------------------------------
[suite] Figures
  [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 named figure carries its name as its id
    [pass] got: <figure id="fig:arch"><img src="diagram.png" alt="diagram.png" /></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>
  [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] 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>
------------------------------------------
[suite] SXML to HTML String Rendering
  [test] rendering raw HTML nodes verbatim without escaping
    [pass] got: <span>Raw</span>
  [test] rendering single element vs list of elements
    [pass] got: <span>text</span>
    [pass] got: <h1>Title</h1><p>Body</p>
  [test] rendering *TOP* wrapped node tree
    [pass] got: <p>Hello</p><p>World</p>
------------------------------------------
[suite] Mixed and nested list types
  [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>
  [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 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>
------------------------------------------
[suite] Block Org AST & Document Parsing
  [test] special blocks: quote, example, src, export, separator
    [pass] got: <pre>*literal* code
</pre>
    [pass] got: <pre class="scheme">(define x 10)
</pre>
    [pass] got: <hr />
    [pass] got: <blockquote>Quoted <strong>text</strong></blockquote>
    [pass] got: <div>Raw content</div>
  [test] headings AST transformation
    [pass] got: <h1 id="level-1">Level 1</h1>
    [pass] got: <h3 id="level-3">Level 3</h3>
    [pass] got: <h2 id="level-2">Level 2</h2>
  [test] paragraph transformation
    [pass] got: <p>A simple paragraph with <strong>bold</strong> text.</p>
------------------------------------------
[suite] Extended inline markup
  [test] forced line break and escaped markup
    [pass] got: line<br />
    [pass] got: +
  [test] footnote reference, definition and inline form
    [pass] got: <sup class="footnote-inline">note</sup>
    [pass] got: <span class="footnote-def" id="fn-1"><sup class="footnote-label">1</sup> note</span>
    [pass] got: see<sup class="footnote-ref"><a href="#fn-1" id="fnref-1">1</a></sup>
  [test] strikethrough, superscript and subscript
    [pass] got: <del>struck</del>
    [pass] got: x<sup>2</sup>
    [pass] got: H<sub>2</sub>O
------------------------------------------
[suite] Named elements
  [test] a name the document does not carry is left a file link
    [pass] got: <a href="nowhere">nowhere</a>
  [test] a prefixed cross-reference link points at the element
    [pass] got: <a href="#tbl:data">tbl:data</a>
  [test] a bare link to a name points at the element as well
    [pass] got: <figure id="diagram">
    [pass] got: <a href="#diagram">diagram</a>
  [test] the id joins the classes an element already has
    [pass] got: <pre id="code" class="scheme">(x)
</pre>
  [test] a named table carries its name as its id
    [pass] got: <table id="tbl:data"><tbody><tr><td> a </td></tr></tbody></table>
------------------------------------------
[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] make-sxml-attrs
  [test] constructs a class attribute when classes are supplied
    [pass] got: ((@ (class btn btn-primary active)))
    [pass] got: ((@ (class heading)))
  [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 btn btn-primary active) (href /about)))
    [pass] got: ((@ (class heading) (href /about)))
  [test] constructs attributes when they are supplied
    [pass] got: ((@ (href /home)))
    [pass] got: ((@ (id main-header) (role banner)))
------------------------------------------
[suite] List transformations
  [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] 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] 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>
[info] ran 18 tasks in: 0 seconds, 42016 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

sh: warning: setlocale: LC_ALL: cannot change locale (C.UTF-8): No such file or directory
sh: warning: setlocale: LC_ALL: cannot change locale (C.UTF-8): No such file or directory
API documentation saved to doc/api
index.texi: warning: must specify a title with a title command or @top
sh: warning: setlocale: LC_ALL: cannot change locale (C.UTF-8): No such file or directory
renamed './index' -> './doc/api-dist'
sh: warning: setlocale: LC_ALL: cannot change locale (C.UTF-8): No such file or directory
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'
sh: warning: setlocale: LC_ALL: cannot change locale (C.UTF-8): No such file or directory
mkdir: created directory '/srv/http/jointhefreeworld.org/api-docs/orgwebalchemy'
sh: warning: setlocale: LC_ALL: cannot change locale (C.UTF-8): No such file or directory
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'
sh: warning: setlocale: LC_ALL: cannot change locale (C.UTF-8): No such file or directory
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.