ifcurl/forgejo
Bruno Postle 4b53dfe426 Replace Go goldmark extension with JS text-node linkification
Bare ifc:// URLs in markdown are now handled entirely client-side:
linkifyBareIfcUrls() walks rendered HTML text nodes and promotes bare
ifc:// strings to <a> elements, which replaceIfcAnchors() then converts
to preview figures.  The [title](ifc://...) form was already handled by
Goldmark's standard link parser, so no Go extension is needed at all.

Removes ifc_url.go and ifc_url_test.go (the goldmark AST transformer
and renderer) from the repository.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 11:07:47 +01:00
..
custom/public/assets Replace Go goldmark extension with JS text-node linkification 2026-05-17 11:07:47 +01:00
server-config Fix query panel visibility and meta panel type counts 2026-05-16 21:02:54 +01:00
templates/custom forgejo: add JS unit tests, extract pure functions into viewer-util.js 2026-04-27 20:24:58 +01:00
tests Viewer: query field and results table (ifcurl-2iv, ifcurl-4u3) 2026-05-09 11:09:55 +00:00
.gitignore viewer: bundle JS deps locally — removes all CDN dependencies 2026-04-25 18:24:50 +01:00
build.js viewer: bundle JS deps locally — removes all CDN dependencies 2026-04-25 18:24:50 +01:00
go.patch forgejo: regenerate go.patch with ServiceToken field 2026-04-26 00:37:15 +01:00
package-lock.json forgejo: add @thatopen/components-front, fix click-to-identify and model overview 2026-04-28 00:01:19 +01:00
package.json forgejo: add @thatopen/components-front, fix click-to-identify and model overview 2026-04-28 00:01:19 +01:00
README.md Merge pull request #1 from OpeningDesign/forgejo/local-setup-docker 2026-04-28 00:02:33 +01:00
SETUP_LOCAL.md forgejo: add Docker-based local setup guide and Dockerfile 2026-04-27 12:26:58 -05:00
viewer-deps-entry.js forgejo: add @thatopen/components-front, fix click-to-identify and model overview 2026-04-28 00:01:19 +01:00

Forgejo integration

This directory contains assets for the ifcurl preview extension. The features split into two tiers by what infrastructure they require.

Feature overview

Feature What it needs Forgejo rebuild?
"View in 3D" button on file/history pages ifcurl.js + ifcurl service No
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

All features except bare-URL markdown rendering work with static asset deployment only — no Forgejo rebuild required. ifcurl.js detects <a href="ifc://..."> links produced by Goldmark's standard link parser and replaces them 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.

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 fetched by the browser (not by Forgejo's server), and browsers block cross-origin image requests to plain http://localhost URLs. Most production Forgejo deployments already sit behind Nginx for TLS termination; adding two proxy_pass lines is the only extra step.


Directory layout

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)
    viewer-util.js                      ← shared pure utilities: parseCommitHref, isSimpleTypeSelector, etc.
    ifcurl.js                           ← "View in 3D" + PR diff injection (ES module, no rebuild needed)
  templates/custom/
    footer.tmpl                         ← loads ifcurl.js (no rebuild needed)
  server-config/
    ifcurl-api.service                  ← systemd unit for the API service (git + caching)
    ifcurl-render.service               ← systemd unit for the render isolation service
    gitconfig-ifcmerge                  ← git merge driver registration for /etc/gitconfig
    gitattributes                       ← system-wide gitattributes for bare repos

Local setup (Windows + Docker)

For a self-contained local deployment of Forgejo + ifcurl on Windows using Docker, see SETUP_LOCAL.md.


Prerequisites

  • ifcurl API service + render service running (see 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:

cd /path/to/forgejo
git log --oneline modules/markup/markdown/markdown.go | head -3

Applying the patch

1. Copy new Go source files

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

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

cd /path/to/forgejo
go test ./modules/markup/markdown/ -run TestIfcURL -v

All seven tests should pass before proceeding.

4. Build and deploy Forgejo

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.


Building the viewer JavaScript bundle

The viewer loads Three.js, ThatOpen components, and JSZip from locally-bundled files rather than CDNs. The bundle files are committed to the repository, so you only need to re-run the build when upgrading a dependency version.

Prerequisites

Node.js 18+ and npm (used only at build time; not required on the server).

Run the build

cd forgejo/
npm install        # installs pinned versions from package-lock.json
npm run build      # produces the four files below
npm test           # run JavaScript unit tests (viewer-url.js, viewer-util.js)

Output files (committed to the repository):

File Source Purpose
custom/public/assets/viewer-deps.js three + @thatopen/components + @thatopen/components-front + jszip Main viewer dependencies
custom/public/assets/fragments-worker.js @thatopen/fragments worker IFC parsing web worker
custom/public/assets/web-ifc.wasm web-ifc IFC geometry kernel (single-threaded)
custom/public/assets/web-ifc-mt.wasm web-ifc IFC geometry kernel (multi-threaded)

The node_modules/ directory is gitignored and is never deployed.

Upgrading a dependency

Edit the version number in package.json, then:

cd forgejo/
npm install        # updates package-lock.json
npm run build      # regenerates the bundle from the new version
git add package.json package-lock.json custom/public/assets/
git commit -m "viewer: upgrade <package> to vX.Y.Z"

Deploying custom assets (no rebuild required)

These files can be updated at any time without recompiling Forgejo.

Viewer and URL logic

sudo mkdir -p /var/lib/forgejo/custom/public/assets/
sudo cp forgejo/custom/public/assets/viewer.html          /var/lib/forgejo/custom/public/assets/
sudo cp forgejo/custom/public/assets/viewer-url.js        /var/lib/forgejo/custom/public/assets/
sudo cp forgejo/custom/public/assets/viewer-util.js       /var/lib/forgejo/custom/public/assets/
sudo cp forgejo/custom/public/assets/viewer-deps.js       /var/lib/forgejo/custom/public/assets/
sudo cp forgejo/custom/public/assets/fragments-worker.js  /var/lib/forgejo/custom/public/assets/
sudo cp forgejo/custom/public/assets/web-ifc.wasm         /var/lib/forgejo/custom/public/assets/
sudo cp forgejo/custom/public/assets/web-ifc-mt.wasm      /var/lib/forgejo/custom/public/assets/
sudo cp forgejo/custom/public/assets/ifcurl.js            /var/lib/forgejo/custom/public/assets/

ifcurl.js is an ES module and imports from viewer-util.js at the same path, so both files must be deployed together.

Served at /assets/viewer.html, /assets/viewer-deps.js, etc.

Note: the correct CustomPath is /var/lib/forgejo/custom (Forgejo's default when FORGEJO_CUSTOM is not set). Do not use /etc/forgejo/public/assets/ — Forgejo does not serve from there.

sudo mkdir -p /var/lib/forgejo/custom/templates/custom/
sudo cp forgejo/templates/custom/footer.tmpl /var/lib/forgejo/custom/templates/custom/
sudo systemctl restart forgejo

Running the preview service

The service is split into two systemd units for security isolation:

  • ifcurl-api — handles HTTP requests, git fetching, and caching. Has network access and can call git CLI tools. Holds credentials (/etc/ifcurl/env). Delegates all IFC rendering to ifcurl-render over a Unix socket.

  • ifcurl-render — handles IFC parsing and rendering via ifcopenshell and pyvista. Has no network access, no credentials, and execve/execveat are blocked by systemd. Communicates only over the Unix socket.

This split limits the blast radius if a crafted IFC file achieves code execution in the render process: it cannot reach git hosts or read credentials.

# Create the two service users
sudo useradd --system --no-create-home --shell /usr/sbin/nologin ifcurl
sudo useradd --system --no-create-home --shell /usr/sbin/nologin ifcurl-render

# Add ifcurl user to ifcurl-render group so it can read the socket
sudo usermod -aG ifcurl-render ifcurl

# Install the units (edit AllowedHosts and port in ifcurl-api.service first)
sudo cp forgejo/server-config/ifcurl-api.service    /etc/systemd/system/
sudo cp forgejo/server-config/ifcurl-render.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now ifcurl-render
sudo systemctl enable --now ifcurl-api

Edit the ExecStart line in ifcurl-api.service to set your Forgejo hostname:

ExecStart=/usr/local/bin/ifcurl serve \
    --host 127.0.0.1 \
    --port 8000 \
    --allowed-hosts git.example.com

Cache limits and other environment variables for the API service go in /etc/ifcurl/env (one KEY=value per line):

IFCURL_CACHE_MAX_GB=10
IFCURL_T2_MAX=16

Render-side environment variables (timeouts, memory limits) go in /etc/ifcurl/render-env:

IFCURL_SANDBOX_TIMEOUT=180
IFCURL_SANDBOX_MEMORY_MB=2048

Single-service mode (testing / simple deployments)

Without IFCURL_RENDER_SOCKET set, the API service handles rendering directly in its own subprocess sandbox. This is simpler to set up but offers less isolation.

ifcurl serve --allowed-hosts git.example.com

For a Forgejo instance on a non-standard port (e.g. 3000 for local dev):

ifcurl serve --allowed-hosts localhost:3000

--allowed-hosts accepts a comma-separated list. Omitting it allows all non-private remote hosts, which is unsafe if the service is reachable from untrusted clients.

Configuration

Add to /etc/forgejo/conf/app.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 <img src="/render_diff?…"> tags that the browser must fetch. The browser resolves /render_diff against the Forgejo origin, so the ifcurl service must be reachable at the public URL.

Add these proxy locations to the Nginx virtual host that fronts Forgejo:

# ifcurl preview service — served under the same origin as Forgejo
location /preview {
    proxy_pass         http://127.0.0.1:8000;
    proxy_set_header   Host $host;
    proxy_read_timeout 120s;
}

location /render_diff {
    proxy_pass         http://127.0.0.1:8000;
    proxy_set_header   Host $host;
    proxy_read_timeout 120s;
}

location /bcf {
    proxy_pass         http://127.0.0.1:8000;
    proxy_set_header   Host $host;
    proxy_read_timeout 120s;
}

Without this proxy the "View in 3D" button (Case 1/2) still works — it opens the viewer HTML which fetches the IFC file directly. The PR diff images (Case 3) will silently fail to load until the proxy is in place.


IFC merge driver (ifcmerge)

Configuring ifcmerge as the git merge driver for .ifc files lets Forgejo automatically merge IFC pull requests instead of marking them as conflicted.

How Forgejo checks mergeability

Forgejo (with git ≥ 2.38) uses git merge-tree --write-tree to perform a trial merge in the bare repository. This command does invoke configured merge drivers — confirmed by testing. With ifcmerge set up correctly, the "can be automatically merged" indicator on a PR reflects ifcmerge's actual result, not a naive text-conflict check.

Important: bare repositories do not read committed .gitattributes files from the tree. The merge driver must be declared via a system-wide core.attributesFile setting in /etc/gitconfig.

Prerequisites

ifcmerge is a single Perl script from github.com/brunopostle/ifcmerge. Download and install it on the Forgejo server:

curl -o /usr/local/bin/ifcmerge \
  https://raw.githubusercontent.com/brunopostle/ifcmerge/main/ifcmerge
chmod +x /usr/local/bin/ifcmerge

Verify it is on PATH for the forgejo system user:

sudo -u forgejo which ifcmerge

Deploy the config files

# System-wide gitattributes (the critical piece for bare repos)
sudo cp forgejo/server-config/gitattributes /etc/gitattributes

# Append merge driver registration + core.attributesFile to /etc/gitconfig
sudo git config --system include.path /path/to/ifcurl/forgejo/server-config/gitconfig-ifcmerge

Or apply the blocks from server-config/gitconfig-ifcmerge directly into /etc/gitconfig by hand.

Merge direction

The invariant is that step-IDs in main/master must never be renumbered, because existing cross-references depend on them. The correct driver depends on which branch is the "local" (%A) side:

Situation %A (local) %B (remote) Driver to use
Forgejo "Merge commit" button main PR branch ifcmerge_ours (--prioritise-local)
Developer rebasing PR from main PR branch main ifcmerge (default)

For Forgejo server-side merges, use the "Merge commit" strategy (not rebase or squash) and configure .gitattributes to use ifcmerge_ours:

*.ifc merge=ifcmerge_ours

ifcmerge (no flag) is for client-side use by developers updating their working branch from main, where main arrives as the remote (%B) side and its IDs are preserved by default.

Client-side .gitattributes

For client-side merges (git merge on a developer's machine) committed .gitattributes files in the repository work fine. It is still worth adding one:

*.ifc merge=ifcmerge

This ensures developers with ifcmerge installed get automatic IFC merges locally, and tools that read gitattributes (e.g. diff viewers) can 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.