From 17f46ab45f63629c3dccf086dabc9c147ee8ca44 Mon Sep 17 00:00:00 2001 From: Hamid Adesokan Date: Sat, 29 Aug 2026 16:47:05 -0500 Subject: [PATCH 1/2] viewer: resolve bundle-relative markdown links in concept bodies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The viewer's rewriteInternalLinks only rewired hrefs in OKF's recommended absolute form ('/tables/customers.md'). Bundles that use the relative form explicitly allowed by SPEC 6.1 (e.g. '../entities/foo.md' — common when bundles are also read in tools like Obsidian) fell through to the external-link path, and following them in a browser produced ERR_FILE_NOT_FOUND instead of navigating. Pass the current concept id into rewriteInternalLinks and resolve relative hrefs against its directory (handling '.' and '..' segments and URL-encoded characters such as %20). A relative target that resolves to an existing concept now navigates within the graph, matching the absolute-form behavior; targets that do not resolve still open externally. --- okf/src/reference_agent/viewer/static/viz.js | 45 ++++++++++++++------ 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/okf/src/reference_agent/viewer/static/viz.js b/okf/src/reference_agent/viewer/static/viz.js index 74088289..0dff7996 100644 --- a/okf/src/reference_agent/viewer/static/viz.js +++ b/okf/src/reference_agent/viewer/static/viz.js @@ -250,7 +250,7 @@ const html = marked.parse(body, { breaks: false, gfm: true }); const bodyEl = document.getElementById("detail-body"); bodyEl.innerHTML = html; - rewriteInternalLinks(bodyEl); + rewriteInternalLinks(bodyEl, conceptId); const bl = backlinks[conceptId] || []; const blSection = document.getElementById("detail-backlinks"); @@ -290,21 +290,42 @@ return event.at ? `${event.by} · ${event.at}` : String(event.by); } - function rewriteInternalLinks(root) { + function resolveRelative(href, conceptId) { + // Resolve a bundle-relative href (e.g. ../entities/Foo%20Bar.md) against + // the directory of the current concept, returning a concept id or null. + try { href = decodeURIComponent(href); } catch (e) { /* keep raw */ } + const baseParts = conceptId.split("/"); + baseParts.pop(); // drop the file name, keep the directory + const segs = href.split("/"); + for (const seg of segs) { + if (seg === "." ) continue; + else if (seg === "..") baseParts.pop(); + else baseParts.push(seg); + } + let id = baseParts.join("/"); + if (id.endsWith(".md")) id = id.slice(0, -3); + return id; + } + + function rewriteInternalLinks(root, conceptId) { root.querySelectorAll("a[href]").forEach((a) => { const href = a.getAttribute("href"); if (!href) return; + let target = null; if (href.startsWith("/") && href.endsWith(".md")) { - const target = href.slice(1, -3); - if (nodeIndex[target]) { - a.className = "internal"; - a.setAttribute("href", "javascript:void(0)"); - a.addEventListener("click", (e) => { - e.preventDefault(); - showDetail(target); - }); - return; - } + target = href.slice(1, -3); + } else if (!href.startsWith("#") && !/^[a-z][a-z0-9+.-]*:/i.test(href) && href.endsWith(".md")) { + // relative link — resolve against the current concept's directory + target = resolveRelative(href, conceptId); + } + if (target && nodeIndex[target]) { + a.className = "internal"; + a.setAttribute("href", "javascript:void(0)"); + a.addEventListener("click", (e) => { + e.preventDefault(); + showDetail(target); + }); + return; } a.className = "external"; a.setAttribute("target", "_blank"); From e2981df521c8f645e551948e457e6ecc065c124e Mon Sep 17 00:00:00 2001 From: Hamid Adesokan Date: Sat, 29 Aug 2026 18:05:06 -0500 Subject: [PATCH 2/2] Trigger CLA re-check after signing