orgwebalchemy-061855ba-0086

job succeeded

starting new job...


→ running command: dir='/etc/byggsteg/job-clone//orgwebalchemy/trunk'
branch='trunk'
if ! command -v git >/dev/null 2>&1; then
  echo 'byggsteg: git is not on this deployment PATH, so the clone cannot be updated' >&2
  exit 1
fi
if ! cd "$dir"; then
  echo "byggsteg: cannot enter the clone $dir" >&2
  exit 1
fi
root=$(git rev-parse --show-toplevel 2>/dev/null)
if [ "$root" != "$(pwd -P)" ]; then
  echo "byggsteg: $dir is not a git checkout; remove it and the next run clones it again" >&2
  exit 1
fi
if ! origin=$(git remote get-url origin 2>/dev/null); then
  echo "byggsteg: $dir has no origin remote to fetch $branch from" >&2
  exit 1
fi
before=$(git rev-parse --short HEAD 2>/dev/null) || before=none
attempt=1
while :; do
  if GIT_TERMINAL_PROMPT=0 git fetch --quiet origin "+refs/heads/$branch:refs/remotes/origin/$branch"; then
    break
  fi
  if [ "$attempt" -ge 3 ]; then
    echo "byggsteg: could not fetch $branch from $origin after 3 attempts; is the branch still there, and is the remote reachable?" >&2
    exit 1
  fi
  echo "byggsteg: fetching $branch from $origin failed; retrying in $((attempt * 2))s" >&2
  sleep $((attempt * 2))
  attempt=$((attempt + 1))
done
if ! git checkout --quiet --force -B "$branch" FETCH_HEAD; then
  echo "byggsteg: fetched $branch from $origin but could not check it out" >&2
  exit 1
fi
after=$(git rev-parse --short HEAD)
if [ "$before" = "$after" ]; then
  echo "byggsteg: $branch was already at $after"
else
  echo "byggsteg: $branch moved from $before to $after"
fi


byggsteg: trunk moved from 8f64796 to d4bd94e

→ running command: cd /etc/byggsteg/job-clone/orgwebalchemy/trunk && uname -a

Linux wolk-jjba 7.2.11-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 d4bd94eaa1396e1066b2ecaf10534b7a59e2626d
Author: Josep Bigorra <jjbigorra@gmail.com>
Date:   Wed Nov 4 23:09:46 2026 +0100

    docs: Update visual identity

M       README.org
A       logo.svg

→ 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 519171294 (replay with VERITAS_SEED=519171294)

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 519171294 (replay with VERITAS_SEED=519171294)
------------------------------------------
[suite] Full document Markdown
  [test] the feature fixture matches its snapshot
    [assert] sample-org-features markdown
    [pass] matches snapshot test/snapshots/sample-org-features-markdown.snap
  [test] the fixture keeps the metadata keyword
    [pass] got: title: Feature Showcase
  [test] the sample text fixture converts without error
    [pass] success running without errors
    [pass] got: # Top Level Heading

This is a standard paragraph.

It sits right under the heading.

## A Subheading

- First list item
- Second list item

### An ordered subheading

1. yes buddy
2. another item

## Another

**key**
:   val
**desc**
:   conte

*In Vino Veritas* but *In Aqua Sanitas*

```scheme
(define (hello)
  (display \"Hello World!\"))
```


  [test] the fixture keeps inline markup
    [pass] got: **bold**
    [pass] got: ~~strikethrough~~
