diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl
index a3322d9..dcf8c4b 100644
--- a/.beads/issues.jsonl
+++ b/.beads/issues.jsonl
@@ -120,6 +120,6 @@
{"id":"ifcurl-vam","title":"Phase 6: Bonsai offer ifc:// URL when saving linked model reference","description":"When saving an IFCDOCUMENTREFERENCE in Bonsai, offer the option to write the location as an ifc:// URL rather than a relative path. When a relative path would traverse above the repository root, prompt the user to convert it to an ifc:// URL. Requires Phase 4 federation (ifcurl-bqt).","status":"deferred","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-23T05:49:16Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:15:13Z","defer_until":"2026-12-01T00:00:00Z","dependencies":[{"issue_id":"ifcurl-vam","depends_on_id":"ifcurl-bqt","type":"blocks","created_at":"2026-04-23T06:49:23Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-9k2","title":"Phase 5: IFC Viewer integration (equivalent of Phase 4)","description":"Add ifc:// OS protocol handler and 'Copy view URL' button to the IFC Viewer desktop application, equivalent to the Bonsai Phase 4 integration. Depends on Phase 4 being complete as a reference implementation.","status":"deferred","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-23T05:49:08Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:15:12Z","defer_until":"2026-12-01T00:00:00Z","dependencies":[{"issue_id":"ifcurl-9k2","depends_on_id":"ifcurl-0oj","type":"blocks","created_at":"2026-04-23T06:49:14Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-prc","title":"Phase 3c: 3D diff view for IFC PR merges","description":"After a PR merge completes, surface a 'View merge in 3D' button using the Phase 3b viewer. Use @thatopen/components highlighter to colour added, removed, and modified elements differently, driven by comparing the merged IFC against either parent. Requires Phase 3b viewer (ifcurl-i1s) to be working.","status":"closed","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-23T05:48:30Z","created_by":"Bruno Postle","updated_at":"2026-04-26T17:15:07Z","closed_at":"2026-04-26T17:15:07Z","close_reason":"Rendered diff already implemented in footer.tmpl Case 3; static PNG via /render_diff endpoint, not interactive 3D","dependencies":[{"issue_id":"ifcurl-prc","depends_on_id":"ifcurl-i1s","type":"blocks","created_at":"2026-04-23T06:48:36Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
-{"_type":"memory","key":"ifcurl-service-names-are-ifcurl-api-service-and","value":"ifcurl service names are ifcurl-api.service and ifcurl-render.service (not ifcurl.service). Install path for Python package: /opt/ifcurl/lib/python3.14/site-packages/ifcurl/. Python binary: /opt/ifcurl/bin/python3."}
{"_type":"memory","key":"forgejo-build-and-deploy-procedure-go-build-c","value":"Forgejo build and deploy procedure: go build -C /home/bruno/src/forgejo -tags 'sqlite sqlite_unlock_notify' -ldflags \"-X 'forgejo.org/modules/setting.StaticRootPath=/usr/share/forgejo'\" -o /home/bruno/src/forgejo/forgejo . — then sudo cp forgejo /usr/bin/forgejo. StaticRootPath=/usr/share/forgejo. CustomPath=/var/lib/forgejo/custom. Custom templates go to /var/lib/forgejo/custom/templates/custom/ (e.g. footer.tmpl → /var/lib/forgejo/custom/templates/custom/footer.tmpl). Custom static files go to /var/lib/forgejo/custom/public/assets/ (NOT /etc/forgejo/public/assets/) and are served at /assets/*."}
+{"_type":"memory","key":"ifcurl-service-names-are-ifcurl-api-service-and","value":"ifcurl service names are ifcurl-api.service and ifcurl-render.service (not ifcurl.service). Install path for Python package: /opt/ifcurl/lib/python3.14/site-packages/ifcurl/. Python binary: /opt/ifcurl/bin/python3."}
{"_type":"memory","key":"torawurl-in-viewer-url-js-must-support-github","value":"toRawUrl in viewer-url.js must support GitHub, GitLab, Forgejo/Gitea (codeberg.org, self-hosted), and localhost. Tests in forgejo/tests/viewer-url.test.js cover all four. The localhost check (http vs https) is based on host.startsWith('localhost'), so subdomain.localhost gets https."}
diff --git a/README.md b/README.md
index 212def5..6b53565 100644
--- a/README.md
+++ b/README.md
@@ -59,17 +59,12 @@ A self-contained WebGL IFC viewer (`viewer.html`) served as a Forgejo asset at `
## Forgejo integration
-A set of JS assets and an optional Go patch for Forgejo. Most features work with
-asset deployment only — no Forgejo rebuild required. See
-[`forgejo/README.md`](forgejo/README.md) for full details, or
-[`forgejo/SETUP_LOCAL.md`](forgejo/SETUP_LOCAL.md) for a self-contained local
-setup on Windows using Docker.
+A set of JS assets for Forgejo — all features work with asset deployment only,
+no Forgejo rebuild required. See [`forgejo/README.md`](forgejo/README.md) for
+full details, or [`forgejo/SETUP_LOCAL.md`](forgejo/SETUP_LOCAL.md) for a
+self-contained local setup on Windows using Docker.
-### Quick setup (no rebuild)
-
-Deploy the assets and the preview service. All features work except bare
-`ifc://...` text in markdown (use `[title](ifc://...)` link syntax instead, which
-the viewer's Issue button produces automatically).
+### Quick setup
```bash
# Deploy assets
@@ -85,33 +80,15 @@ sudo cp forgejo/server-config/ifcurl-preview.service /etc/systemd/system/
sudo systemctl enable --now ifcurl-preview
```
-### Optional: Go patch for bare URL rendering
+### Bare `ifc://` URLs in markdown — underscore caveat
-To also render bare `ifc://...` text in markdown (without `[title](...)` syntax),
-apply the Go patch and rebuild Forgejo:
-
-```bash
-cp forgejo/modules/markup/markdown/ifc_url{,_test}.go /path/to/forgejo/modules/markup/markdown/
-cd /path/to/forgejo && git apply /path/to/ifcurl/forgejo/go.patch
-go build -tags 'sqlite sqlite_unlock_notify' \
- -ldflags "-X 'forgejo.org/modules/setting.StaticRootPath=/usr/share/forgejo'" \
- -o forgejo . && sudo cp forgejo /usr/bin/forgejo
-```
-
-Add to `/etc/forgejo/conf/app.ini`:
-
-```ini
-[ifcurl]
-PREVIEW_SERVICE_URL = http://localhost:8000
-; Optional: Forgejo API token for a read-only machine user.
-; The preview service will use this token to fetch IFC files from private
-; repositories on this Forgejo instance. The token is appended as a query
-; parameter in the
URL generated by the markdown extension, so it
-; is visible in page source. Only set this on trusted private instances.
-; SERVICE_TOKEN =
-```
-
-**Private repositories:** create a machine user with read access and set `SERVICE_TOKEN` to its API token. Alternatively, configure credentials in `~/.config/ifcurl/tokens.json` under the service user account.
+Bare `ifc://` URLs in markdown (pasted without `[title](...)` syntax) are
+linkified client-side by `ifcurl.js`. This works for most paths, but if the
+file path or query parameters contain matched underscores (e.g. `_model_v2.ifc`),
+Goldmark renders them as `` tags and splits the URL — the bare form will
+fail silently. Use the explicit link form `[label](ifc://...)` for any URL where
+the path or parameters contain paired underscores. The viewer's **Issue** button
+always generates this safe form automatically.
### Authentication in the viewer
@@ -303,10 +280,6 @@ python -m pytest tests/
# JavaScript tests (Node 18+)
node --test tests/test_viewer_url.mjs
-
-# Go tests (requires Forgejo source tree with patch applied)
-cd /path/to/forgejo
-go test ./modules/markup/markdown/ -run TestIfcURL -v
```
### Roadmap
@@ -315,7 +288,7 @@ go test ./modules/markup/markdown/ -run TestIfcURL -v
|---|---|---|
| 1 — Python core | ✓ done | URL parsing, git fetch, render |
| 2 — Preview service | ✓ done | HTTP service with caching |
-| 3 — Forgejo integration | ✓ done | Go patch, viewer, markdown extension |
+| 3 — Forgejo integration | ✓ done | Viewer, JS markdown extension |
| 4 — Bonsai integration | planned | Protocol handler + "Copy view URL" |
| 5 — IFC Viewer integration | planned | Plugin for the open-source IFC Viewer |
| 6 — Federation | planned | `IFCDOCUMENTREFERENCE` cross-repo links |
diff --git a/forgejo/README.md b/forgejo/README.md
index ae0f16d..5a6e8b9 100644
--- a/forgejo/README.md
+++ b/forgejo/README.md
@@ -11,18 +11,18 @@ split into two tiers by what infrastructure they require.
| Browser IFC viewer | `viewer.html` static asset | No |
| PR diff 3D render | `ifcurl.js` + ifcurl service + **Nginx proxy** | No |
| `[title](ifc://...)` links in markdown → inline preview | `ifcurl.js` + ifcurl service + **Nginx proxy** | No |
-| Bare `ifc://...` text in markdown → inline preview | Go patch + `PREVIEW_SERVICE_URL` in app.ini | **Yes** |
+| Bare `ifc://...` text in markdown → inline preview | `ifcurl.js` (JS text-node linkification) | No |
-All features except bare-URL markdown rendering work with static asset deployment
-only — no Forgejo rebuild required. `ifcurl.js` detects `` links
-produced by Goldmark's standard link parser and replaces them with preview figures
-at page load.
+All features work with static asset deployment only — no Forgejo rebuild required.
+`ifcurl.js` handles both `` links produced by Goldmark's
+standard link parser and bare `ifc://...` text nodes, replacing both with preview
+figures at page load.
-The Go patch is needed **only** if you want bare `ifc://...` text in markdown
-(without `[title](...)` syntax) to render as a preview. The Issue button in the
-viewer emits `[title](ifc://...)` link syntax, so normal issue/PR workflows work
-without the patch. Minimising the patch surface is deliberate — every line of Go
-code must be re-verified against each Forgejo release.
+**Bare URL caveat:** bare `ifc://` URLs where the path or query parameters
+contain matched underscores (e.g. `_model_v2.ifc`) may be split by Goldmark's
+emphasis parser before `ifcurl.js` can linkify them. Use the explicit
+`[label](ifc://...)` form in those cases — the viewer's **Issue** button always
+generates this safe form.
The PR diff feature requires a **reverse proxy** (e.g. Nginx) to expose the
ifcurl service at the same origin as Forgejo. This is because the diff image is
@@ -37,10 +37,6 @@ deployments already sit behind Nginx for TLS termination; adding two
```
forgejo/
- go.patch ← diff against Forgejo source (apply once)
- modules/markup/markdown/
- ifc_url.go ← new Go source file (copy into source tree)
- ifc_url_test.go ← Go tests (copy into source tree)
custom/public/assets/
viewer.html ← browser IFC viewer (no rebuild needed)
viewer-url.js ← ifc:// URL parse/build/resolve logic (ES module)
@@ -67,64 +63,9 @@ For a self-contained local deployment of Forgejo + ifcurl on Windows using Docke
- ifcurl API service + render service running (see [Running the preview service](#running-the-preview-service))
- Nginx (or equivalent) in front of Forgejo if PR diff images are wanted
-- Forgejo source tree cloned and building cleanly (`go` 1.21+) — **only if**
- markdown inline preview is wanted
-The patch was written against the `v13.0` branch of Forgejo. Check which
-Forgejo commit the patch was authored against with:
-
-```bash
-cd /path/to/forgejo
-git log --oneline modules/markup/markdown/markdown.go | head -3
-```
-
----
-
-## Applying the patch
-
-### 1. Copy new Go source files
-
-```bash
-cp forgejo/modules/markup/markdown/ifc_url.go /path/to/forgejo/modules/markup/markdown/
-cp forgejo/modules/markup/markdown/ifc_url_test.go /path/to/forgejo/modules/markup/markdown/
-```
-
-### 2. Apply the diff to existing files
-
-```bash
-cd /path/to/forgejo
-git apply /path/to/ifcurl/forgejo/go.patch
-```
-
-This adds one line to `modules/markup/markdown/markdown.go` and six lines to
-`modules/setting/markup.go`. If the patch does not apply cleanly (e.g. after
-a Forgejo upstream upgrade), the changes are small enough to apply by hand —
-see `go.patch` for the exact hunks.
-
-### 3. Run the Go tests
-
-```bash
-cd /path/to/forgejo
-go test ./modules/markup/markdown/ -run TestIfcURL -v
-```
-
-All seven tests should pass before proceeding.
-
-### 4. Build and deploy Forgejo
-
-```bash
-cd /path/to/forgejo
-go build \
- -tags 'sqlite sqlite_unlock_notify' \
- -ldflags "-X 'forgejo.org/modules/setting.StaticRootPath=/usr/share/forgejo'" \
- -o forgejo .
-
-sudo cp forgejo /usr/bin/forgejo
-sudo systemctl restart forgejo
-```
-
-Adjust build tags and `-ldflags` to match your existing Forgejo build
-configuration.
+No Forgejo source tree or Go toolchain is required — all features work with
+static asset deployment.
---
@@ -285,25 +226,6 @@ ifcurl serve --allowed-hosts localhost:3000
non-private remote hosts, which is unsafe if the service is reachable from
untrusted clients.
-## Configuration
-
-Add to `/etc/forgejo/conf/app.ini`:
-
-```ini
-[ifcurl]
-PREVIEW_SERVICE_URL = http://localhost:8000
-```
-
-`PREVIEW_SERVICE_URL` is used **server-side** by Forgejo's markdown renderer to
-fetch preview images when rendering ifc:// links in issue descriptions and
-comments. It runs on the Forgejo server itself, so `localhost:8000` is the
-right value.
-
-If left empty, ifc:// links in markdown render as plain links with no preview
-image.
-
----
-
## Nginx reverse-proxy for PR diff images
The PR diff viewer (Case 3 in `footer.tmpl`) injects `
`
@@ -427,13 +349,6 @@ identify `.ifc` files correctly.
## Upgrading Forgejo
-After upgrading Forgejo upstream:
-
-1. Re-apply `go.patch` (or apply the two hunks by hand if context has shifted).
-2. Copy `ifc_url.go` and `ifc_url_test.go` back in — they are not modified by
- upstream.
-3. Run the Go tests to verify compatibility.
-4. Rebuild and redeploy.
-
-Custom assets and `footer.tmpl` do not require a rebuild and are unaffected by
-Forgejo upgrades.
+Custom assets (`viewer.html`, `ifcurl.js`, etc.) and `footer.tmpl` are
+unaffected by Forgejo upstream upgrades — redeploy them after each asset
+change and restart Forgejo once after updating `footer.tmpl`.
diff --git a/forgejo/SETUP_LOCAL.md b/forgejo/SETUP_LOCAL.md
index e5cf4a8..8f87d7d 100644
--- a/forgejo/SETUP_LOCAL.md
+++ b/forgejo/SETUP_LOCAL.md
@@ -195,18 +195,6 @@ Open `http://localhost:3000` and complete the Forgejo setup wizard.
---
-## Step 6 — Add ifcurl config to Forgejo
-
-The setup wizard writes its own `app.ini` inside the container. After
-completing the wizard, append the ifcurl setting and restart:
-
-```bash
-MSYS_NO_PATHCONV=1 docker exec forgejo sh -c 'printf "\n[ifcurl]\nPREVIEW_SERVICE_URL = http://ifcurl:8000\n" >> /data/gitea/conf/app.ini'
-docker compose restart forgejo
-```
-
----
-
## Verify the setup
Check that Forgejo is serving the ifcurl assets:
@@ -228,7 +216,7 @@ You should see JavaScript, not a 404.
| PR diff 3D renders (green=added, red=removed) | Yes |
| `[label](ifc://...)` links in markdown → inline preview | Yes |
| CLI rendering (`ifcurl render "ifc://..."`) | Yes |
-| Bare `ifc://...` text in markdown | No — needs Go patch + Forgejo rebuild |
+| Bare `ifc://...` text in markdown | Yes (avoid paired underscores in paths — use `[label](ifc://...)` form) |
For normal workflows, use `[label](ifc://...)` link syntax — the viewer's
**Issue** button generates this format automatically.
@@ -278,6 +266,5 @@ docker compose up -d
- The ifcurl service clones remote git repos as bare repos into the container's
cache on first use. Mutable refs (`@heads/`, `@HEAD`) trigger a `git fetch`
on each request; immutable refs (commit hashes, tags) use the cache as-is.
-- The Go patch in the ifcurl repo was written against Forgejo v13.0. As of
- this writing the latest release is v15.0.0. The patch is optional and only
- needed for bare `ifc://` URL rendering in markdown.
+- No Go patch or Forgejo rebuild is needed — all features including bare
+ `ifc://` URL rendering are handled client-side by `ifcurl.js`.
diff --git a/forgejo/go.patch b/forgejo/go.patch
deleted file mode 100644
index 4f39e98..0000000
--- a/forgejo/go.patch
+++ /dev/null
@@ -1,32 +0,0 @@
-diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go
-index 2b19e0f1c9..12be3e1c24 100644
---- a/modules/markup/markdown/markdown.go
-+++ b/modules/markup/markdown/markdown.go
-@@ -116,6 +116,7 @@ func SpecializedMarkdown() goldmark.Markdown {
- math.NewExtension(
- math.Enabled(setting.Markdown.EnableMath),
- ),
-+ NewIfcURLExtension(),
- ),
- goldmark.WithParserOptions(
- parser.WithAttribute(),
-diff --git a/modules/setting/markup.go b/modules/setting/markup.go
-index 4ab9e7b2d1..862852381e 100644
---- a/modules/setting/markup.go
-+++ b/modules/setting/markup.go
-@@ -59,8 +59,15 @@ type MarkupSanitizerRule struct {
- AllowDataURIImages bool
- }
-
-+// IfcURL holds configuration for the ifcurl preview service integration.
-+var IfcURL = struct {
-+ PreviewServiceURL string `ini:"PREVIEW_SERVICE_URL"`
-+ ServiceToken string `ini:"SERVICE_TOKEN"`
-+}{}
-+
- func loadMarkupFrom(rootCfg ConfigProvider) {
- mustMapSetting(rootCfg, "markdown", &Markdown)
-+ mustMapSetting(rootCfg, "ifcurl", &IfcURL)
-
- MermaidMaxSourceCharacters = rootCfg.Section("markup").Key("MERMAID_MAX_SOURCE_CHARACTERS").MustInt(50000)
- FilePreviewMaxLines = rootCfg.Section("markup").Key("FILEPREVIEW_MAX_LINES").MustInt(50)