FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Phase 1: embed JSON-LD data island in mashlib HTML wrapper (#7) by melvincarvalho · Pull Request #343 · JavaScriptSolidServer/JavaScriptSolidServer · GitHub

Repository navigation

44 changes: 39 additions & 5 deletions src/handlers/resource.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from '../rdf/conneg.js';
import { emitChange } from '../notifications/events.js';
import { checkIfMatch, checkIfNoneMatchForGet, checkIfNoneMatchForWrite } from '../utils/conditional.js';
import { generateDatabrowserHtml, generateModuleDatabrowserHtml, shouldServeMashlib } from '../mashlib/index.js';
import { generateDatabrowserHtml, generateModuleDatabrowserHtml, shouldServeMashlib, DATA_ISLAND_MAX_BYTES } from '../mashlib/index.js';

/**
* Live reload script - injected into HTML when --live-reload is enabled
Expand Down Expand Up @@ -238,9 +238,21 @@ export async function handleGet(request, reply) {

// Check if we should serve Mashlib data browser for containers
if (shouldServeMashlib(request, request.mashlibEnabled, 'application/ld+json')) {
// Phase 1 of #7: also embed the container's JSON-LD listing as a
// data island so consumers that look for `<script
// type="application/ld+json">` (search-engine rich-results,
// archival crawlers, future mashlib zero-fetch path) get the data
// without a second request. Use compact (no-whitespace) form for
// the embed so we don't burn bytes against DATA_ISLAND_MAX_BYTES
// on indentation that nothing will ever read.
const embedJsonLd = JSON.stringify(jsonLd);
const html = request.mashlibModule
Comment on lines +245 to 249
? generateModuleDatabrowserHtml(request.mashlibModule)
: generateDatabrowserHtml(resourceUrl, request.mashlibCdn ? request.mashlibVersion : null);
? generateModuleDatabrowserHtml(request.mashlibModule, resourceUrl, { embedJsonLd })
: generateDatabrowserHtml(
resourceUrl,
request.mashlibCdn ? request.mashlibVersion : null,
{ embedJsonLd }
);
Comment on lines 249 to +255
Comment on lines +241 to +255
const headers = getAllHeaders({
isContainer: true,
etag: stats.etag,
Expand Down Expand Up @@ -318,9 +330,31 @@ export async function handleGet(request, reply) {
// Check if we should serve Mashlib data browser
// Only for RDF resources when Accept: text/html is requested
if (shouldServeMashlib(request, request.mashlibEnabled, storedContentType)) {
// Phase 1 of #7: embed the resource's JSON-LD bytes as a data
// island when it's already JSON-LD (the JSS-native format). Other
// formats are out of Phase-1 scope; the wrapper still loads
// correctly and mashlib XHR-fetches as before.
//
// Cap-aware short-circuit: skip the read entirely when the file is
// already over the embed cap. The island would be dropped anyway,
// and large JSON-LD resources would otherwise load into memory on
// every HTML navigation.
let embedJsonLd;
if (storedContentType === 'application/ld+json' &&
stats.size <= DATA_ISLAND_MAX_BYTES) {
// dataIsland() in mashlib/index.js coerces Buffer → string itself,
// so we hand it the Buffer directly instead of allocating a UTF-8
// string copy on every navigation.
const buf = await storage.read(storagePath);
if (buf) embedJsonLd = buf;
}
Comment on lines +342 to +350
const html = request.mashlibModule
? generateModuleDatabrowserHtml(request.mashlibModule)
: generateDatabrowserHtml(resourceUrl, request.mashlibCdn ? request.mashlibVersion : null);
? generateModuleDatabrowserHtml(request.mashlibModule, resourceUrl, { embedJsonLd })
: generateDatabrowserHtml(
resourceUrl,
request.mashlibCdn ? request.mashlibVersion : null,
{ embedJsonLd }
);
Comment on lines 351 to +357
const headers = getAllHeaders({
isContainer: false,
etag: stats.etag,
Expand Down
91 changes: 85 additions & 6 deletions src/mashlib/index.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,97 @@
* Generates HTML wrapper that loads SolidOS Mashlib from CDN.
* When a browser requests an RDF resource with Accept: text/html,
* we return this wrapper which then fetches and renders the data.
*
* Phase 1 of #7: when the originating resource is reasonably small
* RDF, the JSON-LD bytes are embedded in the wrapper as a `<script
* type="application/ld+json" id="dataisland" data-uri="…">` block.
* Browsers ignore non-JS script bodies, so this is harmless to all
* existing clients (mashlib still XHR-fetches today). It immediately
* benefits anything that knows to look for `application/ld+json`
* islands — search engine rich-results, archival crawlers, scrapers,
* static-site exporters — and gives Phase 2 a zero-network fast path.
*/

/**
* Cap on how much JSON-LD we'll inline. A 256 KB resource fits any
* realistic profile, type index, or container listing. Above that we
* drop the island and let the existing XHR path handle it so we don't
* make every navigation re-download a multi-megabyte resource.
*/
export const DATA_ISLAND_MAX_BYTES = 256 * 1024;

/**
* Escape a JSON-LD body for safe inclusion inside `<script
* type="application/ld+json">…</script>`.
*
* Browsers don't execute the script (wrong MIME), but the HTML parser
* still scans the body for an end-of-script tag. The relevant rule:
* any `</` followed by `script` (case-insensitive) terminates the
* element regardless of what follows — `</script>`, `</script >`,
* `</script\n>`, `</SCRIPT>` and friends all close it. Escaping just
* the literal `</script>` token is too narrow.
*
* The robust fix is to replace every literal `<` byte in the body with
* the JSON string-escape for U+003C — the six characters
* backslash-u-0-0-3-c (the same form the implementation emits below).
* JSON-LD is JSON, and a JSON parser decodes that escape back to a
* literal `<` natively, so document semantics are preserved. After
* this transform the body literally cannot contain a `<` byte — so no
* end-tag (or comment, CDATA, etc.) can possibly start.
*/
function escapeForScriptBlock(jsonLdString) {
return String(jsonLdString).replace(/</g, '\\u003c');
}

/**
* Build the data-island `<script>` block for the given JSON-LD payload.
* Returns an empty string if the payload is missing or over the size
* cap so callers can unconditionally interpolate `dataIsland(...)`.
*
* The cap applies to the *escaped* body — i.e. the bytes that will
* actually appear in the HTTP response. `escapeForScriptBlock` can
* expand input up to 6x (each literal `<` becomes the 6-byte JSON
* escape sequence backslash-u-0-0-3-c), so checking the raw input
* size alone could let an HTML response balloon past the cap.
*
* Two-stage check:
* 1. Cheap raw-byte pre-check — escape can only grow the body,
* so a raw payload already over the cap is guaranteed to be
* over after escaping; drop without doing the work.
* 2. Post-escape check — catches the rare case where input was
* under the cap but expanded above it (`<`-heavy bodies).
*/
function dataIsland(resourceUrl, jsonLdString) {
if (!jsonLdString) return '';
const raw = String(jsonLdString);
if (Buffer.byteLength(raw, 'utf8') > DATA_ISLAND_MAX_BYTES) return '';
const safeBody = escapeForScriptBlock(raw);
if (Buffer.byteLength(safeBody, 'utf8') > DATA_ISLAND_MAX_BYTES) return '';
const safeUri = escapeHtml(String(resourceUrl));
return `<script type="application/ld+json" id="dataisland" data-uri="${safeUri}">${safeBody}</script>`;
Comment on lines +67 to +74
}

/**
* Generate Mashlib databrowser HTML
*
* @param {string} resourceUrl - The URL of the resource being viewed (unused, kept for API compatibility)
* @param {string} resourceUrl - The URL of the resource being viewed
* @param {string} cdnVersion - If provided, load mashlib from unpkg CDN (e.g., "2.0.0")
* @param {object} [opts]
* @param {string|Buffer} [opts.embedJsonLd] - JSON-LD body to inline
* as a `<script type="application/ld+json">` data island. Accepts a
* UTF-8 string or a Buffer (coerced via `String()`). Honors a 256 KB
* size cap; oversize payloads are silently dropped. Phase 1 of #7.
Comment on lines +82 to +86
* @returns {string} HTML content
*/
export function generateDatabrowserHtml(resourceUrl, cdnVersion = null) {
export function generateDatabrowserHtml(resourceUrl, cdnVersion = null, opts = {}) {
const island = dataIsland(resourceUrl, opts.embedJsonLd);
if (cdnVersion) {
// CDN mode - use script.onload to ensure mashlib is fully loaded before init
// This avoids race conditions with defer + DOMContentLoaded
const cdnBase = `https://unpkg.com/mashlib@${cdnVersion}/dist`;
return `<!doctype html><html><head><meta charset="utf-8"/><title>SolidOS Web App</title>
<link href="${cdnBase}/mash.css" rel="stylesheet"></head>
<body id="PageBody"><header id="PageHeader"></header>
<body id="PageBody">${island}<header id="PageHeader"></header>
<div class="TabulatorOutline" id="DummyUUID" role="main"><table id="outline"></table><div id="GlobalDashboard"></div></div>
<footer id="PageFooter"></footer>
<script>
Expand All @@ -37,22 +111,27 @@ export function generateDatabrowserHtml(resourceUrl, cdnVersion = null) {
// Local mode - use defer (reliable when served locally)
return `<!doctype html><html><head><meta charset="utf-8"/><title>SolidOS Web App</title><script>document.addEventListener('DOMContentLoaded', function() {
panes.runDataBrowser()
})</script><script defer="defer" src="/mashlib.min.js"></script><link href="/mash.css" rel="stylesheet"></head><body id="PageBody"><header id="PageHeader"></header><div class="TabulatorOutline" id="DummyUUID" role="main"><table id="outline"></table><div id="GlobalDashboard"></div></div><footer id="PageFooter"></footer></body></html>`;
})</script><script defer="defer" src="/mashlib.min.js"></script><link href="/mash.css" rel="stylesheet"></head><body id="PageBody">${island}<header id="PageHeader"></header><div class="TabulatorOutline" id="DummyUUID" role="main"><table id="outline"></table><div id="GlobalDashboard"></div></div><footer id="PageFooter"></footer></body></html>`;
}

/**
* Generate ES module-based databrowser HTML
*
* @param {string} moduleUrl - URL to the ES module entry point
* @param {string} resourceUrl - The URL of the resource being viewed
* @param {object} [opts]
* @param {string|Buffer} [opts.embedJsonLd] - JSON-LD body for the
* data island, same contract as `generateDatabrowserHtml`. Phase 1 of #7.
* @returns {string} HTML content
*/
export function generateModuleDatabrowserHtml(moduleUrl) {
export function generateModuleDatabrowserHtml(moduleUrl, resourceUrl = '', opts = {}) {
const cssUrl = moduleUrl.replace(/\.js$/, '.css');
const island = dataIsland(resourceUrl, opts.embedJsonLd);
return `<!doctype html><html lang="en"><head><meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Solid Data Browser</title>
<link rel="stylesheet" href="${cssUrl}"></head>
<body><div id="mashlib"></div>
<body>${island}<div id="mashlib"></div>
<script type="module" src="${moduleUrl}"></script>
</body></html>`;
}
Expand Down
Loading

Back | FazBrowse Home | New Git URL