clone

a personal forge on plain git: static pages, stacked-diff reviews, signed local ci, repo sites and releases

Name the line a diagram's syntax error is on

cat committed d08b3da
Pele 1.6.0 locates a syntax error in the text its parsers see, which is
not quite the diagram's: front matter and %% comment lines are taken out
first and a newline is added at the end, as Mermaid does. So an error
after front matter or a comment was reported lines early, and one at the
end of a diagram a line past its last (a fence's text ends in a newline,
so two past):

    render('flowchart LR\n  A-->')   // PeleError: line 3, snippet ""

The fallback's note now names the source's line. Pele's snippet quotes
the line it means, which is the source's line as written, at or after
the one Pele counted, so diagrams.ts looks for it there; an error at the
end is on the last line with anything on it. If neither works out (an
entity in the line changes its text), Pele's number stays.

This is a workaround, in one function (errorLine), to drop when Pele
reports lines in the source; scenario diagram-error-line holds the
expected lines either way. Not yet reported upstream.
commit
d08b3dad5c639d304c379802410a9e62fbcaefb8
parent
e7033c1310697bee8f81f25a583238f4be4bc6d2
author
cat <admin@dev.site>
ci
✓ passed in 76s, signed by cat
kind
fix risk score 1.4 of 3, Jev's estimate
review
#15 Draw mermaid diagrams at build time, with Pele
ci attestation
forge-attestation 1
type ci
commit d08b3dad5c639d304c379802410a9e62fbcaefb8
tree 031e79dbdbe33a14bdca2b2b52c042960d16253c
script .forge/ci aa9f4eac26627c6559e68cd4b5e5e76afb8e65e8
result pass
exit 0
started 2026-10-19T03:18:28Z
duration 76s
runner (not shown here; refs/notes/ci has it)
system Darwin 25.5.0 arm64
env host
log sha256:ec8a96a54be6bf1c15e03e916ece926ec29a5e7e3e4384379d17be282d0c2acc 263 lines
|pushed wip/i18n
|1123f15 running .forge/ci: greet.sh: say what it is
|  | sh: /bin/sh
|  | ok   greet
|1123f15 pass in 0s, signed
|pushed main
|remote: review #4: /dami/greet/reviews/4/
|pushed wip/i18n
|94c713e running .forge/ci: wave at the world
|  | sh: /bin/sh
|  | FAIL greet : got 'hello, world o/', want 'hello, world'
|94c713e fail in 0s, signed
|forge: 1 commit(s) did not pass
|remote: review #6: /dami/greet/reviews/6/
|pushed ana/wave
|6187ec9 running .forge/ci: update the test for the wave
|  | sh: /bin/sh
|  | ok   greet
|6187ec9 pass in 0s, signed
|remote: review #6: /dami/greet/reviews/6/
|pushed ana/wave
|warning: You appear to have cloned an empty repository.
|site home: deployed 5dff6eb -> /sites/ana/dotfiles/home/
|created ana/notes
|remote: review #1: /ana/notes/reviews/1/
|pushed ana/todo
|landing #1 A todo list: ana/todo -> main
|cf07218 running .forge/ci: web, server and Python versions of greet
|  | sh: /bin/sh
|  | ok   greet
|cf07218 pass in 0s, signed
|pushed main
|e2346b9 running .forge/ci: messages: notes for the later regions
|  | sh: /bin/sh
|  | ok   greet
|e2346b9 pass in 0s, signed
|remote: review #7: /dami/greet/reviews/7/
|pushed dami/regions
|created dami/forge
|warning: You appear to have cloned an empty repository.
|site guide: mkdir -p guide/fonts && cp site/assets/fonts/*.woff2 guide/fonts/
|site guide: deployed 900f477 -> /sites/dami/forge/guide/
|(node:31592) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time
|(Use `node --trace-warnings ...` to show where the warning was created)
|built ana/dotfiles in 0s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.7CaqyA/wt/demo/root/.forge/www
|(node:31721) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time
|(Use `node --trace-warnings ...` to show where the warning was created)
|built ana/notes in 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.7CaqyA/wt/demo/root/.forge/www
|(node:31861) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time
|(Use `node --trace-warnings ...` to show where the warning was created)
|built dami/forge in 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.7CaqyA/wt/demo/root/.forge/www
|(node:32000) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time
|(Use `node --trace-warnings ...` to show where the warning was created)
|built dami/greet in 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.7CaqyA/wt/demo/root/.forge/www
|(node:32989) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time
|(Use `node --trace-warnings ...` to show where the warning was created)
|built - in 0s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.7CaqyA/wt/demo/root/.forge/www
|234 of 234 pages render the same without 11ty
|guide: 8 files, links ok
|ok

Signature: good, cat

3 files changed, +56 -2
Mlib/diagrams.ts24++++++++++++++++++++++-
Mtest/scenarios/diagram-broken.sh2+-
Atest/scenarios/diagram-error-line.sh32++++++++++++++++++++++++++++++
M lib/diagrams.ts +23 -1
@@ -74,6 +74,25 @@ const SVG: sanitize.IOptions = {
7474 parser: { lowerCaseAttributeNames: false },7575 };7676 77+// Which line of the source a syntax error is on. Pele 1.6.0 counts lines in78+// the text it parses, which is the source less its front matter and %%79+// comment lines, plus a newline, as Mermaid does: an error after either is80+// reported lines early, and one at the end a line past the last. The line81+// its snippet quotes is the source's, so find it, at or after the line82+// Pele counted; at the end, it is the last line with anything on it. Drop83+// this when Pele reports lines in the source (upstream: see the commit that84+// added it).85+function errorLine(src: string, e: PeleError): number {86+ const lines = src.split(/\r\n?|\n/);87+ const quoted = e.snippet.split('\n')[0];88+ if (quoted.trim() === '') {89+ const last = lines.findLastIndex((l) => l.trim() !== '') + 1;90+ return last > 0 && e.line > last ? last : e.line;91+ }92+ const at = lines.findIndex((l, i) => i >= e.line - 1 && l === quoted);93+ return at === -1 ? e.line : at + 1;94+}95+7796 const escape = (s: string): string => s.replace(/[&<>"]/g, (c) => `&${{ '&': 'amp', '<': 'lt', '>': 'gt', '"': 'quot' }[c]};`);7897 7998 // The width a phone gives a diagram, about. One drawn wider than this gets
@@ -104,7 +123,10 @@ export function diagram(src: string, href: (url: string) => string = (u) => u):
104123 } catch (e) {105124 // Pele's message names the line, and can quote it: shown as text.106125 let why = 'Pele failed on it';107- if (e instanceof PeleError) why = e.message.split('\n')[0].replace(/[.:]$/, '').slice(0, 160);126+ if (e instanceof PeleError) {127+ why = e.message.split('\n')[0].replace(/[.:]$/, '').slice(0, 160);128+ if (e.code === 'syntax' && e.line) why = why.replace(/ on line \d+/, ` on line ${errorLine(src, e)}`);129+ }108130 return `<figure class="diagram-source"><pre><code class="language-mermaid">${escape(src)}</code></pre><figcaption>Not drawn: ${escape(why)}.</figcaption></figure>\n`;109131 }110132 const clean = sanitize(svg, {
M test/scenarios/diagram-broken.sh +1 -1
A test/scenarios/diagram-error-line.sh +32 -0