commit 07d70951be90774505295492b981fef2d551b20c
parent 16856010180348b21200b4ec0e3a7aca931de133
Author: David Eisinger <[email protected]>
Date: Tue, 8 Sep 2026 23:27:28 -0400
Remove Gemini link numbers
Diffstat:
3 files changed, 142 insertions(+), 16 deletions(-)
diff --git a/bin/check-gemtext b/bin/check-gemtext
@@ -0,0 +1,78 @@
+#!/usr/bin/env python3
+"""Exercise the Gemini converter with Hugo, without the dither server."""
+
+from pathlib import Path
+import shutil
+import subprocess
+import tempfile
+
+
+repo = Path(__file__).resolve().parent.parent
+with tempfile.TemporaryDirectory(prefix="gemtext-check-") as directory:
+ root = Path(directory)
+ (root / "layouts/partials").mkdir(parents=True)
+ (root / "content").mkdir()
+ for name in ("gemtext.gmi", "gemtext-links.gmi"):
+ shutil.copy(repo / "themes/v2/layouts/partials" / name, root / "layouts/partials" / name)
+ (root / "hugo.toml").write_text('baseURL = "https://example.org/"\ndisableKinds = ["home", "taxonomy", "term", "RSS", "sitemap"]\n')
+ (root / "layouts/single.html").write_text('{{ partial "gemtext.gmi" (dict "content" .RawContent "page" .) }}')
+ (root / "content/check.md").write_text('''---
+title: Check
+references:
+- url: https://example.org/guide
+ title: A useful guide
+---
+[First $1][1] and [again][1] start a paragraph
+that continues with [second][2].
+
+Another paragraph uses [first again][1] ([via][3]).
+
+* [First $1][1] ([via][3])
+
+ > Quoted text.
+
+* [First $1][1]
+
+* [First $1][1] with commentary
+* Read [this article][4]
+
+> A quote containing [second][2].
+
+Read [inline](https://example.org/inline) too.
+
+```markdown
+Literal [first][1].
+
+[99]: https://example.org/code-only
+```
+
+An [unresolved][98] reference.
+
+[1]: https://example.org/first
+[2]: https://example.org/second
+[3]: https://source.org/post
+[4]: https://example.org/guide
+
+Last paragraph with [_a book_][2].''')
+ subprocess.run(["hugo", "build", "--source", str(root)], check=True)
+ result = (root / "public/check/index.html").read_text()
+ expected = [
+ "First $1 and again start a paragraph\nthat continues with second.\n\n"
+ "=> https://example.org/first First $1\n=> https://example.org/second second",
+ "Another paragraph uses first again.\n\n=> https://example.org/first first again\n"
+ "=> https://source.org/post via source.org",
+ "=> https://example.org/first First $1\n=> https://source.org/post via source.org\n\n> Quoted text.",
+ "* First $1 with commentary\n* Read this article\n\n"
+ "=> https://example.org/first First $1\n=> https://example.org/guide A useful guide",
+ "> A quote containing second.\n\n=> https://example.org/second second",
+ "Read inline too.\n\n=> https://example.org/inline inline",
+ "```markdown\nLiteral [first][1].\n\n[99]: https://example.org/code-only\n```",
+ "An [unresolved][98] reference.",
+ "Last paragraph with _a book_.\n\n=> https://example.org/second a book",
+ ]
+ for fragment in expected:
+ assert fragment in result, f"Missing expected output:\n{fragment}\n\nActual:\n{result}"
+ assert result.count("=> https://example.org/first ") == 5, result
+ assert "[1]:" not in result, result
+ assert "(via)" not in result, result
+ print("Gemtext conversion checks passed.")
diff --git a/themes/v2/layouts/partials/gemtext-links.gmi b/themes/v2/layouts/partials/gemtext-links.gmi
@@ -0,0 +1,60 @@
+{{- $references := .references -}}
+{{- $titles := dict -}}
+{{- range .page.Params.references -}}
+ {{- $titles = merge $titles (dict .url .title) -}}
+{{- end -}}
+{{- $linkPattern := `\[([^\]]+)\](?:\[(\d+)\]|\(([^)\n]+)\))` -}}
+{{- $output := slice -}}
+{{- $links := slice -}}
+{{- $seen := dict -}}
+{{- $preformatted := false -}}
+{{- /* The final blank line flushes links from a paragraph at EOF. */ -}}
+{{- range split (printf "%s\n" .content) "\n" -}}
+ {{- $line := . -}}
+ {{- $fence := hasPrefix $line "```" -}}
+ {{- if and (not $preformatted) (findRE `^\[\d+\]:[ \t]+` $line) -}}
+ {{- $line = "" -}}
+ {{- end -}}
+ {{- if and (not $preformatted) (or (eq (strings.TrimSpace $line) "") $fence (hasPrefix $line "=>")) -}}
+ {{- with $links -}}
+ {{- $output = $output | append "" -}}
+ {{- range . -}}
+ {{- $output = $output | append . -}}
+ {{- end -}}
+ {{- end -}}
+ {{- $links = slice -}}
+ {{- $seen = dict -}}
+ {{- end -}}
+ {{- if and (not $preformatted) (not $fence) (not (hasPrefix $line "=>")) -}}
+ {{- range findRE $linkPattern $line -}}
+ {{- $text := replaceRE $linkPattern `$1` . -}}
+ {{- $refNum := replaceRE $linkPattern `$2` . -}}
+ {{- $url := replaceRE $linkPattern `$3` . -}}
+ {{- with $refNum -}}
+ {{- $url = index $references . -}}
+ {{- end -}}
+ {{- with $url -}}
+ {{- $label := (index $titles . | default $text) | replaceRE `^[_*]+|[_*]+$` "" -}}
+ {{- if eq $text "via" -}}
+ {{- $label = printf "via %s" ((urls.Parse .).Host | default .) -}}
+ {{- end -}}
+ {{- if not (index $seen .) -}}
+ {{- $links = $links | append (printf "=> %s %s" . $label) -}}
+ {{- $seen = merge $seen (dict . true) -}}
+ {{- end -}}
+ {{- end -}}
+ {{- if $url -}}
+ {{- if eq $text "via" -}}
+ {{- /* Attribution lives on its link line, not as a dangling '(via)'. */ -}}
+ {{- $line = replace $line (printf " (%s)" .) "" -}}
+ {{- end -}}
+ {{- $line = replace $line . $text -}}
+ {{- end -}}
+ {{- end -}}
+ {{- end -}}
+ {{- $output = $output | append $line -}}
+ {{- if $fence -}}
+ {{- $preformatted = not $preformatted -}}
+ {{- end -}}
+{{- end -}}
+{{- return (delimit $output "\n") -}}
diff --git a/themes/v2/layouts/partials/gemtext.gmi b/themes/v2/layouts/partials/gemtext.gmi
@@ -3,11 +3,10 @@
{{- /* Promote standalone list links, with an optional attribution link. */ -}}
{{- $references := dict -}}
-{{- $refPattern := `(?m)^\[(\d+)\]: (.+)$` -}}
+{{- $refPattern := `(?m)^\[(\d+)\]:[ \t]+(.+)$` -}}
{{- range findRE $refPattern $content -}}
- {{- $references = merge $references (dict (replaceRE $refPattern `$1` .) (replaceRE $refPattern `$2` .)) -}}
+ {{- $references = merge $references (dict (replaceRE $refPattern `$1` .) (replaceRE $refPattern `$2` . | strings.TrimSpace)) -}}
{{- end -}}
-{{- $promoted := dict -}}
{{- $listPattern := `(?m)^[*-] \[([^\]]+)\]\[(\d+)\](?:[ ]+\(\[via\]\[(\d+)\]\))?[ ]*$` -}}
{{- $lines := slice -}}
{{- range split $content "\n" -}}
@@ -20,11 +19,9 @@
{{- $viaURL := index $references $viaNum -}}
{{- if and $url (or (eq $viaNum "") $viaURL) -}}
{{- $newLine := printf "=> %s %s" $url $text -}}
- {{- $promoted = merge $promoted (dict $refNum true) -}}
{{- with $viaURL -}}
{{- $label := (urls.Parse .).Host | default . -}}
{{- $newLine = printf "%s\n=> %s via %s" $newLine . $label -}}
- {{- $promoted = merge $promoted (dict $viaNum true) -}}
{{- end -}}
{{- $line = $newLine -}}
{{- end -}}
@@ -34,17 +31,8 @@
{{- $content = delimit $lines "\n" -}}
-{{- /* Keep definitions that are still referenced elsewhere in the prose. */ -}}
-{{- range $refNum, $_ := $promoted -}}
- {{- if not (findRE (printf `\[[^\]]+\]\[%s\]` $refNum) $content) -}}
- {{- $content = replaceRE (printf `(?m)^\[%s\]: .+$` $refNum) "" $content -}}
- {{- end -}}
-{{- end -}}
-
-{{- /* Convert links */ -}}
-{{- $content = $content | replaceRE `([^\w])\[([^\]]+)\]\[(\d+)\]` "$1$2[$3]" -}}
-{{- $content = $content | replaceRE `(?m)^\[(\d+)\]: (.*)$` "=> $2 [$1] $2" -}}
-{{- $content = $content | replaceRE `\[([^\]]+)\]\([^\)]+\)` "$1" -}}
+{{- /* Place descriptive links after the paragraph that uses them. */ -}}
+{{- $content = partial "gemtext-links.gmi" (dict "content" $content "references" $references "page" $page) -}}
{{- /* Replace audio tags */ -}}
{{- $audioRE := `<audio controls src="(.*/([^"]+))"></audio>` -}}