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