diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index be414bd..0de5db5 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -21,7 +21,7 @@ {"id":"ifcurl-ach","title":"Deploy Phase 3b footer template to Forgejo","description":"The footer.tmpl file that injects a 'View in 3D' button on .ifc file pages has been written but not yet deployed. Deploy it to /etc/forgejo/templates/custom/footer.tmpl and verify the button appears when viewing an .ifc file on localhost:3000.","status":"closed","priority":2,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-04-23T05:47:53Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:14:35Z","closed_at":"2026-04-23T06:14:35Z","close_reason":"Split into atomic tasks: ifcurl-ohq, ifcurl-49o","dependencies":[{"issue_id":"ifcurl-ach","depends_on_id":"ifcurl-ahd","type":"blocks","created_at":"2026-04-23T07:02:40Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"ifcurl-8s8","title":"viewer.html: interactive tool to set clipping planes","description":"Currently clip= planes can only be set by hand-editing the URL. Add an interactive clipping tool to the viewer: a UI control (e.g. toolbar button) that lets the user drag/place a clipping plane against the loaded model. The plane position and normal should be written back into the ifc:// URL as clip= parameters so the view is shareable. Consider using ThatOpen's clipper component (OBC.Clipper) if available, or a Three.js-based approach.","status":"closed","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T05:49:22Z","created_by":"Bruno Postle","updated_at":"2026-04-24T06:23:14Z","started_at":"2026-04-24T06:11:49Z","closed_at":"2026-04-24T06:23:14Z","close_reason":"setupClipper() uses OBC.Clipper: seeds URL planes as draggable SimplePlanes, toolbar '✂ clip' toggle for double-click placement, '⊗ clips' clear button. syncCameraUrl extended to serialise current planes back to URL.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-07a","title":"Cache size control for bare repo clones","description":"git.py clones remote repos as bare repos under ~/.cache/ifcurl/\u003chash\u003e/repo.git with no size limit or eviction policy. Large repos accumulate indefinitely. Add a configurable max cache size (e.g. via env var IFCURL_CACHE_MAX_GB or a config file) with an LRU eviction policy based on last-access time of each repo directory. Should also expose a CLI command (ifcurl cache --list, --prune, --clear) to inspect and manage the cache manually.","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-24T05:45:59Z","created_by":"Bruno Postle","updated_at":"2026-04-24T05:45:59Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"ifcurl-ts5","title":"Assess Go test strategy for Forgejo plugin patch","description":"The Forgejo integration is a low-maintenance source patch (footer.tmpl, viewer.html, /viewer and /proxy HTTP handlers). Evaluate whether adding Go tests is feasible without coupling to Forgejo's full test suite: can the handler logic be extracted into a small testable package, or do tests have to live inside the Forgejo tree? If the cost of keeping tests green across Forgejo upstream upgrades is too high, document the decision and close. Outcome: either a small test file or a documented rationale for no Go tests.","status":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-04-24T05:45:57Z","created_by":"Bruno Postle","updated_at":"2026-04-24T05:45:57Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"ifcurl-ts5","title":"Assess Go test strategy for Forgejo plugin patch","description":"The Forgejo integration is a low-maintenance source patch (footer.tmpl, viewer.html, /viewer and /proxy HTTP handlers). Evaluate whether adding Go tests is feasible without coupling to Forgejo's full test suite: can the handler logic be extracted into a small testable package, or do tests have to live inside the Forgejo tree? If the cost of keeping tests green across Forgejo upstream upgrades is too high, document the decision and close. Outcome: either a small test file or a documented rationale for no Go tests.","status":"closed","priority":3,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T05:45:57Z","created_by":"Bruno Postle","updated_at":"2026-04-24T06:51:21Z","started_at":"2026-04-24T06:36:35Z","closed_at":"2026-04-24T06:51:21Z","close_reason":"Go tests are feasible and low-maintenance. 7 tests in ifc_url_test.go live in the same package (markdown_test), use goldmark.New() directly (no Forgejo test infrastructure needed beyond TestMain which is already there). Only deps: setting.IfcURL.PreviewServiceURL (ours), goldmark (stable), testify. Test file copied to forgejo/modules/markup/markdown/ in ifcurl repo as patch artifact.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-hvw","title":"Add JavaScript tests for viewer.html URL logic","description":"viewer.html contains pure-JS functions (parseIfcUrl, buildIfcUrl, toRawUrl, syncCameraUrl) with no test coverage. Add a lightweight JS test harness (e.g. a standalone test.html or Node-runnable test.js using the native test runner) that unit-tests these functions: round-trip URL parse/build, toRawUrl for each host type (GitHub, GitLab, Forgejo), and camera parameter serialisation. No browser automation needed — pure function tests.","status":"closed","priority":3,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T05:45:56Z","created_by":"Bruno Postle","updated_at":"2026-04-24T06:30:47Z","started_at":"2026-04-24T06:26:01Z","closed_at":"2026-04-24T06:30:47Z","close_reason":"32 Node tests in tests/test_viewer_url.mjs covering parseIfcUrl, buildIfcUrl, toRawUrl. Pure functions extracted to viewer-url.js; viewer.html imports from it. Also caught + not being decoded in URL building.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-b9e","title":"Audit and extend Python test coverage","description":"103 tests exist across test_url.py, test_git.py, test_auth.py, test_render.py, and test_service.py. Audit what is missing: check branch/edge coverage in git.py (offline fallback, mutable ref fetch, auth injection), service.py (tier 2 cache eviction, mutable ref bypass), and the CLI entry points in __main__.py. Add tests for any significant gaps found.","status":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-04-24T05:45:40Z","created_by":"Bruno Postle","updated_at":"2026-04-24T05:45:40Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-bqt","title":"Phase 4: Bonsai IFCDOCUMENTREFERENCE federation via ifc://","description":"When opening a model via ifc://, resolve IFCDOCUMENTREFERENCE locations using the repo and ref from the root file's URL as context. Resolve ifc:// locations in linked model references recursively. Requires the Phase 4 protocol handler (ifcurl-0oj).","status":"deferred","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-23T05:48:56Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:15:11Z","defer_until":"2026-12-01T00:00:00Z","dependencies":[{"issue_id":"ifcurl-bqt","depends_on_id":"ifcurl-0oj","type":"blocks","created_at":"2026-04-23T06:49:02Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} diff --git a/forgejo/modules/markup/markdown/ifc_url_test.go b/forgejo/modules/markup/markdown/ifc_url_test.go new file mode 100644 index 0000000..01bdab5 --- /dev/null +++ b/forgejo/modules/markup/markdown/ifc_url_test.go @@ -0,0 +1,84 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package markdown_test + +import ( + "bytes" + "testing" + + "forgejo.org/modules/markup/markdown" + "forgejo.org/modules/setting" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/yuin/goldmark" +) + +func newTestMd(t *testing.T) goldmark.Markdown { + t.Helper() + setting.IfcURL.PreviewServiceURL = "http://localhost:8000" + t.Cleanup(func() { setting.IfcURL.PreviewServiceURL = "" }) + return goldmark.New(goldmark.WithExtensions(markdown.NewIfcURLExtension())) +} + +func convert(t *testing.T, md goldmark.Markdown, src string) string { + t.Helper() + var buf bytes.Buffer + require.NoError(t, md.Convert([]byte(src), &buf)) + return buf.String() +} + +func TestIfcURL_LinkForm(t *testing.T) { + md := newTestMd(t) + out := convert(t, md, "[label](ifc://example.com/org/repo@heads/main?path=model.ifc)") + assert.Contains(t, out, `
`) + assert.Contains(t, out, `ifc://example.com/org/repo@heads/main?path=model.ifc`) + assert.Contains(t, out, ``) + assert.Contains(t, out, `ifc://example.com/org/repo@heads/main?path=model.ifc`) +} + +func TestIfcURL_BareURLWithUnderscoreInPath(t *testing.T) { + // goldmark splits text at _ (emphasis delimiter); transformer must reassemble siblings + md := newTestMd(t) + out := convert(t, md, "ifc://example.com/org/repo@HEAD?path=_test_model.ifc") + assert.Contains(t, out, `
`) + assert.Contains(t, out, `_test_model.ifc`) +} + +func TestIfcURL_DisabledWhenNoServiceURL(t *testing.T) { + setting.IfcURL.PreviewServiceURL = "" + md := goldmark.New(goldmark.WithExtensions(markdown.NewIfcURLExtension())) + out := convert(t, md, "[label](ifc://example.com/org/repo@HEAD?path=model.ifc)") + assert.NotContains(t, out, `
`) +} + +func TestIfcURL_HTMLEscapesURLInOutput(t *testing.T) { + md := newTestMd(t) + // angle brackets and quotes in the URL must be escaped in HTML attributes + out := convert(t, md, `[label](ifc://example.com/org/repo@HEAD?path=m.ifc&x=<"'>)`) + assert.NotContains(t, out, `<"'>`) + assert.Contains(t, out, `<`) + assert.Contains(t, out, `"`) +} + +func TestIfcURL_NonIfcLinkUnchanged(t *testing.T) { + md := newTestMd(t) + out := convert(t, md, "[label](https://example.com/foo)") + assert.NotContains(t, out, ``) + assert.Contains(t, out, "for details.") +}