Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions documentation/notes/wu.resource-pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ A ResourcePage is assembled from three layers:

Runtime code owns graph discovery and source resolution. Templates and stylesheets should arrange already-resolved document and panel data; they should not read RDF graphs, local files, remote URLs, mesh inventories, or config sources themselves.

Repository-backed payload pages show Repository Source instead of a duplicate Working File row. For a safe GitHub floating repository locator, Weave renders the repository URL and repository-relative path as one `blob/HEAD/<path>` browse link. `HEAD` reflects the floating working source; exact release identity remains on the relevant HistoricalState.

## Built-In Panels

The default Semantic Site presentation currently supports these generated panels:
Expand Down
6 changes: 6 additions & 0 deletions src/core/weave/resource_page_models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,18 @@ export interface ResourcePageDocumentModel {
stylesheetHrefs?: readonly string[];
title: string;
summary?: string;
summaryLink?: ResourcePageSummaryLinkModel;
rdfClasses: readonly ResourcePageRdfClassModel[];
breadcrumbs: readonly ResourcePageBreadcrumbModel[];
metadata: readonly ResourcePageMetadataModel[];
panels: readonly ResourcePagePanelModel[];
}

export interface ResourcePageSummaryLinkModel {
label: string;
href: string;
}

export interface ResourcePageBreadcrumbModel {
label: string;
href?: string;
Expand Down
5 changes: 2 additions & 3 deletions src/runtime/weave/page_model_assembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,9 +456,8 @@ function describeSemanticFlowResource(
}
const stateHistory = findHistoryForState(resourcePath, historyGroups);
if (stateHistory) {
return `Historical state for the ${
toLastPathSegment(stateHistory.path)
} artifact history`;
const historyLabel = toLastPathSegment(stateHistory.path);
return `Historical state for the ${historyLabel} artifact history`;
}
if (historyGroups.some((group) => group.path === resourcePath)) {
const ownerResourcePath = dirname(resourcePath);
Expand Down
91 changes: 90 additions & 1 deletion src/runtime/weave/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
ResourcePageReferenceLinkModel,
ResourcePageReferenceTargetLinkModel,
ResourcePageSectionModel,
ResourcePageSummaryLinkModel,
} from "../../core/weave/resource_page_models.ts";
import { Parser, type Quad, type Term } from "n3";
import { codeToHtml } from "shiki";
Expand Down Expand Up @@ -46,6 +47,7 @@ import {
SFLO_NAMESPACE,
SFLO_PREFIX,
} from "../../core/rdf/namespaces.ts";
import { findHistoryForState } from "../../core/weave/resource_page_history_groups.ts";

interface ResourcePageRenderInput {
meshLabel: string;
Expand All @@ -62,6 +64,7 @@ interface ResourcePageRenderInput {
title: string;
breadcrumbs: readonly ResourcePageBreadcrumb[];
summary?: string;
summaryLink?: ResourcePageSummaryLinkModel;
rdfClasses: readonly ResourcePageRdfClass[];
metadataRows: readonly ResourcePageMetadataRow[];
childrenRows: readonly ResourcePageMetadataRow[];
Expand Down Expand Up @@ -832,6 +835,10 @@ function toDefaultResourcePageDocumentModel(
? rdfFacts.classes
: [classifyResourcePage(resourcePath, page.historyGroups ?? [])];
const resourcePathArtifactRole = artifactRoleForResourcePath(resourcePath);
const stateHistory = findHistoryForState(
resourcePath,
page.historyGroups ?? [],
);

return {
kind: "simple",
Expand All @@ -853,6 +860,14 @@ function toDefaultResourcePageDocumentModel(
resourcePath,
),
summary: page.description,
...(stateHistory
? {
summaryLink: {
label: toLastPathSegment(stateHistory.path),
href: toMeshResourceHref(meshRootHref, stateHistory.path),
},
}
: {}),
rdfClasses,
metadata: [
{ label: "Canonical IRI", value: canonical },
Expand Down Expand Up @@ -1131,6 +1146,7 @@ function toResourcePageRenderInput(
title: document.title,
breadcrumbs: document.breadcrumbs,
summary: document.summary,
summaryLink: document.summaryLink,
rdfClasses: document.rdfClasses,
metadataRows: toRenderMetadataRows(
document.meshRootHref,
Expand Down Expand Up @@ -1237,6 +1253,21 @@ function toRenderMetadataRow(
if (row.kind === "repositorySource") {
const repositoryUrl = row.repositorySource.repositoryUrl;
const repositoryPathFromRoot = row.repositorySource.repositoryPathFromRoot;
const repositoryFileUrl = toGitHubRepositoryFileUrl(
repositoryUrl,
repositoryPathFromRoot,
);
if (repositoryFileUrl) {
return {
label: row.label,
value: repositoryFileUrl,
html: `<a class="wf-repository-source" href="${
escapeHtml(repositoryFileUrl)
}" rel="noreferrer noopener" target="_blank">${
escapeHtml(repositoryFileUrl)
}</a>`,
};
}
const repositoryUrlHtml = isSafeHttpUrl(repositoryUrl)
? `<a href="${
escapeHtml(repositoryUrl)
Expand Down Expand Up @@ -1377,6 +1408,24 @@ ${
</ul>`;
}

function renderSummary(
summary: string,
link?: ResourcePageSummaryLinkModel,
): string {
if (!link) {
return escapeHtml(summary);
}
const labelIndex = summary.indexOf(link.label);
if (labelIndex < 0) {
return escapeHtml(summary);
}
const before = summary.slice(0, labelIndex);
const after = summary.slice(labelIndex + link.label.length);
return `${escapeHtml(before)}<a href="${escapeHtml(link.href)}">${
escapeHtml(link.label)
}</a>${escapeHtml(after)}`;
}

async function renderDefaultResourcePage(
input: ResourcePageRenderInput,
): Promise<string> {
Expand All @@ -1387,7 +1436,9 @@ async function renderDefaultResourcePage(
? ` <link rel="icon" href="${escapeHtml(input.meshFaviconHref)}">\n`
: "";
const summary = input.summary
? ` <p class="wf-summary">${escapeHtml(input.summary)}</p>\n`
? ` <p class="wf-summary">${
renderSummary(input.summary, input.summaryLink)
}</p>\n`
: "";
const classes = input.rdfClasses.length > 0
? ` <p class="wf-classes">a ${
Expand Down Expand Up @@ -1861,6 +1912,44 @@ function isSafeHttpUrl(value: string): boolean {
}
}

function toGitHubRepositoryFileUrl(
repositoryUrl: string,
repositoryPathFromRoot: string,
): string | undefined {
if (!isSafeHttpUrl(repositoryUrl)) {
return undefined;
}
const url = new URL(repositoryUrl);
if (
url.hostname.toLowerCase() !== "github.com" ||
url.username ||
url.password ||
url.search ||
url.hash
) {
return undefined;
}
const repositorySegments = url.pathname.split("/").filter(Boolean);
if (repositorySegments.length !== 2) {
return undefined;
}
const [owner, repositoryWithSuffix] = repositorySegments;
const repository = repositoryWithSuffix!.replace(/\.git$/i, "");
const fileSegments = repositoryPathFromRoot.split("/");
if (
!owner || !repository || fileSegments.length === 0 ||
fileSegments.some((segment) =>
!segment || segment === "." || segment === ".." || segment.includes("\\")
)
) {
return undefined;
}
const encodedPath = fileSegments.map(encodeURIComponent).join("/");
return `https://github.com/${encodeURIComponent(owner)}/${
encodeURIComponent(repository)
}/blob/HEAD/${encodedPath}`;
}

function toExtractionSourceMetadataRows(
meshRootHref: string,
meshLabel: string,
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/weave/pages_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ Deno.test("renderResourcePage renders URL and floating repository working locato
);
assertStringIncludes(
html,
'<tr><th scope="row">Repository Source</th><td colspan="3"><span class="wf-repository-source"><a href="https://github.com/semantic-flow/sflo.git" rel="noreferrer noopener" target="_blank">https://github.com/semantic-flow/sflo.git</a><span aria-hidden="true"> / </span><span>semantic-flow-core-ontology.ttl</span></span></td></tr>',
'<tr><th scope="row">Repository Source</th><td colspan="3"><a class="wf-repository-source" href="https://github.com/semantic-flow/sflo/blob/HEAD/semantic-flow-core-ontology.ttl" rel="noreferrer noopener" target="_blank">https://github.com/semantic-flow/sflo/blob/HEAD/semantic-flow-core-ontology.ttl</a></td></tr>',
);
assertFalse(
html.includes(
Expand Down
6 changes: 5 additions & 1 deletion tests/integration/weave_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2880,6 +2880,10 @@ Deno.test("executeGenerate lists every sidecar payload history with working hist
releaseStatePage,
'<p class="wf-classes">a <a href="https://semantic-flow.github.io/sflo/ontology/HistoricalState">sflo:HistoricalState</a></p>',
);
assertStringIncludes(
releaseStatePage,
'<p class="wf-summary">Historical state for the <a href="/mesh-sidecar-fantasy-rules/ontology/releases">releases</a> artifact history</p>',
);
assertStringIncludes(
releaseStatePage,
"<summary>Manifestations</summary>",
Expand Down Expand Up @@ -3107,7 +3111,7 @@ Deno.test("executeGenerate renders working URL and floating repository source lo
);
assertStringIncludes(
page,
'<tr><th scope="row">Repository Source</th><td colspan="3"><span class="wf-repository-source"><a href="https://github.com/semantic-flow/mesh-sidecar-fantasy-rules.git" rel="noreferrer noopener" target="_blank">https://github.com/semantic-flow/mesh-sidecar-fantasy-rules.git</a><span aria-hidden="true"> / </span><span>ontology/fantasy-rules-ontology.ttl</span></span></td></tr>',
'<tr><th scope="row">Repository Source</th><td colspan="3"><a class="wf-repository-source" href="https://github.com/semantic-flow/mesh-sidecar-fantasy-rules/blob/HEAD/ontology/fantasy-rules-ontology.ttl" rel="noreferrer noopener" target="_blank">https://github.com/semantic-flow/mesh-sidecar-fantasy-rules/blob/HEAD/ontology/fantasy-rules-ontology.ttl</a></td></tr>',
);
assertFalse(
page.includes(
Expand Down