------------------------------------------
[suite] Figures in Markdown
  [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 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 caption names an image with no alt text of its own
    [pass] got: ![A caption](a.png)

*A caption*


------------------------------------------
[suite] Named elements in Markdown
  [test] a named table keeps an anchor for its cross-references
    [pass] got: <a id="tbl:data"></a>

| a |
| --- |


  [test] a bare link to a name points at the anchor
    [pass] got: [tbl:data](#tbl:data)
  [test] a named quote block keeps its anchor too
    [pass] got: <a id="q"></a>

> x


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


------------------------------------------
[suite] Links and images in Markdown
  [test] file links lose their file: prefix
    [pass] got: [notes](notes.org)


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


  [test] a heading link becomes the heading slug
    [pass] got: [up](#notes)


    [pass] got: [there](#custom-id)


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


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


------------------------------------------
[suite] Mixed and nested list types
  [test] consecutive kinds become separate Markdown lists
    [pass] got: - u
1. o
**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] a description list nests under an unordered item
    [pass] got: - term
  **k**
  :   v


------------------------------------------
[suite] Nested Markdown structures
  [test] nested ordered lists are indented
    [pass] got: 1. a
   1. b


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


  [test] a checkbox nested inside a list
    [pass] got: - a
  - [ ] b


------------------------------------------
[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-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] 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] Inline Org renders to Markdown
  [test] /italic/ -> *italic*
    [pass] got: *italic*
  [test] x^{2} -> x<sup>2</sup>
    [pass] got: x<sup>2</sup>
  [test] ~code~ -> `code`
    [pass] got: `code`
  [test] +strike+ -> ~~strike~~
    [pass] got: ~~strike~~
  [test] =verbatim= -> `verbatim`
    [pass] got: `verbatim`
  [test] https://x.org -> <https://x.org>
    [pass] got: <https://x.org>
  [test] H_{2}O -> H<sub>2</sub>O
    [pass] got: H<sub>2</sub>O
  [test] plain -> plain
    [pass] got: plain
  [test] _underline_ -> <u>underline</u>
    [pass] got: <u>underline</u>
  [test] [fn::note] -> ^[note]
    [pass] got: ^[note]
  [test] [[u][d]] -> [d](u)
    [pass] got: [d](u)
  [test] *bold* -> **bold**
    [pass] got: **bold**
  [test] [[u]] -> [u](u)
    [pass] got: [u](u)
  [test] [fn:1] -> [^1]
    [pass] got: [^1]
------------------------------------------
[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 line that is only its closing bar holds one empty cell
    [pass] got: |  |
| --- |


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


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


    [pass] got: ```
```


[info] ran 10 tasks in: 1 seconds, -802445 microseconds

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


[info] shuffled with seed 519171294 (replay with VERITAS_SEED=519171294)
------------------------------------------
[suite] Run scanners
  [test] a line ends at a newline, and a table cell at its separator
    [pass] got: (table (table-row (table-cell  a ) (table-cell  b )))
    [pass] got: (table (table-row (table-cell    )))
  [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 )))
    [pass] got: (table (table-row (table-cell  a ) (table-cell )))
  [test] a run ends where an inline construct really begins
    [pass] got: ((inline-text see) (inline-raw-url (url-scheme https://) example.org/a))
    [pass] got: ((inline-text a) (inline-bold-content 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] the last line of a document without a newline parses
    [pass] got: (paragraph (line-content text))
  [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)
  [test] a tab and non-ASCII text are ordinary line content
    [pass] got: (paragraph (line-content a      b))
    [pass] got: (paragraph (line-content café 中文))
  [test] an empty value is text, not a gap in the tree
    [pass] got: (table (table-row (table-cell a) (table-cell ) (table-cell b)))
    [pass] got: (src-block (lang ) (src-content (x)
))
    [pass] got: (keyword (keyword-key KEY) (keyword-value ))
    [pass] got: (quote-block (quote-content ))
  [test] a long line is one string, not one fragment per character
    [pass] got: line-content
    [pass] got: #t
    [pass] got: 10000
  [test] a lone carriage return is content, as it always was
b )))pass] got: (table (table-row (table-cell  a
    [pass] got: (paragraph (line-content a))
------------------------------------------
[suite] Whole-document parsing
  [test] the feature fixture is parsed all the way to the end
    [pass] got: ((keyword (keyword-key title) (keyword-value Feature Showcase)) (keyword (keyword-key author) (keyword-value OrgWebAlchemy)) (comment-line (comment-content A single comment line.)) (heading * (line-content Introduction)) (paragraph (line-content This paragraph shows *bold*, /italic/, ~code~, =verbatim=, _underline_, +strikethrough+, x^{2}, H_{2}O.)) (paragraph (line-content A forced break here:\\)) (paragraph (line-content and then the continuation.)) (paragraph (line-content See [[https://gnu.org][GNU]] or a raw link https://example.org plus a)) (paragraph (line-content footnote reference[fn:one] and an inline footnote[fn::like this].)) (heading * (line-content Links and images)) (paragraph (line-content An external [[https://example.org/docs][documentation link]], a bare [[notes.org]],)) (paragraph (line-content an [[file:photo.png][inline image]], a bare [[images/diagram.svg]] and a jump)) (paragraph (line-content [[file:notes.org::*Introduction][into another file]].)) (paragraph (line-content A local anchor [[#custom-id][custom id]] and a heading link [[*Introduction][up top]].)) (paragraph (line-content Mail [[mailto:hello@example.org][the author]] or dial [[tel:+123456]].)) (paragraph (line-content A figure cross-reference [[fig:showcase]].)) (figure (figure-name fig:showcase) (figure-caption The showcase diagram) (figure-image (figure-target images/showcase.svg) (figure-description a showcase diagram))) (paragraph (line-content [fn:one] The footnote definition.)) (heading * (line-content Named elements)) (named (element-name tbl:metrics) (table (table-header-row (table-cell  Metric ) (table-cell  Value )) table-separator-row (table-row (table-cell  Users  ) (table-cell  1024  )))) (named (element-name feature-list) (org-list (unordered-item (line-content one)) (unordered-item (line-content two)))) (paragraph (line-content A table cross-reference [[tbl:metrics]] and one to [[feature-list]].)) (paragraph (line-content The figure [[fig:showcase]] is named too.)) (heading * (heading-todo TODO) (heading-priority A) (line-content A Task) (heading-tags work urgent)) (org-list (unordered-item (line-content [ ] unchecked item)) (unordered-item (line-content [X] checked item) (nested-list (unordered-item (line-content nested item))))) (heading * (line-content Lists)) (org-list (ordered-item (line-content first)) (ordered-item (line-content second))) (org-list (desc-item (desc-key Term) (line-content Definition))) (org-list (unordered-item (line-content outer) (nested-list (ordered-item (line-content nested ordered)) (ordered-item (line-content another)))) (desc-item (desc-key Tail) (line-content detail))) (heading * (line-content Blocks)) (src-block (lang scheme) (src-content (display "hi")
)) (example-block (example-content *literal*
)) (quote-block (quote-content Quoted *text*
)) (verse-block (verse-content line one
line two
)) (center-block (center-content centered line
)) (export-html-block (export-html-content <div class="raw">Raw HTML</div>
)) (comment-block (comment-block-content hidden comment
)) (fixed-width (fixed-width-content a fixed-width line)) (separator ---------) (table (table-header-row (table-cell  Name ) (table-cell  Age )) table-separator-row (table-row (table-cell  Ada  ) (table-cell  36  ))))
    [pass] got: ((keyword (keyword-key title) (keyword-value Feature Showcase)) (keyword (keyword-key author) (keyword-value OrgWebAlchemy)) (comment-line (comment-content A single comment line.)) (heading * (line-content Introduction)) (paragraph (line-content This paragraph shows *bold*, /italic/, ~code~, =verbatim=, _underline_, +strikethrough+, x^{2}, H_{2}O.)) (paragraph (line-content A forced break here:\\)) (paragraph (line-content and then the continuation.)) (paragraph (line-content See [[https://gnu.org][GNU]] or a raw link https://example.org plus a)) (paragraph (line-content footnote reference[fn:one] and an inline footnote[fn::like this].)) (heading * (line-content Links and images)) (paragraph (line-content An external [[https://example.org/docs][documentation link]], a bare [[notes.org]],)) (paragraph (line-content an [[file:photo.png][inline image]], a bare [[images/diagram.svg]] and a jump)) (paragraph (line-content [[file:notes.org::*Introduction][into another file]].)) (paragraph (line-content A local anchor [[#custom-id][custom id]] and a heading link [[*Introduction][up top]].)) (paragraph (line-content Mail [[mailto:hello@example.org][the author]] or dial [[tel:+123456]].)) (paragraph (line-content A figure cross-reference [[fig:showcase]].)) (figure (figure-name fig:showcase) (figure-caption The showcase diagram) (figure-image (figure-target images/showcase.svg) (figure-description a showcase diagram))) (paragraph (line-content [fn:one] The footnote definition.)) (heading * (line-content Named elements)) (named (element-name tbl:metrics) (table (table-header-row (table-cell  Metric ) (table-cell  Value )) table-separator-row (table-row (table-cell  Users  ) (table-cell  1024  )))) (named (element-name feature-list) (org-list (unordered-item (line-content one)) (unordered-item (line-content two)))) (paragraph (line-content A table cross-reference [[tbl:metrics]] and one to [[feature-list]].)) (paragraph (line-content The figure [[fig:showcase]] is named too.)) (heading * (heading-todo TODO) (heading-priority A) (line-content A Task) (heading-tags work urgent)) (org-list (unordered-item (line-content [ ] unchecked item)) (unordered-item (line-content [X] checked item) (nested-list (unordered-item (line-content nested item))))) (heading * (line-content Lists)) (org-list (ordered-item (line-content first)) (ordered-item (line-content second))) (org-list (desc-item (desc-key Term) (line-content Definition))) (org-list (unordered-item (line-content outer) (nested-list (ordered-item (line-content nested ordered)) (ordered-item (line-content another)))) (desc-item (desc-key Tail) (line-content detail))) (heading * (line-content Blocks)) (src-block (lang scheme) (src-content (display "hi")
)) (example-block (example-content *literal*
)) (quote-block (quote-content Quoted *text*
)) (verse-block (verse-content line one
line two
)) (center-block (center-content centered line
)) (export-html-block (export-html-content <div class="raw">Raw HTML</div>
)) (comment-block (comment-block-content hidden comment
)) (fixed-width (fixed-width-content a fixed-width line)) (separator ---------) (table (table-header-row (table-cell  Name ) (table-cell  Age )) table-separator-row (table-row (table-cell  Ada  ) (table-cell  36  ))))
    [pass] got: keyword
  [test] the project's own README starts with a keyword and is long
    [pass] got: ((keyword (keyword-key title) (keyword-value OrgWebAlchemy)) (keyword (keyword-key subtitle) (keyword-value A Guile Scheme library for Org-mode)) (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)) (keyword (keyword-key description) (keyword-value A Guile Scheme library that parses Org-mode with a PEG grammar and renders clean HTML, Markdown, or SXML.)) (keyword (keyword-key keywords) (keyword-value Org-mode, Guile Scheme, PEG, HTML, Markdown, static site generator, free software)) (paragraph (line-content [[https://jointhefreeworld.org/ggg/dist/scheme-guile.svg]] [[https://jointhefreeworld.org/ggg/dist/gnu-guix.svg]] [[https://jointhefreeworld.org/ggg/dist/license-lgpl3+.svg]]   [[https://jointhefreeworld.org/ggg/dist/license-fdl13+.svg]])) (keyword (keyword-key attr_html) (keyword-value :width 160 :alt OrgWebAlchemy logo)) (paragraph (line-content [[file:logo.svg]])) (quote-block (quote-content *Org-mode in. HTML and Markdown out.*

OrgWebAlchemy is a small, dependency-light [[https://www.gnu.org/software/guile/][Guile Scheme]] library that parses Org-mode documents with a real [[https://en.wikipedia.org/wiki/Parsing_expression_grammar][PEG grammar]] and renders them to clean *HTML*, *Markdown*, or *SXML*. Write your notes, documentation, or blog in Org; publish them from Scheme, with the structure and the CSS you want.
)) (keyword (keyword-key caption) (keyword-value One Org document, rendered by both renderers. Generated from [[file:examples/hello.org][examples/hello.org]] with [[file:examples/demo.scm][examples/demo.scm]].)) (keyword (keyword-key attr_html) (keyword-value :width 100%)) (paragraph (line-content [[file:assets/orgwebalchemy-demo.gif]])) (org-list (desc-item (desc-key *What it is*) (line-content a parser and a pair of renderers for Org-mode, written in Guile Scheme and built around a Parsing Expression Grammar.)) (desc-item (desc-key *Who it is for*) (line-content Scheme hackers, static-site authors, wiki and notebook builders, tool makers, and anyone who keeps writing in Org but wants to publish with a library instead of an Emacs export.)) (desc-item (desc-key *Why you will like it*) (line-content it understands real Org rather than a regex-shaped subset, it gives every renderer the same AST, and it lets your CSS classes -- not ours -- decide how the output looks.)) (desc-item (desc-key *Freedom*) (line-content LGPL-3.0-or-later for the code, FDL-1.3-or-later for this documentation. No account, no telemetry, no build service required.))) (paragraph (line-content *Jump to:* [[#installation][Installation]] · [[#quick-start][Quick start]] · [[#configuration][Configuration]] · [[#supported-org-features][Supported features]] · [[#api-overview][API]] · [[#how-it-works][How it works]] · [[#benchmarks][Benchmarks]] · [[#contributing][Contributing]])) (paragraph (line-content The short version, if you already have Guile and this checkout on your load path:)) (src-block (lang scheme) (src-content (use-modules (orgwebalchemy html)
             (orgwebalchemy markdown))

(org->html "This is ~test~ code.")
;; => "This is <code>test</code> code."

(org->markdown "This is ~test~ code.")
;; => "This is `test` code."
)) (paragraph (line-content And the full technical API documentation lives here:)) (paragraph (line-content https://jointhefreeworld.org/api-docs/orgwebalchemy/API.html)) (separator ---------------------) (heading * (line-content Why OrgWebAlchemy?)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: why-orgwebalchemy)) (paragraph (line-content :END:)) (paragraph (line-content Org mode is a wonderful place to write. It is a less wonderful thing to parse.)) (paragraph (line-content A real Org document is not a pile of lines that each match a regular expression. It has nested lists, source blocks, description lists, links whose meaning depends on their target, inline markup with escaping rules, and constructs that end only when the surrounding context says they end. Tools built from regex substitutions tend to accumulate special cases until the regexes describe the bugs in the parser rather than the language.)) (paragraph (line-content OrgWebAlchemy takes a different route:)) (example-block (example-content Org text
   |
   v
 PEG grammar
   |
   v
 AST
   |        |
   v        v
 HTML     Markdown
 renderer renderer
)) (paragraph (line-content The grammar describes the language. The parser produces a structured tree. The renderers turn that tree into output. Syntax stays in the parser and presentation stays in the renderers, which is why the HTML and Markdown back ends can agree about what a link, a figure, or a heading means.)) (paragraph (line-content Here is what that buys you, depending on what you are building:)) (table (table-header-row (table-cell  If you want to...                              ) (table-cell  Reach for...                                            )) table-separator-row (table-row (table-cell  Turn an Org string into an HTML fragment       ) (table-cell  ~org->html~                                             )) (table-row (table-cell  Turn an Org string into Markdown               ) (table-cell  ~org->markdown~                                         )) (table-row (table-cell  Parse once and render more than once           ) (table-cell  ~org->ast~, then ~org-ast->html~ or ~org-ast->markdown~ )) (table-row (table-cell  Post-process the document tree with SXML       ) (table-cell  ~org->sxml~, ~org-ast->sxml~                            )) (table-row (table-cell  Style every element for your design system     ) (table-cell  the HTML class parameters                               )) (table-row (table-cell  Embed Org rendering in a static site generator ) (table-cell  the three modules below                                 )) (table-row (table-cell  Understand how the parser works                ) (table-cell  [[#how-it-works][the PEG section]]                                         ))) (paragraph (line-content OrgWebAlchemy is a library, not an application. It has no opinion about your web framework, your static-site generator, your templating language, or your CSS. It gives you a correct tree and clean output; the rest is yours.)) (separator ---------------------) (heading * (line-content Features)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: features)) (paragraph (line-content :END:)) (org-list (desc-item (desc-key *One parser, two renderers*) (line-content ~org->html~ and ~org->markdown~ are built on the same parsed AST. They cannot drift apart on what a link target, a heading anchor, a figure, or a named element means.)) (desc-item (desc-key *A real AST*) (line-content ~org->ast~ returns a tree of nodes, not a string that needs to be reparsed. Inspect it, transform it, or render it yourself.)) (desc-item (desc-key *HTML with class hooks everywhere*) (line-content every element the HTML renderer emits takes its CSS classes from a Guile parameter. Bootstrap, Tailwind, Bulma, hand-written CSS -- your call.)) (desc-item (desc-key *Markdown without hand-waving*) (line-content underlines, superscripts, subscripts, description lists, and footnotes fall back to inline HTML or the Markdown conventions that preserve their meaning.)) (desc-item (desc-key *Links that know where they go*) (line-content link targets are classified once, in the parser: external URLs, ~file:~ links, bare paths, ~#custom-id~ links, ~*Heading~ links, image links, and ~::search~ suffixes.)) (desc-item (desc-key *Figures and named elements*) (line-content ~#+CAPTION:~ and ~#+NAME:~ make figures and addressable elements; ~[[fig:name]]~, ~[[tbl:name]]~, and bare-name links resolve to them in both renderers.)) (desc-item (desc-key *Org, not a toy subset*) (line-content headings with TODO keywords, priorities and tags; nested mixed lists; checkboxes; tables with empty cells; source, example, quote, verse, center and export blocks; footnotes; fixed-width lines; separators; comments; and more.)) (desc-item (desc-key *SXML when you need it*) (line-content ~org->sxml~ and ~org-ast->sxml~ let you post-process the tree with ~(sxml simple)~ or any SXML tool you already use.)) (desc-item (desc-key *Small footprint*) (line-content no runtime dependencies beyond Guile's standard library. The whole library is three modules.)) (desc-item (desc-key *Tested and measured*) (line-content unit suites cover the grammar and both renderers; equivalence checks compare parser revisions over tens of thousands of inputs; benchmarks track parse and render cost.)) (desc-item (desc-key *Free software*) (line-content LGPL-3.0-or-later. Use it in free and non-free programs, improve it, share it.))) (separator ---------------------) (heading * (line-content How it works: parsing Org with PEG)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: how-it-works)) (paragraph (line-content :END:)) (heading ** (line-content What is a PEG? (Parsing Expression Grammars))) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: what-is-a-peg-parsing-expression-grammars)) (paragraph (line-content :END:)) (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 :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: peg-instead-of-regex-soup)) (paragraph (line-content :END:)) (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. Other technologies and implementations can achieve similar things, but not, I would argue, with the same elegance.)) (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 :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: peg-gives-structure)) (paragraph (line-content :END:)) (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 :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: context-matters)) (paragraph (line-content :END:)) (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 :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: peg-is-composable)) (paragraph (line-content :END:)) (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 expression.)) (heading ** (line-content PEG is not magic)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: peg-is-not-magic)) (paragraph (line-content :END:)) (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.)) (heading ** (line-content Why not just use regex?)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: why-not-just-use-regex)) (paragraph (line-content :END:)) (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 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 Configuration)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: configuration)) (paragraph (line-content :END:)) (heading ** (line-content Styling HTML with class parameters)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: styling-html-with-class-parameters)) (paragraph (line-content :END:)) (paragraph (line-content The HTML renderer emits semantic elements with no styles attached. Every element reads its CSS classes from a Guile parameter, so one document can be styled for any design system without touching 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"))
;; => "<p class=\"leading-relaxed mb-4\">This paragraph is <strong>bold</strong>.</p>"
)) (paragraph (line-content Each parameter is a list of class-name strings, except ~heading-classes~, which is a procedure of the heading level so that each level can be styled on its own. Parameters are dynamic: use ~parameterize~ to scope a change to one render call, or set them once for your program.)) (org-list (desc-item (desc-key *Inline*) (line-content ~link-classes~, ~image-classes~, ~code-classes~, ~italic-classes~, ~bold-classes~, ~underline-classes~, ~strike-classes~, ~superscript-classes~, ~subscript-classes~.)) (desc-item (desc-key *Blocks*) (line-content ~paragraph-classes~, ~code-block-classes~, ~quote-block-classes~, ~example-block-classes~, ~verse-block-classes~, ~center-block-classes~, ~separator-classes~, ~keyword-classes~, ~fixed-width-classes~.)) (desc-item (desc-key *Lists*) (line-content ~unordered-list-classes~, ~unordered-list-item-classes~, ~ordered-list-classes~, ~ordered-list-item-classes~, ~description-list-classes~, ~description-term-classes~, ~description-detail-classes~, ~checkbox-classes~.)) (desc-item (desc-key *Tables*) (line-content ~table-classes~, ~table-header-row-classes~, ~table-header-cell-classes~, ~table-row-classes~, ~table-cell-classes~.)) (desc-item (desc-key *Media*) (line-content ~image-classes~, ~figure-classes~, ~figure-caption-classes~.)) (desc-item (desc-key *Headings*) (line-content ~heading-classes~ (a procedure of the heading level).))) (paragraph (line-content All parameters are exported by ~(orgwebalchemy html)~ and default to the empty)) (paragraph (line-content class list, except ~heading-classes~, whose default is a procedure that returns)) (paragraph (line-content the empty list for every level.)) (heading ** (line-content What is deliberately not configurable)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: what-is-deliberately-not-configurable)) (paragraph (line-content :END:)) (paragraph (line-content The Markdown renderer has no class parameters: Markdown has no styling of its own. Its job is to preserve structure and meaning, falling back to inline HTML only where Markdown has no equivalent. Link classification, figure folding, and name resolution also live in the parser, so the two renderers always agree about them.)) (heading ** (line-content Heading anchors)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: heading-anchors)) (paragraph (line-content :END:)) (paragraph (line-content Every heading carries an ~id~, and a ~[[*Heading][link]]~ points at the same anchor. Both are produced by ~org-anchor~, so they cannot drift apart:)) (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 A title with nothing anchor-worthy in it gets no ~id~ at all. ~heading-id~ is the helper that builds the attribute.)) (separator ---------------------) (heading * (line-content Supported Org features)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: supported-org-features)) (paragraph (line-content :END:)) (paragraph (line-content Org is a large language. OrgWebAlchemy aims to handle the constructs people actually write, and to be honest about the edges it does not reach yet. The supported surface is:)) (org-list (desc-item (desc-key *Block elements*) (line-content headings, paragraphs, lists, tables, source/example/quote/verse/center/export blocks, comments, fixed-width lines, separators, keywords, footnotes.)) (desc-item (desc-key *Inline markup*) (line-content bold, italic, underline, strikethrough, inline code, verbatim, superscript, subscript, forced line breaks, escaped markup.)) (desc-item (desc-key *Links and media*) (line-content links with and without descriptions, bare URLs, image links, figures, named elements, ~file:~ and ~#custom-id~ targets, ~*Heading~ links, ~::search~ suffixes.)) (desc-item (desc-key *Output*) (line-content HTML fragments, SXML, Markdown, inline-only rendering, configurable CSS classes for HTML.))) (heading ** (line-content Block elements)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: block-elements)) (paragraph (line-content :END:)) (org-list (unordered-item (line-content Headings (lines starting with one or more ~*~), 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 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)) (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 Footnotes: references (~[fn:label]~), definitions (~[fn:label: text]~) and))) (paragraph (line-content   inline footnotes (~[fn::text]~))) (heading ** (line-content Inline markup)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: inline-markup)) (paragraph (line-content :END:)) (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)) (unordered-item (line-content Links to images, which render as images in both renderers))) (heading ** (line-content Links and images)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: links-and-images)) (paragraph (line-content :END:)) (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 :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: figures)) (paragraph (line-content :END:)) (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)) (paragraph (line-content not have.  ~figure-classes~ and ~figure-caption-classes~ style the element.)) (heading ** (line-content Named elements)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: named-elements)) (paragraph (line-content :END:)) (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 :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: exporting-to-markdown)) (paragraph (line-content :END:)) (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"
)) (heading ** (line-content Current limitations)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: current-limitations)) (paragraph (line-content :END:)) (paragraph (line-content Org is large, and this parser does not claim to cover every historical convention.)) (paragraph (line-content The known edges are:)) (org-list (unordered-item (line-content Captions on tables, source blocks, and other non-figure elements are not))) (paragraph (line-content   rendered yet; the ~#+CAPTION:~ line stays an ordinary keyword line. Figures)) (paragraph (line-content   are the exception.)) (org-list (unordered-item (line-content There is no incremental parse-and-render path yet. A changed document is))) (paragraph (line-content   parsed from the beginning.)) (org-list (unordered-item (line-content The benchmarks measure this implementation, not other Org implementations,))) (paragraph (line-content   and do not compare against them.)) (org-list (unordered-item (line-content OrgWebAlchemy is a library, not an Emacs package: it does not integrate with))) (paragraph (line-content   Org's export dispatcher, agenda, or Babel.)) (paragraph (line-content If one of these gets in your way, that is exactly the kind of issue worth reporting.)) (paragraph (line-content See [[#contributing][Contributing]].)) (separator ---------------------) (heading * (line-content Requirements)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: requirements)) (paragraph (line-content :END:)) (org-list (unordered-item (line-content *Guile 3.0 or newer.* The library uses Guile's ~(ice-9 peg)~ module and Guile 3 features such as declarative modules.)) (unordered-item (line-content *No external runtime dependencies.* Everything else comes from Guile's standard library.)) (unordered-item (line-content *Guix is recommended, but optional.* It is the easiest way to get a matching Guile, the test framework, and the documentation tools. The library itself works with any Guile 3 installation.)) (unordered-item (line-content *For development:* [[https://codeberg.org/jjba23/veritas][Veritas]], ~guile-documenta~, Texinfo, ~watchexec~, and ~guile-ares-rs~. The included [[file:manifest.scm][manifest.scm]] provides all of them.))) (paragraph (line-content The project is developed and tested on GNU/Linux. If you get it running elsewhere, a note in the issue tracker is welcome.)) (separator ---------------------) (heading * (line-content Ways to install)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: ways-to-install)) (paragraph (line-content :END:)) (paragraph (line-content /Four routes to the same 1.2.2, from the most automated to the most manual.  They all build the tag this README documents./)) (heading ** (line-content From the Guix channel)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: from-the-guix-channel)) (paragraph (line-content :END:)) (paragraph (line-content *GNU Guix* is the functional package manager this project is developed with: a build is a pure function of its inputs, so the same recipe gives the same result everywhere, and an upgrade that goes wrong can be rolled back.)) (paragraph (line-content The jointhefreeworld channel carries ~guile-orgwebalchemy~ at 1.2.2, so a checkout of the channel is enough — no package definition to write:)) (src-block (lang shell) (src-content git clone https://codeberg.org/jjba23/guix-jointhefreeworld.git
guix install -L ./guix-jointhefreeworld guile-orgwebalchemy
)) (paragraph (line-content Add the channel to ~$HOME/.config/guix/channels.scm~ (the channel's README has the four lines) and a plain ~guix install guile-orgwebalchemy~ keeps working on every later ~guix pull~.)) (heading ** (line-content From the Nix flake)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: from-the-nix-flake)) (paragraph (line-content :END:)) (paragraph (line-content *Nix* offers the same declarative, reproducible model with its own language, its own store and a binary cache, and it is a natural fit if the rest of your machine is already described in Nix.)) (paragraph (line-content The jointhefreeworld flake builds this project with this repository's own expression, so the package cannot drift from the source:)) (src-block (lang shell) (src-content nix build github:jjba23/nix-jointhefreeworld#guile-orgwebalchemy
)) (paragraph (line-content The same attribute is available as an overlay, so ~pkgs.jointhefreeworld.guile-orgwebalchemy~ works inside a NixOS or home-manager configuration too.)) (heading ** (line-content As a container image)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: as-a-container-image)) (paragraph (line-content :END:)) (paragraph (line-content *Podman or Docker* is the route that puts nothing on the host: the ~Dockerfile~ in this repository builds the project in a builder stage and copies only what it installed into a runtime stage, so the image is small and the host keeps its own toolchain untouched.)) (src-block (lang shell) (src-content podman build -t orgwebalchemy .   # docker build -t orgwebalchemy . also works
podman run --rm -it orgwebalchemy
)) (paragraph (line-content The image's entry point is Guile, with orgwebalchemy on its load path, which makes it useful for a quick try and for reproducing a bug on a clean system.)) (heading ** (line-content As a distribution package)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: as-a-distribution-package)) (paragraph (line-content :END:)) (paragraph (line-content *Debian, openSUSE and Arch* each get the project through their own package manager.  Packaging lives in this repository under ~packaging/~ and is pinned to the same release tag, so the recipes and the sources they build are versioned together.)) (paragraph (line-content None of the three archives ships orgwebalchemy yet, so these are recipes to build rather than packages to install from a mirror:)) (table (table-header-row (table-cell  Distribution   ) (table-cell  Package         ) (table-cell  How to build it                                                                     )) table-separator-row (table-row (table-cell  Debian, Ubuntu ) (table-cell  ~orgwebalchemy~ ) (table-cell  copy ~packaging/debian~ to the repository root, then ~dpkg-buildpackage -us -uc -b~ )) (table-row (table-cell  openSUSE       ) (table-cell  ~orgwebalchemy~ ) (table-cell  ~osc build~ with ~packaging/opensuse~                                               )) (table-row (table-cell  Arch           ) (table-cell  ~orgwebalchemy~ ) (table-cell  ~makepkg -si~ in ~packaging/arch~                                                   ))) (paragraph (line-content Each recipe carries its own dependency list, so a missing library fails the build with its name rather than producing a package that cannot load.)) (heading ** (line-content Which one?)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: which-one)) (paragraph (line-content :END:)) (org-list (unordered-item (line-content *Guix* if you want the toolchain the project is tested with, and the ability to roll back a bad upgrade.)) (unordered-item (line-content *Nix* if the rest of your machine is already declarative; the flake pins nixpkgs and gives you a binary cache.)) (unordered-item (line-content *Container* if you only want to try the release, or reproduce a report on a machine that has nothing installed.)) (unordered-item (line-content *Distribution package* if you want the project managed by the same tool as the rest of your system, with its licence, manual page and launcher in the usual places.))) (heading * (line-content Installation)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: installation)) (paragraph (line-content :END:)) (heading ** (line-content The quickest path: a checkout and a Guix shell)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: the-quickest-path-a-checkout-and-a-guix-shell)) (paragraph (line-content :END:)) (src-block (lang shell) (src-content git clone https://codeberg.org/jjba23/orgwebalchemy.git
cd orgwebalchemy
guix shell -m manifest.scm -- guile -L ./src examples/demo.scm
)) (paragraph (line-content That command renders [[file:examples/hello.org][examples/hello.org]] and prints both an HTML fragment and its Markdown equivalent. It is the same document you see in the animated demo above.)) (paragraph (line-content If you already have Guile 3 and do not want a Guix shell, run the demo directly:)) (src-block (lang shell) (src-content guile -L ./src examples/demo.scm
)) (heading ** (line-content Use it in your own project)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: use-it-in-your-own-project)) (paragraph (line-content :END:)) (heading *** (line-content With Guix)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: with-guix)) (paragraph (line-content :END:)) (paragraph (line-content OrgWebAlchemy is on its way to being packaged in Guix officially as ~guile-orgwebalchemy~. Until then, the recipe below is self-contained and builds the current release. Save it as ~guile-orgwebalchemy.scm~:)) (src-block (lang scheme) (src-content (use-modules (guix packages)
             (guix download)
             (guix build-system guile)
             (gnu packages guile)
             ((guix licenses) #:prefix license:))

(define-public guile-orgwebalchemy
  (package
    (name "guile-orgwebalchemy")
    (version "1.2.2")
    (source
     (origin
       (method url-fetch)
       (uri (string-append
             "https://codeberg.org/jjba23/orgwebalchemy/archive/v"
             version ".tar.gz"))
       (sha256
        (base32 "0lb1xhd9fshhxadqh73z4md2ngpv3pqylsaxrj65qxzv1swwx2qy"))))
    (build-system guile-build-system)
    (arguments
     (list #:source-directory "src"))
    (native-inputs (list guile-3.0))
    (home-page "https://codeberg.org/jjba23/orgwebalchemy")
    (synopsis "Convert Org markup to HTML and Markdown with Guile Scheme")
    (description
     "OrgWebAlchemy parses Org-mode documents with a PEG grammar and renders
them to HTML, Markdown, or SXML.")
    (license license:lgpl3+)))
)) (paragraph (line-content Build it, or add it to your own channel:)) (src-block (lang shell) (src-content guix build -f guile-orgwebalchemy.scm
)) (paragraph (line-content When you bump the version, refresh the hash with:)) (src-block (lang shell) (src-content guix download https://codeberg.org/jjba23/orgwebalchemy/archive/v1.2.2.tar.gz
)) (paragraph (line-content Then use the package as you would any other Guix package. The simplest way is)) (paragraph (line-content to put both files in the same directory and load the package definition from a)) (paragraph (line-content manifest:)) (src-block (lang scheme) (src-content (use-modules (guix profiles)
             (guix packages)
             (gnu packages guile))

(load "guile-orgwebalchemy.scm")

(packages->manifest
 (list guile-3.0 guile-orgwebalchemy))
)) (src-block (lang shell) (src-content guix shell -m manifest-with-orgwebalchemy.scm -- guile your-program.scm
)) (paragraph (line-content If you maintain a Guix channel, add the package definition to it and use)) (paragraph (line-content ~guile-orgwebalchemy~ like any other package from that channel.)) (heading *** (line-content Manually, with ~GUILE_LOAD_PATH~)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: manually-with-guileloadpath)) (paragraph (line-content :END:)) (paragraph (line-content Clone the repository and point Guile at its ~src~ directory:)) (src-block (lang shell) (src-content git clone https://codeberg.org/jjba23/orgwebalchemy.git
export GUILE_LOAD_PATH="$PWD/orgwebalchemy/src${GUILE_LOAD_PATH:+:$GUILE_LOAD_PATH}"
)) (paragraph (line-content Or pass the same directory on the command line, which keeps the change local to one program:)) (src-block (lang shell) (src-content guile -L /path/to/orgwebalchemy/src your-program.scm
)) (heading *** (line-content Verify the installation)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: verify-the-installation)) (paragraph (line-content :END:)) (src-block (lang shell) (src-content guile -c '(use-modules (orgwebalchemy html)) (display (org->html "* It works!\n"))'
)) (paragraph (line-content You should see:)) (src-block (lang html) (src-content <h1 id="it-works">It works!</h1>
)) (separator ---------------------) (heading * (line-content Quick start)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: quick-start)) (paragraph (line-content :END:)) (heading ** (line-content Render a string to HTML)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: render-a-string-to-html)) (paragraph (line-content :END:)) (src-block (lang scheme) (src-content (use-modules (orgwebalchemy html))

(org->html "This is ~test~ code.")
;; => "This is <code>test</code> code."
)) (heading ** (line-content Render a string to Markdown)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: render-a-string-to-markdown)) (paragraph (line-content :END:)) (src-block (lang scheme) (src-content (use-modules (orgwebalchemy markdown))

(org->markdown "This is ~test~ code.")
;; => "This is `test` code."
)) (heading ** (line-content Render a file)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: render-a-file)) (paragraph (line-content :END:)) (src-block (lang scheme) (src-content (use-modules (ice-9 textual-ports)
             (orgwebalchemy html)
             (orgwebalchemy markdown))

(define org-source
  (call-with-input-file "notes.org" get-string-all))

(display (org->html org-source))
(newline)
(display (org->markdown org-source))
)) (heading ** (line-content From the shell)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: from-the-shell)) (paragraph (line-content :END:)) (src-block (lang shell) (src-content guix shell -m manifest.scm -- \
  guile -L ./src \
  -c '(use-modules (orgwebalchemy html)) (display (org->html "* Hello, Org!\n"))'
)) (paragraph (line-content That prints:)) (src-block (lang html) (src-content <h1 id="hello-org">Hello, Org!</h1>
)) (heading ** (line-content A complete, runnable demo)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: a-complete-runnable-demo)) (paragraph (line-content :END:)) (paragraph (line-content [[file:examples/demo.scm][examples/demo.scm]] reads [[file:examples/hello.org][examples/hello.org]] and prints both renderings. It is deliberately small -- a good starting point to copy into your own project:)) (src-block (lang scheme) (src-content (use-modules (ice-9 textual-ports)
             (orgwebalchemy html)
             (orgwebalchemy markdown))

(define here
  (dirname (current-filename)))

(define org-source
  (call-with-input-file (string-append here "/hello.org")
    get-string-all))

(display ";; HTML fragment\n")
(display (org->html org-source))
(newline)
(newline)
(display ";; Markdown\n")
(display (org->markdown org-source))
)) (heading ** (line-content How the pieces fit together)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: how-the-pieces-fit-together)) (paragraph (line-content :END:)) (org-list (unordered-item (line-content *Just need a string?* Use ~org->html~ or ~org->markdown~. They parse and render in one call.)) (unordered-item (line-content *Need to render more than once?* Parse with ~org->ast~, then call ~org-ast->html~ or ~org-ast->markdown~.)) (unordered-item (line-content *Need to transform the tree?* Work with the AST directly, or use ~org->sxml~ and stay in SXML.)) (unordered-item (line-content *Need inline markup only?* Use ~inline-org->ast~, ~inline-org->html~, or ~inline-org->markdown~.))) (separator ---------------------) (heading * (line-content API overview)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: api-overview)) (paragraph (line-content :END:)) (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.  The full generated API documentation is at)) (paragraph (line-content https://jointhefreeworld.org/api-docs/orgwebalchemy/API.html.)) (heading ** (line-content ~(orgwebalchemy peg)~ -- the parser)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: orgwebalchemy-peg-the-parser)) (paragraph (line-content :END:)) (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)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: orgwebalchemy-html-rendering-to-html)) (paragraph (line-content :END:)) (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.)) (paragraph (line-content The full set of CSS class parameters is documented under [[#configuration][Configuration]].)) (heading ** (line-content ~(orgwebalchemy markdown)~ -- rendering to Markdown)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: orgwebalchemy-markdown-rendering-to-markdown)) (paragraph (line-content :END:)) (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 What the AST looks like)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: what-the-ast-looks-like)) (paragraph (line-content :END:)) (paragraph (line-content The parser is not merely answering "does this line match?" It is answering)) (paragraph (line-content "what is this thing, and what are its parts?" For example, this Org:)) (example-block (example-content - name :: Joe
)) (paragraph (line-content becomes something conceptually similar to:)) (src-block (lang scheme) (src-content (org-list
 (desc-item
  (desc-key "name")
  (line-content "Joe")))
)) (paragraph (line-content A heading and a paragraph are nodes too:)) (src-block (lang scheme) (src-content (use-modules (orgwebalchemy peg))

(org->ast "* Hello\nA *bold* line.\n")
;; => ((heading "*" (line-content "Hello"))
;;     (paragraph (line-content "A *bold* line.")))
)) (paragraph (line-content Once you have that tree, rendering is a separate, much simpler problem -- and)) (paragraph (line-content so is any transformation you want to perform before rendering.)) (separator ---------------------) (heading * (line-content Benchmarks)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: benchmarks)) (paragraph (line-content :END:)) (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 the 39 KB README)) (paragraph (line-content those figures were measured on in 3.4 ms, and two renderers that walk the)) (paragraph (line-content resulting tree at the same speed as each other.  A 50-clause paragraph of)) (paragraph (line-content inline markup -- fifty links, bold runs and code spans in one line -- takes)) (paragraph (line-content 2.1 ms; inline parsing is the most expensive stage per byte, and deliberately)) (paragraph (line-content so, because a line that carries that many constructs has to try every)) (paragraph (line-content alternative at every position of it.)) (heading ** (line-content Allocation and GC pressure)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: allocation-and-gc-pressure)) (paragraph (line-content :END:)) (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 that README 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 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 :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: fragments-and-the-collapse-pass)) (paragraph (line-content :END:)) (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 one.)) (paragraph (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 that README 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 :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: what-the-benchmarks-do-not-cover)) (paragraph (line-content :END:)) (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 [[#contributing][Contributing]].)) (separator ---------------------) (heading * (line-content Testing)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: testing)) (paragraph (line-content :END:)) (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.)) (heading ** (line-content Running the suite)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: running-the-suite)) (paragraph (line-content :END:)) (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, if you have the project's task runner installed:)) (src-block (lang shell) (src-content maak test
)) (paragraph (line-content Fixtures under [[file:test/fixtures/]] are shared between the suites and the)) (paragraph (line-content benchmarks, so both stay honest about the same documents.)) (heading ** (line-content Equivalence and stop-set checks)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: equivalence-and-stop-set-checks)) (paragraph (line-content :END:)) (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.)) (heading ** (line-content Three deliberate differences)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: three-deliberate-differences)) (paragraph (line-content :END:)) (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 own))) (paragraph (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.)) (heading ** (line-content A trap when adding tests)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: a-trap-when-adding-tests)) (paragraph (line-content :END:)) (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).)) (separator ---------------------) (heading * (line-content Projects that use OrgWebAlchemy)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: projects)) (paragraph (line-content :END:)) (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))) (paragraph (line-content LucidPlan uses OrgWebAlchemy to render tickets, backlogs, and project notes:)) (keyword (keyword-key caption) (keyword-value A LucidPlan backlog, rendered with OrgWebAlchemy.)) (keyword (keyword-key attr_html) (keyword-value :width 100%)) (paragraph (line-content [[file:assets/lucidplan-screenshot.png]])) (paragraph (line-content If you build something with OrgWebAlchemy, send a link. It is always good to)) (paragraph (line-content see where the library travels.)) (separator ---------------------) (heading * (line-content What people are saying)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: what-people-are-saying)) (paragraph (line-content :END:)) (org-list (unordered-item (line-content [[https://jointhefreeworld.org/blog/articles/lisps/orgwebalchemy-benchmarking-profiling-and-org-support/][Benchmarking and Optimizing OrgWebAlchemy]] - OrgWebAlchemy: Benchmarking, Profiling, and the Long Road to Real Org Support)) (unordered-item (line-content [[https://jointhefreeworld.org/blog/articles/lisps/parsing-expression-grammar-lisp-org-convert-to-html/index.html][Parsing Expression Grammar in Lisp: convert Org to HTML]] -- the article that introduced the PEG approach behind OrgWebAlchemy.)) (unordered-item (line-content [[https://www.reddit.com/r/lisp/comments/1wox8h4/parsing_org_mode_with_parsing_expression_grammars/][Parsing Org mode with Parsing Expression Grammars]] -- discussion on r/lisp.))) (separator ---------------------) (heading * (line-content Contributing)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: contributing)) (paragraph (line-content :END:)) (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. Whether you)) (paragraph (line-content have a bug report, a half-formed idea, a documentation fix, a test case, a)) (paragraph (line-content benchmark, or a patch, you are welcome here. You do not need to be an Org)) (paragraph (line-content expert, a Guile expert, or a long-time contributor. Curiosity is enough.)) (heading ** (line-content Ways to contribute)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: ways-to-contribute)) (paragraph (line-content :END:)) (org-list (unordered-item (line-content *Report a bug.* A small Org snippet that shows the problem is worth more than))) (paragraph (line-content   a long description.)) (org-list (unordered-item (line-content *Suggest a feature.* Tell us what you are trying to build, not only what))) (paragraph (line-content   function you want.)) (org-list (unordered-item (line-content *Improve the documentation.* If something was confusing to you, it will be))) (paragraph (line-content   confusing to someone else too.)) (org-list (unordered-item (line-content *Add a test.* Edge cases around lists, tables, links, figures, and escaping))) (paragraph (line-content   are especially welcome.)) (org-list (unordered-item (line-content *Benchmark something.* More workloads, other Guile versions, and comparisons))) (paragraph (line-content   with other Org implementations would all be useful.)) (org-list (unordered-item (line-content *Build with it.* A static site generator, a wiki, a notebook, a CLI, an))) (paragraph (line-content   export pipeline -- real users are the best specification.)) (heading ** (line-content Reporting a bug)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: reporting-a-bug)) (paragraph (line-content :END:)) (paragraph (line-content Please include:)) (org-list (unordered-item (line-content the Guile version (~guile --version~) and how you installed OrgWebAlchemy))) (paragraph (line-content   (Guix, ~GUILE_LOAD_PATH~, a checkout);)) (org-list (unordered-item (line-content the smallest Org document that reproduces the problem;)) (unordered-item (line-content what you expected to happen;)) (unordered-item (line-content what actually happened, including the HTML or Markdown you got;)) (unordered-item (line-content if a test fails, the failing test's name and report, if you have one.))) (paragraph (line-content The issue tracker is on Codeberg:)) (paragraph (line-content https://codeberg.org/jjba23/orgwebalchemy/issues)) (heading ** (line-content Sending a patch)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: sending-a-patch)) (paragraph (line-content :END:)) (paragraph (line-content A comfortable workflow:)) (src-block (lang shell) (src-content git clone https://codeberg.org/jjba23/orgwebalchemy.git
cd orgwebalchemy
guix shell -m manifest.scm

# make your change
maak fmt      # format Scheme sources with guix style
maak test     # run the unit suite
)) (paragraph (line-content Then open a pull request on Codeberg. Small, focused changes are easiest to review.)) (paragraph (line-content If you are changing the grammar, add a test that pins down the old and)) (paragraph (line-content new behavior, and consider running the equivalence check described under)) (paragraph (line-content [[#testing][Testing]].)) (paragraph (line-content By contributing, you agree that your work can be distributed under the)) (paragraph (line-content project's licenses (LGPL-3.0-or-later for code, FDL-1.3-or-later for)) (paragraph (line-content documentation).)) (heading ** (line-content Development tasks)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: development-tasks)) (paragraph (line-content :END:)) (paragraph (line-content The project uses [[https://codeberg.org/jjba23/maak][maak]] as its task runner.)) (paragraph (line-content From the repository root:)) (table (table-header-row (table-cell  Task          ) (table-cell  What it does                                                       )) table-separator-row (table-row (table-cell  ~maak test~   ) (table-cell  Run the unit suite with Veritas.                                   )) (table-row (table-cell  ~maak fmt~    ) (table-cell  Format every ~.scm~ file with ~guix style~.                        )) (table-row (table-cell  ~maak docs~   ) (table-cell  Generate the API documentation with ~guile-documenta~ and Texinfo. )) (table-row (table-cell  ~maak repl~   ) (table-cell  Start a development REPL with ~guile-ares-rs~.                     )) (table-row (table-cell  ~maak dev~    ) (table-cell  Watch the sources and re-run the tests on change.                  )) (table-row (table-cell  ~maak deploy~ ) (table-cell  Generate and publish the API documentation.                        ))) (paragraph (line-content The manifest in [[file:manifest.scm][manifest.scm]] pins the development tools, so)) (paragraph (line-content ~guix shell -m manifest.scm~ gives you a matching environment.)) (heading ** (line-content Continuous integration)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: continuous-integration)) (paragraph (line-content :END:)) (paragraph (line-content Pushes to the ~trunk~ branch trigger the Woodpecker pipeline in)) (paragraph (line-content [[file:.woodpecker/byggsteg.yaml][.woodpecker/byggsteg.yaml]]. The pipeline submits the)) (paragraph (line-content ~maak-test~ and ~maak-deploy~ tasks to)) (paragraph (line-content [[https://byggsteg.jointhefreeworld.org][Byggsteg]]. If you are changing the)) (paragraph (line-content build or the test setup, run those tasks locally first.)) (heading ** (line-content Code of conduct)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: code-of-conduct)) (paragraph (line-content :END:)) (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.)) (heading ** (line-content Project board and contact)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: project-board-and-contact)) (paragraph (line-content :END:)) (paragraph (line-content Find the backlog and Kanban boards for OrgWebAlchemy here:)) (paragraph (line-content https://lucidplan.jointhefreeworld.org/tickets/orgwebalchemy)) (paragraph (line-content You can also contact the maintainer directly:)) (org-list (unordered-item (line-content Josep Bigorra <jjbigorra@gmail.com>)) (unordered-item (line-content Codeberg: https://codeberg.org/jjba23))) (heading ** (line-content Support the project)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: support-the-project)) (paragraph (line-content :END:)) (paragraph (line-content If OrgWebAlchemy saves you time, you can support its development by)) (paragraph (line-content [[https://bmc.link/jjbigorra][buying the maintainer a cup of coffee ☕]]. It is appreciated, but never)) (paragraph (line-content required: using the library, reporting bugs, and telling other people about it)) (paragraph (line-content are already wonderful contributions.)) (separator ---------------------) (heading * (line-content The OrgWebAlchemy mascot)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: mascot)) (paragraph (line-content :END:)) (keyword (keyword-key attr_html) (keyword-value :width 240)) (paragraph (line-content [[file:assets/orgwebalchemy-mascot.png]])) (paragraph (line-content Part GNU, part alchemist, all free software. The mascot is a reminder that)) (paragraph (line-content turning Org into HTML should feel like magic, even when the grammar underneath)) (paragraph (line-content is carefully written and thoroughly tested.)) (separator ---------------------) (heading * (line-content Licensing)) (paragraph (line-content :PROPERTIES:)) (paragraph (line-content :CUSTOM_ID: licensing)) (paragraph (line-content :END:)) (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)) (paragraph (line-content In SPDX terms: ~LGPL-3.0-or-later~ for the code and ~FDL-1.3-or-later~ for the documentation.)))
    [pass] got: 803
    [pass] got: keyword
------------------------------------------
[suite] Figures
  [test] a link to an image elsewhere is no local figure
    [pass] got: (keyword (keyword-key CAPTION) (keyword-value c))
  [test] several captions join in document order
    [pass] got: (figure (figure-caption One two) (figure-image (figure-target file:a.png)))
  [test] a 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] a figure keeps its place among the paragraphs around it
    [pass] got: ((paragraph (line-content before)) (figure (figure-caption c) (figure-image (figure-target file:a.png))) (paragraph (line-content after)))
  [test] an empty name is no name
    [pass] got: (keyword (keyword-key NAME) (keyword-value ))
  [test] a caption alone is still a figure
    [pass] got: (figure (figure-caption A caption) (figure-image (figure-target file:a.png) (figure-description alt)))
  [test] a figure 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 name alone is still a figure
    [pass] got: (figure (figure-name fig:x) (figure-image (figure-target images/x.svg)))
------------------------------------------
[suite] Heading anchors
  [test] a heading link uses the anchor of its title
    [pass] got: #my-section
  [test] titles become URL fragments
    [pass] got: my-first-section
    [pass] got: spaced-punct
    [pass] got: a-b
    [pass] got: 
------------------------------------------
[suite] Inline markup nodes
  [test] =verbatim= parses
    [pass] got: (inline-verbatim-content verbatim)
  [test] +strike+ parses
    [pass] got: (inline-strike-content strike)
  [test] /italic/ parses
    [pass] got: (inline-italic-content italic)
  [test] ~code~ parses
    [pass] got: (inline-code-content code)
  [test] _underline_ parses
    [pass] got: (inline-underline-content underline)
  [test] *bold* parses
    [pass] got: (inline-bold-content bold)
------------------------------------------
[suite] Org link targets
  [test] a Windows drive letter is not a scheme
    [pass] got: file
    [pass] got: C:/tmp/x.txt
  [test] custom ids and heading links become fragments
    [pass] got: internal
    [pass] got: #custom-id
    [pass] got: custom-id
    [pass] got: heading
    [pass] got: #my-section
    [pass] got: My Section
  [test] image links are recognised by extension
    [pass] got: #t
    [pass] got: #t
    [pass] got: #t
    [pass] got: #t
    [pass] got: #f
    [pass] got: #f
  [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: notes.org
    [pass] got: #f
  [test] file links and bare paths are local files
    [pass] got: file
    [pass] got: notes.org
    [pass] got: notes.org
    [pass] got: file
    [pass] got: images/a.png
  [test] mailto and tel links keep their scheme
    [pass] got: mailto
    [pass] got: mailto:me@example.org
    [pass] got: tel
------------------------------------------
[suite] Heading metadata
  [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:))
  [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 plain heading keeps its simple shape
    [pass] got: (heading ** (line-content Title))
------------------------------------------
[suite] Inline constructs
  [test] links with and without a description
    [pass] got: (inline-link-with-desc (link-target-content u) (link-description-content d))
    [pass] got: (inline-link-no-desc (link-target-content u))
  [test] a forced line break before end of line
    [pass] got: (inline-line-break \\)
  [test] footnote reference, definition and inline form
    [pass] got: (inline-footnote-ref (footnote-label 1))
    [pass] got: (inline-footnote-def (footnote-label 1) (footnote-content  note))
    [pass] got: (inline-footnote-inline (footnote-content note))
  [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] escaped markup characters are recognised
    [pass] got: (inline-escape-char +)
    [pass] got: (inline-escape-char *)
  [test] raw urls carry their scheme
    [pass] got: (inline-raw-url (url-scheme https://) x.org)
------------------------------------------
[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 list keeps every kind of item in one run
    [pass] got: (org-list (unordered-item (line-content u)) (ordered-item (line-content o)) (desc-item (desc-key k) (line-content v)))
  [test] 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 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] 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)))))
------------------------------------------
[suite] Document elements recognise their construct
  [test] paragraph parses as paragraph
    [pass] got: paragraph
  [test] comment parses as comment-line
    [pass] got: comment-line
  [test] separator parses as separator
    [pass] got: separator
  [test] ordered list parses as org-list
    [pass] got: org-list
  [test] quote block parses as quote-block
    [pass] got: quote-block
  [test] fixed width parses as fixed-width
    [pass] got: fixed-width
  [test] description list parses as org-list
    [pass] got: org-list
  [test] table parses as table
    [pass] got: table
  [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] src block parses as src-block
    [pass] got: src-block
  [test] mixed sibling list parses as org-list
    [pass] got: org-list
  [test] verse block parses as verse-block
    [pass] got: verse-block
  [test] example block parses as example-block
    [pass] got: example-block
  [test] center block parses as center-block
    [pass] got: center-block
  [test] export html parses as export-html-block
    [pass] got: export-html-block
  [test] unordered list parses as org-list
    [pass] got: org-list
------------------------------------------
[suite] Named elements
  [test] a named element reports its name and itself
    [pass] got: (code (src-block (lang scheme) (src-content (x)
)))
  [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 prefixed cross-reference needs no rewriting
    [pass] got: #tbl:data
    [pass] got: #fig:arch
  [test] a named element still names the image below it a figure
    [pass] got: (figure (figure-name diagram) (figure-image (figure-target file:d.png)))
  [test] a name becomes the id of the element under it
    [pass] got: (named (element-name tbl:data) (table (table-row (table-cell  a ))))
    [pass] got: (named (element-name q) (quote-block (quote-content x
)))
  [test] a name wins over the file path it would otherwise be
    [pass] got: (inline-link-no-desc (link-target-content #notes.org))
  [test] no names in scope leaves every target as written
    [pass] got: (inline-link-no-desc (link-target-content diagram))
  [test] a target that says what it is is left alone
    [pass] got: (inline-link-no-desc (link-target-content https://x/diagram))
    [pass] got: (inline-link-no-desc (link-target-content #diagram))
    [pass] got: (inline-link-no-desc (link-target-content *diagram))
    [pass] got: (inline-link-no-desc (link-target-content diagram::*H))
    [pass] got: (inline-link-no-desc (link-target-content nowhere))
  [test] a bare target naming an element becomes a fragment link
    [pass] got: (inline-link-no-desc (link-target-content #diagram))
    [pass] got: (inline-link-with-desc (link-target-content #diagram) (link-description-content it))
  [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] the names of a document are the names its elements carry
    [pass] got: (tbl:data diagram)
    [pass] got: ()
[info] ran 11 tasks in: 1 seconds, -784448 microseconds

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


[info] shuffled with seed 519171294 (replay with VERITAS_SEED=519171294)
------------------------------------------
[suite] Block Org AST & Document Parsing
  [test] special blocks: quote, example, src, export, separator
    [pass] got: <blockquote>Quoted <strong>text</strong></blockquote>
    [pass] got: <pre>*literal* code
</pre>
    [pass] got: <pre class="scheme">(define x 10)
</pre>
    [pass] got: <div>Raw content</div>
    [pass] got: <hr />
  [test] paragraph transformation
    [pass] got: <p>A simple paragraph with <strong>bold</strong> text.</p>
  [test] headings AST transformation
    [pass] got: <h1 id="level-1">Level 1</h1>
    [pass] got: <h2 id="level-2">Level 2</h2>
    [pass] got: <h3 id="level-3">Level 3</h3>
------------------------------------------
[suite] Full Org-to-HTML Integration
  [test] sample org blocks parsing without errors
    [pass] success running without errors
  [test] sample org text parsing without errors
    [pass] success running without errors
------------------------------------------
[suite] Feature fixture
  [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] the whole fixture renders without error
    [pass] success running without errors
  [test] raw HTML export passes through untouched
    [pass] got: <div class="raw">Raw HTML</div>
------------------------------------------
[suite] Heading metadata
  [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>
  [test] plain headings carry an anchor id
    [pass] got: <h1 id="plain">Plain</h1>
------------------------------------------
[suite] Figures
  [test] a caption names an image with no alt text of its own
    [pass] got: <figure><img src="a.png" alt="A caption" /><figcaption>A caption</figcaption></figure>
  [test] a 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] 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 captioned image renders as a figure with its caption
    [pass] got: <figure><img src="diagram.png" alt="a diagram" /><figcaption>The architecture</figcaption></figure>
------------------------------------------
[suite] Links and images
  [test] a heading link points at the heading anchor
    [pass] got: <h1 id="notes">Notes</h1><p>See <a href="#notes">there</a>.</p>
  [test] 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] file links lose their file: prefix
    [pass] got: <p><a href="notes.org">notes</a></p>
    [pass] got: <p><a href="notes.org">notes.org</a></p>
  [test] image classes can be customised
    [pass] got: <p><img class="rounded" src="photo.png" alt="photo.png" /></p>
------------------------------------------
[suite] Inline Raw URL Transformation
  [test] supports communication protocols
    [pass] got: <a href="tel:+31612345678">tel:+31612345678</a>
    [pass] got: <a href="mailto:jjbigorra@gmail.com">mailto:jjbigorra@gmail.com</a>
    [pass] got: <a href="irc://irc.example.org/#emacs">irc://irc.example.org/#emacs</a>
    [pass] got: <a href="ircs://irc.example.org/#emacs">ircs://irc.example.org/#emacs</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] raw URLs and Org links coexist
    [pass] got: Visit <a href="https://example.org">https://example.org</a> or <a href="https://gnu.org">GNU</a>.
  [test] does not treat unsupported schemes as raw URLs
    [pass] got: custom://example.org
    [pass] got: javascript:alert(1)
  [test] supports all HTTP and file transfer protocols
    [pass] got: <a href="http://example.org">http://example.org</a>
    [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="file:///home/user/document.org">file:///home/user/document.org</a>
  [test] supports remote access and network protocols
    [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>
    [pass] got: <a href="gopher://example.org/1/hello">gopher://example.org/1/hello</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/search?q=org-mode&amp;lang=en">https://example.org/search?q=org-mode&amp;lang=en</a>
    [pass] got: <a href="https://example.org/page#section">https://example.org/page#section</a>
  [test] URLs stop at a newline
    [pass] got: <a href="https://example.org">https://example.org</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] 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>
------------------------------------------
[suite] make-sxml-attrs
  [test] constructs attributes when they are supplied
    [pass] got: ((@ (href /home)))
    [pass] got: ((@ (id main-header) (role banner)))
  [test] returns empty list when no classes or attributes provided
    [pass] got: ()
    [pass] got: ()
    [pass] got: ()
  [test] constructs a class attribute when classes and attributes are supplied
    [pass] got: ((@ (class heading) (href /about)))
    [pass] got: ((@ (class btn btn-primary active) (href /about)))
  [test] constructs a class attribute when classes are supplied
    [pass] got: ((@ (class heading)))
    [pass] got: ((@ (class btn btn-primary active)))
------------------------------------------
[suite] Inline Org Transformation
  [test] basic inline attributes
    [pass] got: <strong>bold</strong>
    [pass] got: <em>italic</em>
    [pass] got: Unsure if Option A/B
    [pass] got: cron job / shepherd timer that does git fetch --all for work and <code>~/hacking</code>
    [pass] got: <u>underlined</u>
    [pass] got: <code>code</code>
    [pass] got: <code>verbatim</code>
    [pass] got: <a href="https://gnu.org">GNU</a>
    [pass] got: <a href="https://gnu.org">https://gnu.org</a>
  [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>
------------------------------------------
[suite] SXML to HTML String Rendering
  [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>
  [test] rendering raw HTML nodes verbatim without escaping
    [pass] got: <span>Raw</span>
------------------------------------------
[suite] Mixed and nested list types
  [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] an ordered list nests under an unordered item
    [pass] got: <ul><li>outer<ol><li>n1</li><li>n2</li></ol></li></ul>
  [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] 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] Org Table Transformation
  [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>
  [test] table error resilience
    [pass] success running without errors
------------------------------------------
[suite] Extended block elements
  [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>
  [test] fixed-width, verse and center blocks
    [pass] got: <pre>fixed
</pre>
    [pass] got: <p>one<br />two</p>
    [pass] got: <div><p>mid</p></div>
------------------------------------------
[suite] Extended inline markup
  [test] forced line break and escaped markup
    [pass] got: line<br />
    [pass] got: +
  [test] strikethrough, superscript and subscript
    [pass] got: <del>struck</del>
    [pass] got: x<sup>2</sup>
    [pass] got: H<sub>2</sub>O
  [test] footnote reference, definition and inline form
    [pass] got: see<sup class="footnote-ref"><a href="#fn-1" id="fnref-1">1</a></sup>
    [pass] got: <span class="footnote-def" id="fn-1"><sup class="footnote-label">1</sup> note</span>
    [pass] got: <sup class="footnote-inline">note</sup>
------------------------------------------
[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: <p class="leading-relaxed mb-4">Paragraph text.</p>
    [pass] got: <pre class="python bg-gray-900 p-4">print(1)
</pre>
    [pass] got: <ul class="list-disc ml-6"><li>item</li></ul>
  [test] customizing inline markup classes
    [pass] got: <strong class="font-bold text-red-500">styled bold</strong>
    [pass] got: <a class="underline text-blue-600" href="/path">link</a>
------------------------------------------
[suite] Named elements
  [test] a named table carries its name as its id
    [pass] got: <table id="tbl:data"><tbody><tr><td> a </td></tr></tbody></table>
  [test] the id joins the classes an element already has
    [pass] got: <pre id="code" class="scheme">(x)
</pre>
  [test] a bare link to a name points at the element as well
    [pass] got: <a href="#diagram">diagram</a>
    [pass] got: <figure id="diagram">
  [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>
------------------------------------------
[suite] Empty values
  [test] an empty keyword value still renders its line
    [pass] got: <div>KEY: </div>
  [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: <blockquote />
    [pass] got: <pre></pre>
  [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 line that is only its closing bar holds one empty cell
    [pass] got: <table><tbody><tr><td /></tr></tbody></table>
    [pass] got: <table><tbody><tr><td> </td></tr></tbody></table>
------------------------------------------
[suite] List transformations
  [test] unordered and ordered lists transformation
    [pass] got: <ul><li>Item 1</li><li>Item 2</li></ul>
    [pass] got: <ol><li>First</li><li>Second</li></ol>
  [test] description list transformation
    [pass] got: <dl><dt>Term</dt><dd>Definition text</dd></dl>
  [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] nested unordered and nested ordered lists transformation
    [pass] got: <ul><li>Item 1<ul><li>Item 1.1</li><li>Item 1.2</li></ul></li></ul>
    [pass] got: <ol><li>Item 1<ol><li>Item 1.1</li><li>Item 1.2</li></ol></li></ol>
[info] ran 18 tasks in: 0 seconds, 151194 microseconds

finished running tests
;;; note: source file ./test/veritas/unit/markdown-spec.scm
;;;       newer than compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/orgwebalchemy/trunk/test/veritas/unit/markdown-spec.scm.go
;;; note: auto-compilation is enabled, set GUILE_AUTO_COMPILE=0
;;;       or pass the --no-auto-compile argument to disable.
;;; compiling ./test/veritas/unit/markdown-spec.scm
;;; note: source file ./src/orgwebalchemy/peg.scm
;;;       newer than compiled /run/current-system/profile/lib/guile/3.0/site-ccache/orgwebalchemy/peg.go
;;; note: source file ./src/orgwebalchemy/peg.scm
;;;       newer than compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/peg.scm.go
;;; compiling ./src/orgwebalchemy/peg.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/peg.scm.go
;;; note: source file ./src/orgwebalchemy/markdown.scm
;;;       newer than compiled /run/current-system/profile/lib/guile/3.0/site-ccache/orgwebalchemy/markdown.go
;;; note: source file ./src/orgwebalchemy/markdown.scm
;;;       newer than compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/markdown.scm.go
;;; compiling ./src/orgwebalchemy/markdown.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/markdown.scm.go
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/orgwebalchemy/trunk/test/veritas/unit/markdown-spec.scm.go
;;; note: source file ./test/veritas/unit/peg-spec.scm
;;;       newer than compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/orgwebalchemy/trunk/test/veritas/unit/peg-spec.scm.go
;;; compiling ./test/veritas/unit/peg-spec.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/orgwebalchemy/trunk/test/veritas/unit/peg-spec.scm.go
;;; note: source file ./test/veritas/unit/html-spec.scm
;;;       newer than compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/orgwebalchemy/trunk/test/veritas/unit/html-spec.scm.go
;;; compiling ./test/veritas/unit/html-spec.scm
;;; note: source file ./src/orgwebalchemy/html.scm
;;;       newer than compiled /run/current-system/profile/lib/guile/3.0/site-ccache/orgwebalchemy/html.go
;;; note: source file ./src/orgwebalchemy/html.scm
;;;       newer than compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/html.scm.go
;;; compiling ./src/orgwebalchemy/html.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/html.scm.go
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/orgwebalchemy/trunk/test/veritas/unit/html-spec.scm.go


--------------------------------------------------------------

Maak 0.14.0: 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.

Executing: guix time-machine --channels=channels.scm -- shell -m manifest.scm -- guile -L ./src -L ./test -c '((@ (veritas runner) run-tests))'
;;; note: source file /etc/byggsteg/job-clone/orgwebalchemy/trunk/maak.scm
;;;       newer than compiled /root/.cache/guile/ccache/3.0-LE-8-4.6/etc/byggsteg/job-clone/orgwebalchemy/trunk/maak.scm.go
;;; note: auto-compilation is enabled, set GUILE_AUTO_COMPILE=0
;;;       or pass the --no-auto-compile argument to disable.
;;; compiling /etc/byggsteg/job-clone/orgwebalchemy/trunk/maak.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.6/etc/byggsteg/job-clone/orgwebalchemy/trunk/maak.scm.go

→ running command: cd /etc/byggsteg/job-clone/orgwebalchemy/trunk && sudo maak deploy



--------------------------------------------------------------

Maak 0.14.0: 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.
;;; note: source file /etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/html.scm
;;;       newer than compiled /root/.cache/guile/ccache/3.0-LE-8-4.6/etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/html.scm.go
;;; note: auto-compilation is enabled, set GUILE_AUTO_COMPILE=0
;;;       or pass the --no-auto-compile argument to disable.
;;; compiling /etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/html.scm
;;; note: source file /etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/peg.scm
;;;       newer than compiled /root/.cache/guile/ccache/3.0-LE-8-4.6/etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/peg.scm.go
;;; compiling /etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/peg.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.6/etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/peg.scm.go
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.6/etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/html.scm.go
;;; note: source file /etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/markdown.scm
;;;       newer than compiled /root/.cache/guile/ccache/3.0-LE-8-4.6/etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/markdown.scm.go
;;; compiling /etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/markdown.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.6/etc/byggsteg/job-clone/orgwebalchemy/trunk/src/orgwebalchemy/markdown.scm.go
API documentation saved to doc/api
index.texi: warning: must specify a title with a title command or @top

Executing: guix time-machine --channels=channels.scm -- shell -m manifest.scm -- documenta api ./src/orgwebalchemy

Executing: guix time-machine --channels=channels.scm -- shell -m manifest.scm -- texi2any -v --html --css-ref=https://jointhefreeworld.org/static/css/jointhefreeworld.css ./doc/api/index.texi
renamed './index' -> './doc/api-dist'
removed '/srv/http/jointhefreeworld.org/api-docs/orgwebalchemy/API.html'
removed '/srv/http/jointhefreeworld.org/api-docs/orgwebalchemy/orgwebalchemy_005fhtml.html'
removed '/srv/http/jointhefreeworld.org/api-docs/orgwebalchemy/orgwebalchemy_005fmarkdown.html'
removed '/srv/http/jointhefreeworld.org/api-docs/orgwebalchemy/orgwebalchemy_005fpeg.html'
removed directory '/srv/http/jointhefreeworld.org/api-docs/orgwebalchemy'
mkdir: created directory '/srv/http/jointhefreeworld.org/api-docs/orgwebalchemy'
renamed 'doc/api-dist/API.html' -> '/srv/http/jointhefreeworld.org/api-docs/orgwebalchemy/API.html'
renamed 'doc/api-dist/orgwebalchemy_005fhtml.html' -> '/srv/http/jointhefreeworld.org/api-docs/orgwebalchemy/orgwebalchemy_005fhtml.html'
renamed 'doc/api-dist/orgwebalchemy_005fmarkdown.html' -> '/srv/http/jointhefreeworld.org/api-docs/orgwebalchemy/orgwebalchemy_005fmarkdown.html'
renamed 'doc/api-dist/orgwebalchemy_005fpeg.html' -> '/srv/http/jointhefreeworld.org/api-docs/orgwebalchemy/orgwebalchemy_005fpeg.html'

Executing: mv -v doc/api-dist/* /srv/http/jointhefreeworld.org/api-docs/orgwebalchemy
removed 'doc/api/index.texi'
removed directory 'doc/api'
removed directory 'doc/api-dist'
removed directory 'doc'
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)))))