mirror of
https://github.com/brunopostle/ifcurl.git
synced 2026-07-12 02:08:13 +00:00
viewer: extract URL logic to viewer-url.js, add JS tests
parseIfcUrl, buildIfcUrl, toRawUrl extracted from viewer.html into viewer-url.js so they can be imported and tested independently. viewer.html imports from the module. 32 Node test-runner tests in tests/test_viewer_url.mjs cover all three functions across GitHub, GitLab, Forgejo/Gitea, SSH user@ prefix, and round-trip cases. Caught a bug: + (selector union operator) was being URL-encoded as %2B in buildIfcUrl; fixed by adding .replace(/%2B/g, "+") alongside the existing comma fix, in both viewer-url.js and syncCameraUrl. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
13c8687027
commit
d98d4c35d5
4 changed files with 272 additions and 91 deletions
|
|
@ -22,7 +22,7 @@
|
|||
{"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-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":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-04-24T05:45:56Z","created_by":"Bruno Postle","updated_at":"2026-04-24T05:45:56Z","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}
|
||||
{"id":"ifcurl-lcr","title":"Phase 4: Bonsai 'Copy view URL' button","description":"Add a button to Bonsai that generates an ifc:// URL from the current state: git remote/ref/file path, active selection as IfcOpenShell selector, current camera as camera+fov or scale params in IFC world coordinates, active clipping planes as clip params, current visibility mode. Default to commit hash ref; secondary option for @heads/ ref.","status":"deferred","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-23T05:48:55Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:15:10Z","defer_until":"2026-12-01T00:00:00Z","dependencies":[{"issue_id":"ifcurl-lcr","depends_on_id":"ifcurl-0oj","type":"blocks","created_at":"2026-04-23T06:49:01Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
||||
|
|
|
|||
78
forgejo/custom/public/assets/viewer-url.js
Normal file
78
forgejo/custom/public/assets/viewer-url.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
// Pure ifc:// URL logic shared between viewer.html and tests.
|
||||
|
||||
export function parseIfcUrl(raw) {
|
||||
if (!raw?.startsWith("ifc://")) return null;
|
||||
const body = raw.slice("ifc://".length);
|
||||
|
||||
const slashIdx = body.indexOf("/");
|
||||
const authority = slashIdx < 0 ? body : body.slice(0, slashIdx);
|
||||
const pathAndQuery = slashIdx < 0 ? "" : body.slice(slashIdx + 1);
|
||||
|
||||
const atInAuth = authority.indexOf("@");
|
||||
const userAt = atInAuth < 0 ? "" : authority.slice(0, atInAuth + 1);
|
||||
const host = atInAuth < 0 ? authority : authority.slice(atInAuth + 1);
|
||||
|
||||
const qIdx = pathAndQuery.indexOf("?");
|
||||
const pathPart = qIdx < 0 ? pathAndQuery : pathAndQuery.slice(0, qIdx);
|
||||
const qs = new URLSearchParams(qIdx < 0 ? "" : pathAndQuery.slice(qIdx + 1));
|
||||
|
||||
const atIdx = pathPart.lastIndexOf("@");
|
||||
const repoSuffix = atIdx < 0 ? pathPart : pathPart.slice(0, atIdx);
|
||||
const ref = atIdx < 0 ? "" : pathPart.slice(atIdx + 1);
|
||||
|
||||
return {
|
||||
repo: userAt + host + "/" + repoSuffix,
|
||||
host, userAt, repoSuffix, ref,
|
||||
path: qs.get("path") ?? "",
|
||||
selector: qs.get("selector") ?? "",
|
||||
camera: qs.get("camera") ?? "",
|
||||
fov: qs.get("fov") ?? "",
|
||||
scale: qs.get("scale") ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
export function buildIfcUrl(repo, ref, path, selector) {
|
||||
if (!repo || !ref) return null;
|
||||
const qs = new URLSearchParams();
|
||||
if (path) qs.set("path", path);
|
||||
if (selector) qs.set("selector", selector);
|
||||
const qsStr = qs.toString().replace(/%2C/g, ",").replace(/%2B/g, "+");
|
||||
return `ifc://${repo}@${ref}${qsStr ? "?" + qsStr : ""}`;
|
||||
}
|
||||
|
||||
export function toRawUrl(raw) {
|
||||
const body = raw.slice("ifc://".length);
|
||||
|
||||
const slashIdx = body.indexOf("/");
|
||||
const authority = slashIdx < 0 ? body : body.slice(0, slashIdx);
|
||||
const pathAndQuery = slashIdx < 0 ? "" : body.slice(slashIdx + 1);
|
||||
|
||||
const atInAuth = authority.indexOf("@");
|
||||
const host = atInAuth < 0 ? authority : authority.slice(atInAuth + 1);
|
||||
|
||||
const qIdx = pathAndQuery.indexOf("?");
|
||||
const pathPart = qIdx < 0 ? pathAndQuery : pathAndQuery.slice(0, qIdx);
|
||||
const qs = new URLSearchParams(qIdx < 0 ? "" : pathAndQuery.slice(qIdx + 1));
|
||||
const filePath = qs.get("path") ?? "";
|
||||
|
||||
const atIdx = pathPart.lastIndexOf("@");
|
||||
const repoPath = atIdx < 0 ? pathPart : pathPart.slice(0, atIdx);
|
||||
const ref = atIdx < 0 ? "" : pathPart.slice(atIdx + 1);
|
||||
|
||||
const plainRef = ref.startsWith("heads/") ? ref.slice(6)
|
||||
: ref.startsWith("tags/") ? ref.slice(5)
|
||||
: ref;
|
||||
|
||||
if (host === "github.com" || host.endsWith(".github.com")) {
|
||||
return `https://raw.githubusercontent.com/${repoPath}/${plainRef}/${filePath}`;
|
||||
}
|
||||
if (host === "gitlab.com" || host.includes("gitlab")) {
|
||||
const proto = host.startsWith("localhost") ? "http" : "https";
|
||||
return `${proto}://${host}/${repoPath}/-/raw/${plainRef}/${filePath}`;
|
||||
}
|
||||
const refSeg = ref.startsWith("heads/") ? "branch/" + ref.slice(6)
|
||||
: ref.startsWith("tags/") ? "tag/" + ref.slice(5)
|
||||
: "commit/" + ref;
|
||||
const proto = host.startsWith("localhost") ? "http" : "https";
|
||||
return `${proto}://${host}/${repoPath}/raw/${refSeg}/${filePath}`;
|
||||
}
|
||||
|
|
@ -123,6 +123,7 @@
|
|||
<script type="module">
|
||||
import * as THREE from "three";
|
||||
import * as OBC from "@thatopen/components";
|
||||
import { parseIfcUrl, toRawUrl, buildIfcUrl as _buildIfcUrl } from "./viewer-url.js";
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const ifcUrl = params.get("url") ?? "";
|
||||
|
|
@ -145,55 +146,12 @@
|
|||
let threeCamera = null;
|
||||
let activeClipper = null;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Parse an ifc:// URL into its components.
|
||||
// -----------------------------------------------------------------------
|
||||
function parseIfcUrl(raw) {
|
||||
if (!raw?.startsWith("ifc://")) return null;
|
||||
const body = raw.slice("ifc://".length);
|
||||
|
||||
const slashIdx = body.indexOf("/");
|
||||
const authority = slashIdx < 0 ? body : body.slice(0, slashIdx);
|
||||
const pathAndQuery = slashIdx < 0 ? "" : body.slice(slashIdx + 1);
|
||||
|
||||
const atInAuth = authority.indexOf("@");
|
||||
const userAt = atInAuth < 0 ? "" : authority.slice(0, atInAuth + 1);
|
||||
const host = atInAuth < 0 ? authority : authority.slice(atInAuth + 1);
|
||||
|
||||
const qIdx = pathAndQuery.indexOf("?");
|
||||
const pathPart = qIdx < 0 ? pathAndQuery : pathAndQuery.slice(0, qIdx);
|
||||
const qs = new URLSearchParams(qIdx < 0 ? "" : pathAndQuery.slice(qIdx + 1));
|
||||
|
||||
const atIdx = pathPart.lastIndexOf("@");
|
||||
const repoSuffix = atIdx < 0 ? pathPart : pathPart.slice(0, atIdx);
|
||||
const ref = atIdx < 0 ? "" : pathPart.slice(atIdx + 1);
|
||||
|
||||
return {
|
||||
repo: userAt + host + "/" + repoSuffix,
|
||||
host, userAt, repoSuffix, ref,
|
||||
path: qs.get("path") ?? "",
|
||||
selector: qs.get("selector") ?? "",
|
||||
camera: qs.get("camera") ?? "",
|
||||
fov: qs.get("fov") ?? "",
|
||||
scale: qs.get("scale") ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Build an ifc:// URL from structured fields (no camera — will fit to box).
|
||||
// -----------------------------------------------------------------------
|
||||
// Build an ifc:// URL from the structured form fields.
|
||||
function buildIfcUrl() {
|
||||
const repo = repoInput.value.trim();
|
||||
const ref = refInput.value.trim();
|
||||
const path = pathInput.value.trim();
|
||||
const sel = selectorInput.value.trim();
|
||||
if (!repo || !ref) return null;
|
||||
|
||||
const qs = new URLSearchParams();
|
||||
if (path) qs.set("path", path);
|
||||
if (sel) qs.set("selector", sel);
|
||||
const qsStr = qs.toString().replace(/%2C/g, ",");
|
||||
return `ifc://${repo}@${ref}${qsStr ? "?" + qsStr : ""}`;
|
||||
return _buildIfcUrl(
|
||||
repoInput.value.trim(), refInput.value.trim(),
|
||||
pathInput.value.trim(), selectorInput.value.trim()
|
||||
);
|
||||
}
|
||||
|
||||
// Populate structured fields from the current ifcUrl.
|
||||
|
|
@ -327,7 +285,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
const newIfcUrl = base + "?" + qs.toString().replace(/%2C/g, ",");
|
||||
const newIfcUrl = base + "?" + qs.toString().replace(/%2C/g, ",").replace(/%2B/g, "+");
|
||||
|
||||
urlInput.value = newIfcUrl;
|
||||
|
||||
|
|
@ -446,47 +404,6 @@
|
|||
return true;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Convert an ifc:// URL to an HTTPS raw file URL.
|
||||
// Handles SSH user@ prefix, GitHub, GitLab, and Forgejo/Gitea.
|
||||
// -----------------------------------------------------------------------
|
||||
function toRawUrl(raw) {
|
||||
const body = raw.slice("ifc://".length);
|
||||
|
||||
const slashIdx = body.indexOf("/");
|
||||
const authority = slashIdx < 0 ? body : body.slice(0, slashIdx);
|
||||
const pathAndQuery = slashIdx < 0 ? "" : body.slice(slashIdx + 1);
|
||||
|
||||
const atInAuth = authority.indexOf("@");
|
||||
const host = atInAuth < 0 ? authority : authority.slice(atInAuth + 1);
|
||||
|
||||
const qIdx = pathAndQuery.indexOf("?");
|
||||
const pathPart = qIdx < 0 ? pathAndQuery : pathAndQuery.slice(0, qIdx);
|
||||
const qs = new URLSearchParams(qIdx < 0 ? "" : pathAndQuery.slice(qIdx + 1));
|
||||
const filePath = qs.get("path") ?? "";
|
||||
|
||||
const atIdx = pathPart.lastIndexOf("@");
|
||||
const repoPath = atIdx < 0 ? pathPart : pathPart.slice(0, atIdx);
|
||||
const ref = atIdx < 0 ? "" : pathPart.slice(atIdx + 1);
|
||||
|
||||
const plainRef = ref.startsWith("heads/") ? ref.slice(6)
|
||||
: ref.startsWith("tags/") ? ref.slice(5)
|
||||
: ref;
|
||||
|
||||
if (host === "github.com" || host.endsWith(".github.com")) {
|
||||
return `https://raw.githubusercontent.com/${repoPath}/${plainRef}/${filePath}`;
|
||||
}
|
||||
if (host === "gitlab.com" || host.includes("gitlab")) {
|
||||
const proto = host.startsWith("localhost") ? "http" : "https";
|
||||
return `${proto}://${host}/${repoPath}/-/raw/${plainRef}/${filePath}`;
|
||||
}
|
||||
const refSeg = ref.startsWith("heads/") ? "branch/" + ref.slice(6)
|
||||
: ref.startsWith("tags/") ? "tag/" + ref.slice(5)
|
||||
: "commit/" + ref;
|
||||
const proto = host.startsWith("localhost") ? "http" : "https";
|
||||
return `${proto}://${host}/${repoPath}/raw/${refSeg}/${filePath}`;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Main
|
||||
// -----------------------------------------------------------------------
|
||||
|
|
|
|||
186
tests/test_viewer_url.mjs
Normal file
186
tests/test_viewer_url.mjs
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { parseIfcUrl, buildIfcUrl, toRawUrl } from "../forgejo/custom/public/assets/viewer-url.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// parseIfcUrl
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("parseIfcUrl: null for non-ifc scheme", () => {
|
||||
assert.equal(parseIfcUrl("https://example.com/foo"), null);
|
||||
});
|
||||
|
||||
test("parseIfcUrl: null for null input", () => {
|
||||
assert.equal(parseIfcUrl(null), null);
|
||||
});
|
||||
|
||||
test("parseIfcUrl: null for empty string", () => {
|
||||
assert.equal(parseIfcUrl(""), null);
|
||||
});
|
||||
|
||||
test("parseIfcUrl: simple HTTPS host", () => {
|
||||
const u = parseIfcUrl("ifc://example.com/org/repo@heads/main?path=model.ifc");
|
||||
assert.equal(u.host, "example.com");
|
||||
assert.equal(u.userAt, "");
|
||||
assert.equal(u.repoSuffix, "org/repo");
|
||||
assert.equal(u.ref, "heads/main");
|
||||
assert.equal(u.path, "model.ifc");
|
||||
assert.equal(u.repo, "example.com/org/repo");
|
||||
});
|
||||
|
||||
test("parseIfcUrl: SSH user@ in authority", () => {
|
||||
const u = parseIfcUrl("ifc://git@github.com/org/repo@abc1234?path=m.ifc");
|
||||
assert.equal(u.host, "github.com");
|
||||
assert.equal(u.userAt, "git@");
|
||||
assert.equal(u.repo, "git@github.com/org/repo");
|
||||
});
|
||||
|
||||
test("parseIfcUrl: host with port", () => {
|
||||
const u = parseIfcUrl("ifc://localhost:3000/org/repo@heads/main?path=m.ifc");
|
||||
assert.equal(u.host, "localhost:3000");
|
||||
});
|
||||
|
||||
test("parseIfcUrl: tag ref", () => {
|
||||
const u = parseIfcUrl("ifc://example.com/org/repo@tags/v1.2.3?path=m.ifc");
|
||||
assert.equal(u.ref, "tags/v1.2.3");
|
||||
});
|
||||
|
||||
test("parseIfcUrl: commit hash ref", () => {
|
||||
const u = parseIfcUrl("ifc://example.com/org/repo@deadbeef?path=m.ifc");
|
||||
assert.equal(u.ref, "deadbeef");
|
||||
});
|
||||
|
||||
test("parseIfcUrl: selector param", () => {
|
||||
const u = parseIfcUrl("ifc://example.com/org/repo@HEAD?path=m.ifc&selector=IfcWall");
|
||||
assert.equal(u.selector, "IfcWall");
|
||||
});
|
||||
|
||||
test("parseIfcUrl: camera param", () => {
|
||||
const u = parseIfcUrl("ifc://example.com/org/repo@HEAD?path=m.ifc&camera=1,2,3,4,5,6,7,8,9");
|
||||
assert.equal(u.camera, "1,2,3,4,5,6,7,8,9");
|
||||
});
|
||||
|
||||
test("parseIfcUrl: fov param", () => {
|
||||
const u = parseIfcUrl("ifc://example.com/org/repo@HEAD?path=m.ifc&fov=60");
|
||||
assert.equal(u.fov, "60");
|
||||
});
|
||||
|
||||
test("parseIfcUrl: scale param", () => {
|
||||
const u = parseIfcUrl("ifc://example.com/org/repo@HEAD?path=m.ifc&scale=10");
|
||||
assert.equal(u.scale, "10");
|
||||
});
|
||||
|
||||
test("parseIfcUrl: missing params default to empty string", () => {
|
||||
const u = parseIfcUrl("ifc://example.com/org/repo@HEAD");
|
||||
assert.equal(u.path, "");
|
||||
assert.equal(u.selector, "");
|
||||
assert.equal(u.camera, "");
|
||||
assert.equal(u.fov, "");
|
||||
assert.equal(u.scale, "");
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// buildIfcUrl
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("buildIfcUrl: null when repo missing", () => {
|
||||
assert.equal(buildIfcUrl("", "heads/main", "", ""), null);
|
||||
});
|
||||
|
||||
test("buildIfcUrl: null when ref missing", () => {
|
||||
assert.equal(buildIfcUrl("example.com/org/repo", "", "", ""), null);
|
||||
});
|
||||
|
||||
test("buildIfcUrl: bare repo + ref", () => {
|
||||
const url = buildIfcUrl("example.com/org/repo", "heads/main", "", "");
|
||||
assert.equal(url, "ifc://example.com/org/repo@heads/main");
|
||||
});
|
||||
|
||||
test("buildIfcUrl: with path", () => {
|
||||
const url = buildIfcUrl("example.com/org/repo", "heads/main", "models/building.ifc", "");
|
||||
assert.equal(url, "ifc://example.com/org/repo@heads/main?path=models%2Fbuilding.ifc");
|
||||
});
|
||||
|
||||
test("buildIfcUrl: with selector", () => {
|
||||
const url = buildIfcUrl("example.com/org/repo", "heads/main", "", "IfcWall");
|
||||
assert.equal(url, "ifc://example.com/org/repo@heads/main?selector=IfcWall");
|
||||
});
|
||||
|
||||
test("buildIfcUrl: commas in selector not percent-encoded", () => {
|
||||
const url = buildIfcUrl("example.com/org/repo", "heads/main", "m.ifc", "IfcWall+IfcSlab");
|
||||
assert.ok(url.includes("IfcWall+IfcSlab"), `got: ${url}`);
|
||||
assert.ok(!url.includes("%2B") && !url.includes("%2C"), `got: ${url}`);
|
||||
});
|
||||
|
||||
test("buildIfcUrl: SSH-style repo", () => {
|
||||
const url = buildIfcUrl("git@github.com/org/repo", "heads/main", "m.ifc", "");
|
||||
assert.ok(url.startsWith("ifc://git@github.com/org/repo@heads/main"));
|
||||
});
|
||||
|
||||
test("buildIfcUrl: round-trips through parseIfcUrl", () => {
|
||||
const url = buildIfcUrl("example.com/org/repo", "heads/main", "model.ifc", "IfcWall");
|
||||
const p = parseIfcUrl(url);
|
||||
assert.equal(p.repo, "example.com/org/repo");
|
||||
assert.equal(p.ref, "heads/main");
|
||||
assert.equal(p.path, "model.ifc");
|
||||
assert.equal(p.selector, "IfcWall");
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// toRawUrl
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("toRawUrl: GitHub heads/ branch", () => {
|
||||
const url = toRawUrl("ifc://github.com/org/repo@heads/main?path=model.ifc");
|
||||
assert.equal(url, "https://raw.githubusercontent.com/org/repo/main/model.ifc");
|
||||
});
|
||||
|
||||
test("toRawUrl: GitHub tags/ tag", () => {
|
||||
const url = toRawUrl("ifc://github.com/org/repo@tags/v1.0?path=model.ifc");
|
||||
assert.equal(url, "https://raw.githubusercontent.com/org/repo/v1.0/model.ifc");
|
||||
});
|
||||
|
||||
test("toRawUrl: GitHub commit hash", () => {
|
||||
const url = toRawUrl("ifc://github.com/org/repo@deadbeef?path=model.ifc");
|
||||
assert.equal(url, "https://raw.githubusercontent.com/org/repo/deadbeef/model.ifc");
|
||||
});
|
||||
|
||||
test("toRawUrl: GitHub with SSH user@ stripped", () => {
|
||||
const url = toRawUrl("ifc://git@github.com/org/repo@heads/main?path=model.ifc");
|
||||
assert.equal(url, "https://raw.githubusercontent.com/org/repo/main/model.ifc");
|
||||
});
|
||||
|
||||
test("toRawUrl: GitLab.com heads/ branch", () => {
|
||||
const url = toRawUrl("ifc://gitlab.com/org/repo@heads/main?path=model.ifc");
|
||||
assert.equal(url, "https://gitlab.com/org/repo/-/raw/main/model.ifc");
|
||||
});
|
||||
|
||||
test("toRawUrl: self-hosted GitLab", () => {
|
||||
const url = toRawUrl("ifc://gitlab.example.com/org/repo@heads/main?path=model.ifc");
|
||||
assert.equal(url, "https://gitlab.example.com/org/repo/-/raw/main/model.ifc");
|
||||
});
|
||||
|
||||
test("toRawUrl: Forgejo/Gitea heads/ branch", () => {
|
||||
const url = toRawUrl("ifc://gitea.example.com/org/repo@heads/main?path=model.ifc");
|
||||
assert.equal(url, "https://gitea.example.com/org/repo/raw/branch/main/model.ifc");
|
||||
});
|
||||
|
||||
test("toRawUrl: Forgejo/Gitea tags/ tag", () => {
|
||||
const url = toRawUrl("ifc://gitea.example.com/org/repo@tags/v1.0?path=model.ifc");
|
||||
assert.equal(url, "https://gitea.example.com/org/repo/raw/tag/v1.0/model.ifc");
|
||||
});
|
||||
|
||||
test("toRawUrl: Forgejo/Gitea commit hash", () => {
|
||||
const url = toRawUrl("ifc://gitea.example.com/org/repo@deadbeef?path=model.ifc");
|
||||
assert.equal(url, "https://gitea.example.com/org/repo/raw/commit/deadbeef/model.ifc");
|
||||
});
|
||||
|
||||
test("toRawUrl: localhost Forgejo uses http://", () => {
|
||||
const url = toRawUrl("ifc://localhost:3000/org/repo@heads/main?path=model.ifc");
|
||||
assert.ok(url.startsWith("http://localhost:3000/"), `got: ${url}`);
|
||||
});
|
||||
|
||||
test("toRawUrl: nested path within repo", () => {
|
||||
const url = toRawUrl("ifc://github.com/org/repo@heads/main?path=sub/dir/model.ifc");
|
||||
assert.equal(url, "https://raw.githubusercontent.com/org/repo/main/sub/dir/model.ifc");
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue