@moku-labs/web 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.d.mts +17 -1
- package/dist/browser.mjs +79 -1
- package/dist/index.cjs +213 -13
- package/dist/index.d.cts +17 -1
- package/dist/index.d.mts +17 -1
- package/dist/index.mjs +213 -14
- package/package.json +1 -1
package/dist/browser.d.mts
CHANGED
|
@@ -1734,6 +1734,13 @@ declare const headPlugin: import("@moku-labs/core").PluginInstance<"head", Confi
|
|
|
1734
1734
|
*/
|
|
1735
1735
|
declare function createComponent(name: string, hooks: ComponentHooks): ComponentDef;
|
|
1736
1736
|
//#endregion
|
|
1737
|
+
//#region src/plugins/spa/lazy-embed.d.ts
|
|
1738
|
+
/**
|
|
1739
|
+
* Lazy-embed island: facade button click → real `<iframe loading="lazy">`.
|
|
1740
|
+
* The companion of the content pipeline's `::embed` directive.
|
|
1741
|
+
*/
|
|
1742
|
+
declare const lazyEmbed: ComponentDef;
|
|
1743
|
+
//#endregion
|
|
1737
1744
|
//#region src/plugins/spa/index.d.ts
|
|
1738
1745
|
/**
|
|
1739
1746
|
* SPA plugin — progressive client-side navigation layered over the static site:
|
|
@@ -2098,6 +2105,15 @@ type FileSystemContentOptions = {
|
|
|
2098
2105
|
* (plus playwright with an installed browser). Defaults to disabled.
|
|
2099
2106
|
*/
|
|
2100
2107
|
mermaid?: boolean | MermaidDiagramOptions;
|
|
2108
|
+
/**
|
|
2109
|
+
* Lazy iframe embeds: rewrite `::embed{src="…" title="…"}` leaf directives
|
|
2110
|
+
* into static click-to-activate facades (no iframe — and none of the target's
|
|
2111
|
+
* network/JS cost — until the reader clicks). Pair with the `lazyEmbed` SPA
|
|
2112
|
+
* island, which swaps the facade for the real `<iframe loading="lazy">`.
|
|
2113
|
+
* Requires `trustedContent: true` (the facade is raw HTML the sanitize pass
|
|
2114
|
+
* would strip). Defaults to disabled.
|
|
2115
|
+
*/
|
|
2116
|
+
embed?: boolean;
|
|
2101
2117
|
};
|
|
2102
2118
|
/**
|
|
2103
2119
|
* Internal mutable state of the filesystem provider: the lazy unified processor and
|
|
@@ -2400,4 +2416,4 @@ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs
|
|
|
2400
2416
|
*/
|
|
2401
2417
|
declare const createPlugin: import("@moku-labs/core").BoundCreatePluginFunction<Config$5, Events, import("@moku-labs/core").CoreApisFromTuple<[import("@moku-labs/core").CorePluginInstance<"log", LogConfig, LogState, LogApi>, import("@moku-labs/core").CorePluginInstance<"env", EnvConfig, EnvState, EnvApi>]>>;
|
|
2402
2418
|
//#endregion
|
|
2403
|
-
export { types_d_exports as Content, types_d_exports$1 as Data, types_d_exports$2 as Env, types_d_exports$3 as Head, types_d_exports$4 as Log, types_d_exports$5 as Router, types_d_exports$6 as Spa, browserEnv, buildArticleHead, canonical, contentPlugin, createApp, createComponent, createPlugin, createUrls, dataPlugin, defineRoutes, envPlugin, feedLink, headPlugin, hreflang, i18nPlugin, jsonLd, logPlugin, meta, og, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|
|
2419
|
+
export { types_d_exports as Content, types_d_exports$1 as Data, types_d_exports$2 as Env, types_d_exports$3 as Head, types_d_exports$4 as Log, types_d_exports$5 as Router, types_d_exports$6 as Spa, browserEnv, buildArticleHead, canonical, contentPlugin, createApp, createComponent, createPlugin, createUrls, dataPlugin, defineRoutes, envPlugin, feedLink, headPlugin, hreflang, i18nPlugin, jsonLd, lazyEmbed, logPlugin, meta, og, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|
package/dist/browser.mjs
CHANGED
|
@@ -4428,6 +4428,84 @@ function disposeSpa() {
|
|
|
4428
4428
|
}
|
|
4429
4429
|
}
|
|
4430
4430
|
//#endregion
|
|
4431
|
+
//#region src/plugins/spa/lazy-embed.ts
|
|
4432
|
+
/**
|
|
4433
|
+
* @file `lazyEmbed` island — activates the static embed facades emitted by the
|
|
4434
|
+
* content pipeline's `::embed` directive (pipeline/embed.ts). Mounts on every
|
|
4435
|
+
* `[data-component="lazy-embed"]` figure; a click on the facade's button swaps
|
|
4436
|
+
* it for the real `<iframe loading="lazy">`. Until that click the embedded
|
|
4437
|
+
* document costs the page nothing — no request, no third-party JS, no
|
|
4438
|
+
* scroll-jacking. Register it in `pluginConfigs.spa.components`; all visual
|
|
4439
|
+
* chrome (`.lazy-embed*` classes) is consumer CSS.
|
|
4440
|
+
*/
|
|
4441
|
+
/** CSS class on the injected `<iframe>` (consumer CSS sizes it). */
|
|
4442
|
+
const EMBED_FRAME_CLASS = "lazy-embed-frame";
|
|
4443
|
+
/**
|
|
4444
|
+
* Swap a facade `<figure>`'s content for its real `<iframe>`. The iframe
|
|
4445
|
+
* carries `loading="lazy"` plus fullscreen permission, and the figure gains
|
|
4446
|
+
* `data-embed-active` so consumer CSS can restyle the activated state.
|
|
4447
|
+
*
|
|
4448
|
+
* @param figure - The facade element carrying `data-embed-src`/`data-embed-title`.
|
|
4449
|
+
* @example
|
|
4450
|
+
* ```ts
|
|
4451
|
+
* activateEmbed(figure);
|
|
4452
|
+
* ```
|
|
4453
|
+
*/
|
|
4454
|
+
function activateEmbed(figure) {
|
|
4455
|
+
const src = figure.dataset.embedSrc;
|
|
4456
|
+
if (!src) return;
|
|
4457
|
+
const iframe = document.createElement("iframe");
|
|
4458
|
+
iframe.src = src;
|
|
4459
|
+
iframe.title = figure.dataset.embedTitle ?? "";
|
|
4460
|
+
iframe.className = EMBED_FRAME_CLASS;
|
|
4461
|
+
iframe.setAttribute("loading", "lazy");
|
|
4462
|
+
iframe.allow = "fullscreen; autoplay; gamepad";
|
|
4463
|
+
iframe.allowFullscreen = true;
|
|
4464
|
+
figure.replaceChildren(iframe);
|
|
4465
|
+
figure.dataset.embedActive = "";
|
|
4466
|
+
}
|
|
4467
|
+
/**
|
|
4468
|
+
* Shared click handler (module-level so mount/unmount detach the same
|
|
4469
|
+
* reference): a click on the facade's button activates the embed.
|
|
4470
|
+
*
|
|
4471
|
+
* @param event - The click event from the facade figure.
|
|
4472
|
+
* @example
|
|
4473
|
+
* ```ts
|
|
4474
|
+
* element.addEventListener("click", onFacadeClick);
|
|
4475
|
+
* ```
|
|
4476
|
+
*/
|
|
4477
|
+
function onFacadeClick(event) {
|
|
4478
|
+
if (!event.target.closest("button.lazy-embed-button")) return;
|
|
4479
|
+
const figure = event.currentTarget;
|
|
4480
|
+
if (figure instanceof HTMLElement) activateEmbed(figure);
|
|
4481
|
+
}
|
|
4482
|
+
/**
|
|
4483
|
+
* Lazy-embed island: facade button click → real `<iframe loading="lazy">`.
|
|
4484
|
+
* The companion of the content pipeline's `::embed` directive.
|
|
4485
|
+
*/
|
|
4486
|
+
const lazyEmbed = createComponent("lazy-embed", {
|
|
4487
|
+
/**
|
|
4488
|
+
* Bind the activation click handler when a facade mounts.
|
|
4489
|
+
*
|
|
4490
|
+
* @param ctx - The island lifecycle context.
|
|
4491
|
+
* @example
|
|
4492
|
+
* onMount(ctx);
|
|
4493
|
+
*/
|
|
4494
|
+
onMount(ctx) {
|
|
4495
|
+
ctx.el.addEventListener("click", onFacadeClick);
|
|
4496
|
+
},
|
|
4497
|
+
/**
|
|
4498
|
+
* Remove the activation click handler when the facade is destroyed.
|
|
4499
|
+
*
|
|
4500
|
+
* @param ctx - The island lifecycle context.
|
|
4501
|
+
* @example
|
|
4502
|
+
* onDestroy(ctx);
|
|
4503
|
+
*/
|
|
4504
|
+
onDestroy(ctx) {
|
|
4505
|
+
ctx.el.removeEventListener("click", onFacadeClick);
|
|
4506
|
+
}
|
|
4507
|
+
});
|
|
4508
|
+
//#endregion
|
|
4431
4509
|
//#region src/plugins/spa/index.ts
|
|
4432
4510
|
/**
|
|
4433
4511
|
* @file spa — Complex Plugin (WIRING ONLY, ≤30 lines). All logic lives in the
|
|
@@ -5115,4 +5193,4 @@ const createApp = core.createApp;
|
|
|
5115
5193
|
*/
|
|
5116
5194
|
const createPlugin = core.createPlugin;
|
|
5117
5195
|
//#endregion
|
|
5118
|
-
export { types_exports as Content, types_exports$1 as Data, types_exports$2 as Env, types_exports$3 as Head, types_exports$4 as Log, types_exports$5 as Router, types_exports$6 as Spa, browserEnv, buildArticleHead, canonical, contentPlugin, createApp, createComponent, createPlugin, createUrls, dataPlugin, defineRoutes, envPlugin, feedLink, headPlugin, hreflang, i18nPlugin, jsonLd, logPlugin, meta, og, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|
|
5196
|
+
export { types_exports as Content, types_exports$1 as Data, types_exports$2 as Env, types_exports$3 as Head, types_exports$4 as Log, types_exports$5 as Router, types_exports$6 as Spa, browserEnv, buildArticleHead, canonical, contentPlugin, createApp, createComponent, createPlugin, createUrls, dataPlugin, defineRoutes, envPlugin, feedLink, headPlugin, hreflang, i18nPlugin, jsonLd, lazyEmbed, logPlugin, meta, og, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|
package/dist/index.cjs
CHANGED
|
@@ -1582,13 +1582,14 @@ function validateContentConfig(config) {
|
|
|
1582
1582
|
}
|
|
1583
1583
|
/**
|
|
1584
1584
|
* Validates the `fileSystemContent` provider options (fail-fast at provider
|
|
1585
|
-
* construction). Throws when `mermaid` is enabled without
|
|
1586
|
-
*
|
|
1587
|
-
* XSS boundary) would strip — so
|
|
1588
|
-
* `[web]` prefix.
|
|
1585
|
+
* construction). Throws when `mermaid` or `embed` is enabled without
|
|
1586
|
+
* `trustedContent: true`: both emit raw HTML (inline SVG / the embed facade),
|
|
1587
|
+
* which the sanitize pass (the untrusted-content XSS boundary) would strip — so
|
|
1588
|
+
* the combination can never work. Errors use the `[web]` prefix.
|
|
1589
1589
|
*
|
|
1590
1590
|
* @param options - The provider options to validate.
|
|
1591
|
-
* @throws {Error} If `mermaid` is enabled while `trustedContent` is
|
|
1591
|
+
* @throws {Error} If `mermaid` or `embed` is enabled while `trustedContent` is
|
|
1592
|
+
* not `true`.
|
|
1592
1593
|
* @example
|
|
1593
1594
|
* ```ts
|
|
1594
1595
|
* validateFileSystemContentOptions({ contentDir: "./content", trustedContent: true, mermaid: true });
|
|
@@ -1596,6 +1597,7 @@ function validateContentConfig(config) {
|
|
|
1596
1597
|
*/
|
|
1597
1598
|
function validateFileSystemContentOptions(options) {
|
|
1598
1599
|
if (Boolean(options.mermaid) && options.trustedContent !== true) throw new Error("[web] content: `mermaid` requires `trustedContent: true`.\n Mermaid diagrams render to raw inline SVG, which the sanitize pass would strip.\n Set trustedContent: true ONLY for fully author-controlled Markdown.");
|
|
1600
|
+
if (Boolean(options.embed) && options.trustedContent !== true) throw new Error("[web] content: `embed` requires `trustedContent: true`.\n Embed directives render to a raw-HTML facade, which the sanitize pass would strip\n (and embedding third-party iframes is never safe for untrusted Markdown).\n Set trustedContent: true ONLY for fully author-controlled Markdown.");
|
|
1599
1601
|
}
|
|
1600
1602
|
//#endregion
|
|
1601
1603
|
//#region src/plugins/content/index.ts
|
|
@@ -11312,6 +11314,84 @@ function disposeSpa() {
|
|
|
11312
11314
|
}
|
|
11313
11315
|
}
|
|
11314
11316
|
//#endregion
|
|
11317
|
+
//#region src/plugins/spa/lazy-embed.ts
|
|
11318
|
+
/**
|
|
11319
|
+
* @file `lazyEmbed` island — activates the static embed facades emitted by the
|
|
11320
|
+
* content pipeline's `::embed` directive (pipeline/embed.ts). Mounts on every
|
|
11321
|
+
* `[data-component="lazy-embed"]` figure; a click on the facade's button swaps
|
|
11322
|
+
* it for the real `<iframe loading="lazy">`. Until that click the embedded
|
|
11323
|
+
* document costs the page nothing — no request, no third-party JS, no
|
|
11324
|
+
* scroll-jacking. Register it in `pluginConfigs.spa.components`; all visual
|
|
11325
|
+
* chrome (`.lazy-embed*` classes) is consumer CSS.
|
|
11326
|
+
*/
|
|
11327
|
+
/** CSS class on the injected `<iframe>` (consumer CSS sizes it). */
|
|
11328
|
+
const EMBED_FRAME_CLASS = "lazy-embed-frame";
|
|
11329
|
+
/**
|
|
11330
|
+
* Swap a facade `<figure>`'s content for its real `<iframe>`. The iframe
|
|
11331
|
+
* carries `loading="lazy"` plus fullscreen permission, and the figure gains
|
|
11332
|
+
* `data-embed-active` so consumer CSS can restyle the activated state.
|
|
11333
|
+
*
|
|
11334
|
+
* @param figure - The facade element carrying `data-embed-src`/`data-embed-title`.
|
|
11335
|
+
* @example
|
|
11336
|
+
* ```ts
|
|
11337
|
+
* activateEmbed(figure);
|
|
11338
|
+
* ```
|
|
11339
|
+
*/
|
|
11340
|
+
function activateEmbed(figure) {
|
|
11341
|
+
const src = figure.dataset.embedSrc;
|
|
11342
|
+
if (!src) return;
|
|
11343
|
+
const iframe = document.createElement("iframe");
|
|
11344
|
+
iframe.src = src;
|
|
11345
|
+
iframe.title = figure.dataset.embedTitle ?? "";
|
|
11346
|
+
iframe.className = EMBED_FRAME_CLASS;
|
|
11347
|
+
iframe.setAttribute("loading", "lazy");
|
|
11348
|
+
iframe.allow = "fullscreen; autoplay; gamepad";
|
|
11349
|
+
iframe.allowFullscreen = true;
|
|
11350
|
+
figure.replaceChildren(iframe);
|
|
11351
|
+
figure.dataset.embedActive = "";
|
|
11352
|
+
}
|
|
11353
|
+
/**
|
|
11354
|
+
* Shared click handler (module-level so mount/unmount detach the same
|
|
11355
|
+
* reference): a click on the facade's button activates the embed.
|
|
11356
|
+
*
|
|
11357
|
+
* @param event - The click event from the facade figure.
|
|
11358
|
+
* @example
|
|
11359
|
+
* ```ts
|
|
11360
|
+
* element.addEventListener("click", onFacadeClick);
|
|
11361
|
+
* ```
|
|
11362
|
+
*/
|
|
11363
|
+
function onFacadeClick(event) {
|
|
11364
|
+
if (!event.target.closest("button.lazy-embed-button")) return;
|
|
11365
|
+
const figure = event.currentTarget;
|
|
11366
|
+
if (figure instanceof HTMLElement) activateEmbed(figure);
|
|
11367
|
+
}
|
|
11368
|
+
/**
|
|
11369
|
+
* Lazy-embed island: facade button click → real `<iframe loading="lazy">`.
|
|
11370
|
+
* The companion of the content pipeline's `::embed` directive.
|
|
11371
|
+
*/
|
|
11372
|
+
const lazyEmbed = createComponent("lazy-embed", {
|
|
11373
|
+
/**
|
|
11374
|
+
* Bind the activation click handler when a facade mounts.
|
|
11375
|
+
*
|
|
11376
|
+
* @param ctx - The island lifecycle context.
|
|
11377
|
+
* @example
|
|
11378
|
+
* onMount(ctx);
|
|
11379
|
+
*/
|
|
11380
|
+
onMount(ctx) {
|
|
11381
|
+
ctx.el.addEventListener("click", onFacadeClick);
|
|
11382
|
+
},
|
|
11383
|
+
/**
|
|
11384
|
+
* Remove the activation click handler when the facade is destroyed.
|
|
11385
|
+
*
|
|
11386
|
+
* @param ctx - The island lifecycle context.
|
|
11387
|
+
* @example
|
|
11388
|
+
* onDestroy(ctx);
|
|
11389
|
+
*/
|
|
11390
|
+
onDestroy(ctx) {
|
|
11391
|
+
ctx.el.removeEventListener("click", onFacadeClick);
|
|
11392
|
+
}
|
|
11393
|
+
});
|
|
11394
|
+
//#endregion
|
|
11315
11395
|
//#region src/plugins/spa/index.ts
|
|
11316
11396
|
/**
|
|
11317
11397
|
* @file spa — Complex Plugin (WIRING ONLY, ≤30 lines). All logic lives in the
|
|
@@ -11586,6 +11666,121 @@ function parseFrontmatter(raw, config) {
|
|
|
11586
11666
|
};
|
|
11587
11667
|
}
|
|
11588
11668
|
//#endregion
|
|
11669
|
+
//#region src/plugins/content/pipeline/embed.ts
|
|
11670
|
+
/** CSS class on the `<figure>` facade wrapping each embed. */
|
|
11671
|
+
const EMBED_FIGURE_CLASS = "lazy-embed";
|
|
11672
|
+
/** `data-component` name binding the facade to the `lazyEmbed` SPA island. */
|
|
11673
|
+
const EMBED_COMPONENT_NAME = "lazy-embed";
|
|
11674
|
+
/** CSS class on the facade's activation button. */
|
|
11675
|
+
const EMBED_BUTTON_CLASS = "lazy-embed-button";
|
|
11676
|
+
/** CSS class on the title span inside the activation button. */
|
|
11677
|
+
const EMBED_TITLE_CLASS = "lazy-embed-title";
|
|
11678
|
+
/**
|
|
11679
|
+
* Type guard for an `::embed` leaf directive.
|
|
11680
|
+
*
|
|
11681
|
+
* @param node - AST node to test.
|
|
11682
|
+
* @returns `true` when the node is an `::embed` leaf directive.
|
|
11683
|
+
* @example
|
|
11684
|
+
* ```ts
|
|
11685
|
+
* if (isEmbedDirective(node)) console.log(node.attributes?.src);
|
|
11686
|
+
* ```
|
|
11687
|
+
*/
|
|
11688
|
+
function isEmbedDirective(node) {
|
|
11689
|
+
return node.type === "leafDirective" && node.name === "embed";
|
|
11690
|
+
}
|
|
11691
|
+
/**
|
|
11692
|
+
* Escape a string for safe interpolation into a double-quoted HTML attribute.
|
|
11693
|
+
*
|
|
11694
|
+
* @param value - The raw attribute value.
|
|
11695
|
+
* @returns The escaped value.
|
|
11696
|
+
* @example
|
|
11697
|
+
* ```ts
|
|
11698
|
+
* escapeAttribute('He said "hi" & left'); // "He said "hi" & left"
|
|
11699
|
+
* ```
|
|
11700
|
+
*/
|
|
11701
|
+
function escapeAttribute(value) {
|
|
11702
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
|
|
11703
|
+
}
|
|
11704
|
+
/**
|
|
11705
|
+
* Validate an embed `src` URL: only `https:`/`http:` absolute URLs and
|
|
11706
|
+
* root-relative paths are embeddable — anything else (`javascript:`, `data:`,
|
|
11707
|
+
* scheme-relative, …) fails the build.
|
|
11708
|
+
*
|
|
11709
|
+
* @param src - The raw `src` attribute value.
|
|
11710
|
+
* @returns `true` when the URL is embeddable.
|
|
11711
|
+
* @example
|
|
11712
|
+
* ```ts
|
|
11713
|
+
* isEmbeddableUrl("https://game.example.com/"); // true
|
|
11714
|
+
* isEmbeddableUrl("javascript:alert(1)"); // false
|
|
11715
|
+
* ```
|
|
11716
|
+
*/
|
|
11717
|
+
function isEmbeddableUrl(src) {
|
|
11718
|
+
if (src.startsWith("/") && !src.startsWith("//")) return true;
|
|
11719
|
+
return /^https?:\/\//i.test(src);
|
|
11720
|
+
}
|
|
11721
|
+
/**
|
|
11722
|
+
* Build the static facade HTML for one embed: the `<figure>` carrying the
|
|
11723
|
+
* target in data attributes plus the activation `<button>` (the button label
|
|
11724
|
+
* is the embed's title; visual chrome is consumer CSS).
|
|
11725
|
+
*
|
|
11726
|
+
* @param src - The validated embed URL.
|
|
11727
|
+
* @param title - The human-readable embed title (button label, iframe title).
|
|
11728
|
+
* @returns The facade HTML string.
|
|
11729
|
+
* @example
|
|
11730
|
+
* ```ts
|
|
11731
|
+
* embedFacadeHtml("https://game.example.com/", "My Game");
|
|
11732
|
+
* ```
|
|
11733
|
+
*/
|
|
11734
|
+
function embedFacadeHtml(src, title) {
|
|
11735
|
+
const safeSource = escapeAttribute(src);
|
|
11736
|
+
const safeTitle = escapeAttribute(title);
|
|
11737
|
+
return `<figure class="${EMBED_FIGURE_CLASS}" data-component="${EMBED_COMPONENT_NAME}" data-embed-src="${safeSource}" data-embed-title="${safeTitle}"><button type="button" class="${EMBED_BUTTON_CLASS}" aria-label="Load embed: ${safeTitle}"><span class="${EMBED_TITLE_CLASS}">${safeTitle}</span></button></figure>`;
|
|
11738
|
+
}
|
|
11739
|
+
/**
|
|
11740
|
+
* Mdast transformer rewriting every `::embed` leaf directive to its facade
|
|
11741
|
+
* HTML node. A directive missing `src`/`title`, or carrying a non-embeddable
|
|
11742
|
+
* URL, fails the build with the offending value quoted.
|
|
11743
|
+
*
|
|
11744
|
+
* @param tree - The mdast tree to mutate.
|
|
11745
|
+
* @throws {Error} When an `::embed` directive is missing `src` or `title`, or
|
|
11746
|
+
* its `src` is not an embeddable URL.
|
|
11747
|
+
* @example
|
|
11748
|
+
* ```ts
|
|
11749
|
+
* embedTransform(tree);
|
|
11750
|
+
* ```
|
|
11751
|
+
*/
|
|
11752
|
+
function embedTransform(tree) {
|
|
11753
|
+
(0, unist_util_visit.visit)(tree, (node, index, parent) => {
|
|
11754
|
+
if (!isEmbedDirective(node)) return;
|
|
11755
|
+
if (parent === void 0 || index === void 0) return;
|
|
11756
|
+
const src = node.attributes?.src ?? "";
|
|
11757
|
+
const title = node.attributes?.title ?? "";
|
|
11758
|
+
if (src === "" || title === "") throw new Error("[web] content: `::embed` requires both `src` and `title` attributes, e.g. ::embed{src=\"https://…\" title=\"…\"}.");
|
|
11759
|
+
if (!isEmbeddableUrl(src)) throw new Error(`[web] content: \`::embed\` src must be an http(s) URL or a root-relative path (got "${src}").`);
|
|
11760
|
+
const html = {
|
|
11761
|
+
type: "html",
|
|
11762
|
+
value: embedFacadeHtml(src, title)
|
|
11763
|
+
};
|
|
11764
|
+
parent.children[index] = html;
|
|
11765
|
+
});
|
|
11766
|
+
}
|
|
11767
|
+
/**
|
|
11768
|
+
* Remark transform: rewrites `::embed{src="…" title="…"}` leaf directives into
|
|
11769
|
+
* static click-to-activate facades (no iframe until the reader clicks — see
|
|
11770
|
+
* the file header). Opt-in via the provider's `embed` option; requires
|
|
11771
|
+
* `trustedContent: true` because the facade is raw HTML the sanitize pass
|
|
11772
|
+
* would strip.
|
|
11773
|
+
*
|
|
11774
|
+
* @returns An mdast tree transformer.
|
|
11775
|
+
* @example
|
|
11776
|
+
* ```ts
|
|
11777
|
+
* unified().use(embedPlugin);
|
|
11778
|
+
* ```
|
|
11779
|
+
*/
|
|
11780
|
+
function embedPlugin() {
|
|
11781
|
+
return embedTransform;
|
|
11782
|
+
}
|
|
11783
|
+
//#endregion
|
|
11589
11784
|
//#region src/plugins/content/pipeline/mermaid.ts
|
|
11590
11785
|
/** CSS class on the `<figure>` wrapper around each rendered diagram. */
|
|
11591
11786
|
const MERMAID_FIGURE_CLASS = "mermaid-diagram";
|
|
@@ -11869,14 +12064,17 @@ function sectionDividerPlugin() {
|
|
|
11869
12064
|
}
|
|
11870
12065
|
/**
|
|
11871
12066
|
* The hardcoded framework default remark (Markdown-AST) plugins, in order:
|
|
11872
|
-
* parse, frontmatter, gfm, directive, pull-quote, the OPT-IN mermaid
|
|
11873
|
-
* then the mdast→hast bridge (`remark-rehype` with
|
|
11874
|
-
* Pull-quote and mermaid run on the mdast before
|
|
11875
|
-
* directive carries its `hName`/`hProperties`,
|
|
11876
|
-
*
|
|
11877
|
-
*
|
|
11878
|
-
*
|
|
11879
|
-
*
|
|
12067
|
+
* parse, frontmatter, gfm, directive, pull-quote, the OPT-IN embed + mermaid
|
|
12068
|
+
* transforms, then the mdast→hast bridge (`remark-rehype` with
|
|
12069
|
+
* `allowDangerousHtml`). Pull-quote, embed and mermaid run on the mdast before
|
|
12070
|
+
* the bridge — pull-quote so the directive carries its `hName`/`hProperties`,
|
|
12071
|
+
* embed so the directive is replaced with its raw facade HTML, mermaid so the
|
|
12072
|
+
* fence is replaced with raw SVG HTML before Shiki could ever claim the code
|
|
12073
|
+
* block.
|
|
12074
|
+
*
|
|
12075
|
+
* @param config - Optional provider configuration; only the opt-in flags are
|
|
12076
|
+
* read here: `mermaid` and `embed` (each truthy value enables its transform at
|
|
12077
|
+
* a fixed mdast position).
|
|
11880
12078
|
* @returns The ordered default remark pluggables.
|
|
11881
12079
|
* @example
|
|
11882
12080
|
* ```ts
|
|
@@ -11891,6 +12089,7 @@ function defaultRemarkPlugins(config) {
|
|
|
11891
12089
|
remark_directive.default,
|
|
11892
12090
|
pullQuotePlugin
|
|
11893
12091
|
];
|
|
12092
|
+
if (config?.embed) plugins.push(embedPlugin);
|
|
11894
12093
|
if (config?.mermaid) plugins.push([remarkMermaidDiagrams, normalizeMermaidOptions(config.mermaid)]);
|
|
11895
12094
|
plugins.push([remark_rehype.default, { allowDangerousHtml: true }]);
|
|
11896
12095
|
return plugins;
|
|
@@ -12446,6 +12645,7 @@ exports.headPlugin = headPlugin;
|
|
|
12446
12645
|
exports.hreflang = hreflang;
|
|
12447
12646
|
exports.i18nPlugin = i18nPlugin;
|
|
12448
12647
|
exports.jsonLd = jsonLd;
|
|
12648
|
+
exports.lazyEmbed = lazyEmbed;
|
|
12449
12649
|
exports.logPlugin = logPlugin;
|
|
12450
12650
|
exports.meta = meta;
|
|
12451
12651
|
exports.og = og;
|
package/dist/index.d.cts
CHANGED
|
@@ -2849,6 +2849,15 @@ type FileSystemContentOptions = {
|
|
|
2849
2849
|
* (plus playwright with an installed browser). Defaults to disabled.
|
|
2850
2850
|
*/
|
|
2851
2851
|
mermaid?: boolean | MermaidDiagramOptions;
|
|
2852
|
+
/**
|
|
2853
|
+
* Lazy iframe embeds: rewrite `::embed{src="…" title="…"}` leaf directives
|
|
2854
|
+
* into static click-to-activate facades (no iframe — and none of the target's
|
|
2855
|
+
* network/JS cost — until the reader clicks). Pair with the `lazyEmbed` SPA
|
|
2856
|
+
* island, which swaps the facade for the real `<iframe loading="lazy">`.
|
|
2857
|
+
* Requires `trustedContent: true` (the facade is raw HTML the sanitize pass
|
|
2858
|
+
* would strip). Defaults to disabled.
|
|
2859
|
+
*/
|
|
2860
|
+
embed?: boolean;
|
|
2852
2861
|
};
|
|
2853
2862
|
/**
|
|
2854
2863
|
* Internal mutable state of the filesystem provider: the lazy unified processor and
|
|
@@ -3486,6 +3495,13 @@ declare const sitePlugin: import("@moku-labs/core").PluginInstance<"site", Confi
|
|
|
3486
3495
|
*/
|
|
3487
3496
|
declare function createComponent(name: string, hooks: ComponentHooks): ComponentDef;
|
|
3488
3497
|
//#endregion
|
|
3498
|
+
//#region src/plugins/spa/lazy-embed.d.ts
|
|
3499
|
+
/**
|
|
3500
|
+
* Lazy-embed island: facade button click → real `<iframe loading="lazy">`.
|
|
3501
|
+
* The companion of the content pipeline's `::embed` directive.
|
|
3502
|
+
*/
|
|
3503
|
+
declare const lazyEmbed: ComponentDef;
|
|
3504
|
+
//#endregion
|
|
3489
3505
|
//#region src/plugins/spa/index.d.ts
|
|
3490
3506
|
/**
|
|
3491
3507
|
* SPA plugin — progressive client-side navigation layered over the static site:
|
|
@@ -3687,4 +3703,4 @@ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs
|
|
|
3687
3703
|
*/
|
|
3688
3704
|
declare const createPlugin: import("@moku-labs/core").BoundCreatePluginFunction<Config$8, Events, import("@moku-labs/core").CoreApisFromTuple<[import("@moku-labs/core").CorePluginInstance<"log", LogConfig, LogState, LogApi>, import("@moku-labs/core").CorePluginInstance<"env", EnvConfig, EnvState, EnvApi>]>>;
|
|
3689
3705
|
//#endregion
|
|
3690
|
-
export { types_d_exports as Build, types_d_exports$1 as Cli, types_d_exports$2 as Content, types_d_exports$3 as Data, types_d_exports$4 as Deploy, types_d_exports$5 as Env, types_d_exports$6 as Head, types_d_exports$7 as Log, types_d_exports$8 as Router, types_d_exports$9 as Spa, browserEnv, buildArticleHead, buildPlugin, canonical, cliPlugin, cloudflareBindings, contentPlugin, createApp, createComponent, createPlugin, createUrls, dataPlugin, defineRoutes, deployPlugin, dotenv, envPlugin, feedLink, fileSystemContent, headPlugin, hreflang, i18nPlugin, jsonLd, logPlugin, meta, og, processEnv, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|
|
3706
|
+
export { types_d_exports as Build, types_d_exports$1 as Cli, types_d_exports$2 as Content, types_d_exports$3 as Data, types_d_exports$4 as Deploy, types_d_exports$5 as Env, types_d_exports$6 as Head, types_d_exports$7 as Log, types_d_exports$8 as Router, types_d_exports$9 as Spa, browserEnv, buildArticleHead, buildPlugin, canonical, cliPlugin, cloudflareBindings, contentPlugin, createApp, createComponent, createPlugin, createUrls, dataPlugin, defineRoutes, deployPlugin, dotenv, envPlugin, feedLink, fileSystemContent, headPlugin, hreflang, i18nPlugin, jsonLd, lazyEmbed, logPlugin, meta, og, processEnv, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|
package/dist/index.d.mts
CHANGED
|
@@ -2849,6 +2849,15 @@ type FileSystemContentOptions = {
|
|
|
2849
2849
|
* (plus playwright with an installed browser). Defaults to disabled.
|
|
2850
2850
|
*/
|
|
2851
2851
|
mermaid?: boolean | MermaidDiagramOptions;
|
|
2852
|
+
/**
|
|
2853
|
+
* Lazy iframe embeds: rewrite `::embed{src="…" title="…"}` leaf directives
|
|
2854
|
+
* into static click-to-activate facades (no iframe — and none of the target's
|
|
2855
|
+
* network/JS cost — until the reader clicks). Pair with the `lazyEmbed` SPA
|
|
2856
|
+
* island, which swaps the facade for the real `<iframe loading="lazy">`.
|
|
2857
|
+
* Requires `trustedContent: true` (the facade is raw HTML the sanitize pass
|
|
2858
|
+
* would strip). Defaults to disabled.
|
|
2859
|
+
*/
|
|
2860
|
+
embed?: boolean;
|
|
2852
2861
|
};
|
|
2853
2862
|
/**
|
|
2854
2863
|
* Internal mutable state of the filesystem provider: the lazy unified processor and
|
|
@@ -3486,6 +3495,13 @@ declare const sitePlugin: import("@moku-labs/core").PluginInstance<"site", Confi
|
|
|
3486
3495
|
*/
|
|
3487
3496
|
declare function createComponent(name: string, hooks: ComponentHooks): ComponentDef;
|
|
3488
3497
|
//#endregion
|
|
3498
|
+
//#region src/plugins/spa/lazy-embed.d.ts
|
|
3499
|
+
/**
|
|
3500
|
+
* Lazy-embed island: facade button click → real `<iframe loading="lazy">`.
|
|
3501
|
+
* The companion of the content pipeline's `::embed` directive.
|
|
3502
|
+
*/
|
|
3503
|
+
declare const lazyEmbed: ComponentDef;
|
|
3504
|
+
//#endregion
|
|
3489
3505
|
//#region src/plugins/spa/index.d.ts
|
|
3490
3506
|
/**
|
|
3491
3507
|
* SPA plugin — progressive client-side navigation layered over the static site:
|
|
@@ -3687,4 +3703,4 @@ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs
|
|
|
3687
3703
|
*/
|
|
3688
3704
|
declare const createPlugin: import("@moku-labs/core").BoundCreatePluginFunction<Config$8, Events, import("@moku-labs/core").CoreApisFromTuple<[import("@moku-labs/core").CorePluginInstance<"log", LogConfig, LogState, LogApi>, import("@moku-labs/core").CorePluginInstance<"env", EnvConfig, EnvState, EnvApi>]>>;
|
|
3689
3705
|
//#endregion
|
|
3690
|
-
export { types_d_exports as Build, types_d_exports$1 as Cli, types_d_exports$2 as Content, types_d_exports$3 as Data, types_d_exports$4 as Deploy, types_d_exports$5 as Env, types_d_exports$6 as Head, types_d_exports$7 as Log, types_d_exports$8 as Router, types_d_exports$9 as Spa, browserEnv, buildArticleHead, buildPlugin, canonical, cliPlugin, cloudflareBindings, contentPlugin, createApp, createComponent, createPlugin, createUrls, dataPlugin, defineRoutes, deployPlugin, dotenv, envPlugin, feedLink, fileSystemContent, headPlugin, hreflang, i18nPlugin, jsonLd, logPlugin, meta, og, processEnv, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|
|
3706
|
+
export { types_d_exports as Build, types_d_exports$1 as Cli, types_d_exports$2 as Content, types_d_exports$3 as Data, types_d_exports$4 as Deploy, types_d_exports$5 as Env, types_d_exports$6 as Head, types_d_exports$7 as Log, types_d_exports$8 as Router, types_d_exports$9 as Spa, browserEnv, buildArticleHead, buildPlugin, canonical, cliPlugin, cloudflareBindings, contentPlugin, createApp, createComponent, createPlugin, createUrls, dataPlugin, defineRoutes, deployPlugin, dotenv, envPlugin, feedLink, fileSystemContent, headPlugin, hreflang, i18nPlugin, jsonLd, lazyEmbed, logPlugin, meta, og, processEnv, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|
package/dist/index.mjs
CHANGED
|
@@ -1569,13 +1569,14 @@ function validateContentConfig(config) {
|
|
|
1569
1569
|
}
|
|
1570
1570
|
/**
|
|
1571
1571
|
* Validates the `fileSystemContent` provider options (fail-fast at provider
|
|
1572
|
-
* construction). Throws when `mermaid` is enabled without
|
|
1573
|
-
*
|
|
1574
|
-
* XSS boundary) would strip — so
|
|
1575
|
-
* `[web]` prefix.
|
|
1572
|
+
* construction). Throws when `mermaid` or `embed` is enabled without
|
|
1573
|
+
* `trustedContent: true`: both emit raw HTML (inline SVG / the embed facade),
|
|
1574
|
+
* which the sanitize pass (the untrusted-content XSS boundary) would strip — so
|
|
1575
|
+
* the combination can never work. Errors use the `[web]` prefix.
|
|
1576
1576
|
*
|
|
1577
1577
|
* @param options - The provider options to validate.
|
|
1578
|
-
* @throws {Error} If `mermaid` is enabled while `trustedContent` is
|
|
1578
|
+
* @throws {Error} If `mermaid` or `embed` is enabled while `trustedContent` is
|
|
1579
|
+
* not `true`.
|
|
1579
1580
|
* @example
|
|
1580
1581
|
* ```ts
|
|
1581
1582
|
* validateFileSystemContentOptions({ contentDir: "./content", trustedContent: true, mermaid: true });
|
|
@@ -1583,6 +1584,7 @@ function validateContentConfig(config) {
|
|
|
1583
1584
|
*/
|
|
1584
1585
|
function validateFileSystemContentOptions(options) {
|
|
1585
1586
|
if (Boolean(options.mermaid) && options.trustedContent !== true) throw new Error("[web] content: `mermaid` requires `trustedContent: true`.\n Mermaid diagrams render to raw inline SVG, which the sanitize pass would strip.\n Set trustedContent: true ONLY for fully author-controlled Markdown.");
|
|
1587
|
+
if (Boolean(options.embed) && options.trustedContent !== true) throw new Error("[web] content: `embed` requires `trustedContent: true`.\n Embed directives render to a raw-HTML facade, which the sanitize pass would strip\n (and embedding third-party iframes is never safe for untrusted Markdown).\n Set trustedContent: true ONLY for fully author-controlled Markdown.");
|
|
1586
1588
|
}
|
|
1587
1589
|
//#endregion
|
|
1588
1590
|
//#region src/plugins/content/index.ts
|
|
@@ -11299,6 +11301,84 @@ function disposeSpa() {
|
|
|
11299
11301
|
}
|
|
11300
11302
|
}
|
|
11301
11303
|
//#endregion
|
|
11304
|
+
//#region src/plugins/spa/lazy-embed.ts
|
|
11305
|
+
/**
|
|
11306
|
+
* @file `lazyEmbed` island — activates the static embed facades emitted by the
|
|
11307
|
+
* content pipeline's `::embed` directive (pipeline/embed.ts). Mounts on every
|
|
11308
|
+
* `[data-component="lazy-embed"]` figure; a click on the facade's button swaps
|
|
11309
|
+
* it for the real `<iframe loading="lazy">`. Until that click the embedded
|
|
11310
|
+
* document costs the page nothing — no request, no third-party JS, no
|
|
11311
|
+
* scroll-jacking. Register it in `pluginConfigs.spa.components`; all visual
|
|
11312
|
+
* chrome (`.lazy-embed*` classes) is consumer CSS.
|
|
11313
|
+
*/
|
|
11314
|
+
/** CSS class on the injected `<iframe>` (consumer CSS sizes it). */
|
|
11315
|
+
const EMBED_FRAME_CLASS = "lazy-embed-frame";
|
|
11316
|
+
/**
|
|
11317
|
+
* Swap a facade `<figure>`'s content for its real `<iframe>`. The iframe
|
|
11318
|
+
* carries `loading="lazy"` plus fullscreen permission, and the figure gains
|
|
11319
|
+
* `data-embed-active` so consumer CSS can restyle the activated state.
|
|
11320
|
+
*
|
|
11321
|
+
* @param figure - The facade element carrying `data-embed-src`/`data-embed-title`.
|
|
11322
|
+
* @example
|
|
11323
|
+
* ```ts
|
|
11324
|
+
* activateEmbed(figure);
|
|
11325
|
+
* ```
|
|
11326
|
+
*/
|
|
11327
|
+
function activateEmbed(figure) {
|
|
11328
|
+
const src = figure.dataset.embedSrc;
|
|
11329
|
+
if (!src) return;
|
|
11330
|
+
const iframe = document.createElement("iframe");
|
|
11331
|
+
iframe.src = src;
|
|
11332
|
+
iframe.title = figure.dataset.embedTitle ?? "";
|
|
11333
|
+
iframe.className = EMBED_FRAME_CLASS;
|
|
11334
|
+
iframe.setAttribute("loading", "lazy");
|
|
11335
|
+
iframe.allow = "fullscreen; autoplay; gamepad";
|
|
11336
|
+
iframe.allowFullscreen = true;
|
|
11337
|
+
figure.replaceChildren(iframe);
|
|
11338
|
+
figure.dataset.embedActive = "";
|
|
11339
|
+
}
|
|
11340
|
+
/**
|
|
11341
|
+
* Shared click handler (module-level so mount/unmount detach the same
|
|
11342
|
+
* reference): a click on the facade's button activates the embed.
|
|
11343
|
+
*
|
|
11344
|
+
* @param event - The click event from the facade figure.
|
|
11345
|
+
* @example
|
|
11346
|
+
* ```ts
|
|
11347
|
+
* element.addEventListener("click", onFacadeClick);
|
|
11348
|
+
* ```
|
|
11349
|
+
*/
|
|
11350
|
+
function onFacadeClick(event) {
|
|
11351
|
+
if (!event.target.closest("button.lazy-embed-button")) return;
|
|
11352
|
+
const figure = event.currentTarget;
|
|
11353
|
+
if (figure instanceof HTMLElement) activateEmbed(figure);
|
|
11354
|
+
}
|
|
11355
|
+
/**
|
|
11356
|
+
* Lazy-embed island: facade button click → real `<iframe loading="lazy">`.
|
|
11357
|
+
* The companion of the content pipeline's `::embed` directive.
|
|
11358
|
+
*/
|
|
11359
|
+
const lazyEmbed = createComponent("lazy-embed", {
|
|
11360
|
+
/**
|
|
11361
|
+
* Bind the activation click handler when a facade mounts.
|
|
11362
|
+
*
|
|
11363
|
+
* @param ctx - The island lifecycle context.
|
|
11364
|
+
* @example
|
|
11365
|
+
* onMount(ctx);
|
|
11366
|
+
*/
|
|
11367
|
+
onMount(ctx) {
|
|
11368
|
+
ctx.el.addEventListener("click", onFacadeClick);
|
|
11369
|
+
},
|
|
11370
|
+
/**
|
|
11371
|
+
* Remove the activation click handler when the facade is destroyed.
|
|
11372
|
+
*
|
|
11373
|
+
* @param ctx - The island lifecycle context.
|
|
11374
|
+
* @example
|
|
11375
|
+
* onDestroy(ctx);
|
|
11376
|
+
*/
|
|
11377
|
+
onDestroy(ctx) {
|
|
11378
|
+
ctx.el.removeEventListener("click", onFacadeClick);
|
|
11379
|
+
}
|
|
11380
|
+
});
|
|
11381
|
+
//#endregion
|
|
11302
11382
|
//#region src/plugins/spa/index.ts
|
|
11303
11383
|
/**
|
|
11304
11384
|
* @file spa — Complex Plugin (WIRING ONLY, ≤30 lines). All logic lives in the
|
|
@@ -11573,6 +11653,121 @@ function parseFrontmatter(raw, config) {
|
|
|
11573
11653
|
};
|
|
11574
11654
|
}
|
|
11575
11655
|
//#endregion
|
|
11656
|
+
//#region src/plugins/content/pipeline/embed.ts
|
|
11657
|
+
/** CSS class on the `<figure>` facade wrapping each embed. */
|
|
11658
|
+
const EMBED_FIGURE_CLASS = "lazy-embed";
|
|
11659
|
+
/** `data-component` name binding the facade to the `lazyEmbed` SPA island. */
|
|
11660
|
+
const EMBED_COMPONENT_NAME = "lazy-embed";
|
|
11661
|
+
/** CSS class on the facade's activation button. */
|
|
11662
|
+
const EMBED_BUTTON_CLASS = "lazy-embed-button";
|
|
11663
|
+
/** CSS class on the title span inside the activation button. */
|
|
11664
|
+
const EMBED_TITLE_CLASS = "lazy-embed-title";
|
|
11665
|
+
/**
|
|
11666
|
+
* Type guard for an `::embed` leaf directive.
|
|
11667
|
+
*
|
|
11668
|
+
* @param node - AST node to test.
|
|
11669
|
+
* @returns `true` when the node is an `::embed` leaf directive.
|
|
11670
|
+
* @example
|
|
11671
|
+
* ```ts
|
|
11672
|
+
* if (isEmbedDirective(node)) console.log(node.attributes?.src);
|
|
11673
|
+
* ```
|
|
11674
|
+
*/
|
|
11675
|
+
function isEmbedDirective(node) {
|
|
11676
|
+
return node.type === "leafDirective" && node.name === "embed";
|
|
11677
|
+
}
|
|
11678
|
+
/**
|
|
11679
|
+
* Escape a string for safe interpolation into a double-quoted HTML attribute.
|
|
11680
|
+
*
|
|
11681
|
+
* @param value - The raw attribute value.
|
|
11682
|
+
* @returns The escaped value.
|
|
11683
|
+
* @example
|
|
11684
|
+
* ```ts
|
|
11685
|
+
* escapeAttribute('He said "hi" & left'); // "He said "hi" & left"
|
|
11686
|
+
* ```
|
|
11687
|
+
*/
|
|
11688
|
+
function escapeAttribute(value) {
|
|
11689
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
|
|
11690
|
+
}
|
|
11691
|
+
/**
|
|
11692
|
+
* Validate an embed `src` URL: only `https:`/`http:` absolute URLs and
|
|
11693
|
+
* root-relative paths are embeddable — anything else (`javascript:`, `data:`,
|
|
11694
|
+
* scheme-relative, …) fails the build.
|
|
11695
|
+
*
|
|
11696
|
+
* @param src - The raw `src` attribute value.
|
|
11697
|
+
* @returns `true` when the URL is embeddable.
|
|
11698
|
+
* @example
|
|
11699
|
+
* ```ts
|
|
11700
|
+
* isEmbeddableUrl("https://game.example.com/"); // true
|
|
11701
|
+
* isEmbeddableUrl("javascript:alert(1)"); // false
|
|
11702
|
+
* ```
|
|
11703
|
+
*/
|
|
11704
|
+
function isEmbeddableUrl(src) {
|
|
11705
|
+
if (src.startsWith("/") && !src.startsWith("//")) return true;
|
|
11706
|
+
return /^https?:\/\//i.test(src);
|
|
11707
|
+
}
|
|
11708
|
+
/**
|
|
11709
|
+
* Build the static facade HTML for one embed: the `<figure>` carrying the
|
|
11710
|
+
* target in data attributes plus the activation `<button>` (the button label
|
|
11711
|
+
* is the embed's title; visual chrome is consumer CSS).
|
|
11712
|
+
*
|
|
11713
|
+
* @param src - The validated embed URL.
|
|
11714
|
+
* @param title - The human-readable embed title (button label, iframe title).
|
|
11715
|
+
* @returns The facade HTML string.
|
|
11716
|
+
* @example
|
|
11717
|
+
* ```ts
|
|
11718
|
+
* embedFacadeHtml("https://game.example.com/", "My Game");
|
|
11719
|
+
* ```
|
|
11720
|
+
*/
|
|
11721
|
+
function embedFacadeHtml(src, title) {
|
|
11722
|
+
const safeSource = escapeAttribute(src);
|
|
11723
|
+
const safeTitle = escapeAttribute(title);
|
|
11724
|
+
return `<figure class="${EMBED_FIGURE_CLASS}" data-component="${EMBED_COMPONENT_NAME}" data-embed-src="${safeSource}" data-embed-title="${safeTitle}"><button type="button" class="${EMBED_BUTTON_CLASS}" aria-label="Load embed: ${safeTitle}"><span class="${EMBED_TITLE_CLASS}">${safeTitle}</span></button></figure>`;
|
|
11725
|
+
}
|
|
11726
|
+
/**
|
|
11727
|
+
* Mdast transformer rewriting every `::embed` leaf directive to its facade
|
|
11728
|
+
* HTML node. A directive missing `src`/`title`, or carrying a non-embeddable
|
|
11729
|
+
* URL, fails the build with the offending value quoted.
|
|
11730
|
+
*
|
|
11731
|
+
* @param tree - The mdast tree to mutate.
|
|
11732
|
+
* @throws {Error} When an `::embed` directive is missing `src` or `title`, or
|
|
11733
|
+
* its `src` is not an embeddable URL.
|
|
11734
|
+
* @example
|
|
11735
|
+
* ```ts
|
|
11736
|
+
* embedTransform(tree);
|
|
11737
|
+
* ```
|
|
11738
|
+
*/
|
|
11739
|
+
function embedTransform(tree) {
|
|
11740
|
+
visit(tree, (node, index, parent) => {
|
|
11741
|
+
if (!isEmbedDirective(node)) return;
|
|
11742
|
+
if (parent === void 0 || index === void 0) return;
|
|
11743
|
+
const src = node.attributes?.src ?? "";
|
|
11744
|
+
const title = node.attributes?.title ?? "";
|
|
11745
|
+
if (src === "" || title === "") throw new Error("[web] content: `::embed` requires both `src` and `title` attributes, e.g. ::embed{src=\"https://…\" title=\"…\"}.");
|
|
11746
|
+
if (!isEmbeddableUrl(src)) throw new Error(`[web] content: \`::embed\` src must be an http(s) URL or a root-relative path (got "${src}").`);
|
|
11747
|
+
const html = {
|
|
11748
|
+
type: "html",
|
|
11749
|
+
value: embedFacadeHtml(src, title)
|
|
11750
|
+
};
|
|
11751
|
+
parent.children[index] = html;
|
|
11752
|
+
});
|
|
11753
|
+
}
|
|
11754
|
+
/**
|
|
11755
|
+
* Remark transform: rewrites `::embed{src="…" title="…"}` leaf directives into
|
|
11756
|
+
* static click-to-activate facades (no iframe until the reader clicks — see
|
|
11757
|
+
* the file header). Opt-in via the provider's `embed` option; requires
|
|
11758
|
+
* `trustedContent: true` because the facade is raw HTML the sanitize pass
|
|
11759
|
+
* would strip.
|
|
11760
|
+
*
|
|
11761
|
+
* @returns An mdast tree transformer.
|
|
11762
|
+
* @example
|
|
11763
|
+
* ```ts
|
|
11764
|
+
* unified().use(embedPlugin);
|
|
11765
|
+
* ```
|
|
11766
|
+
*/
|
|
11767
|
+
function embedPlugin() {
|
|
11768
|
+
return embedTransform;
|
|
11769
|
+
}
|
|
11770
|
+
//#endregion
|
|
11576
11771
|
//#region src/plugins/content/pipeline/mermaid.ts
|
|
11577
11772
|
/** CSS class on the `<figure>` wrapper around each rendered diagram. */
|
|
11578
11773
|
const MERMAID_FIGURE_CLASS = "mermaid-diagram";
|
|
@@ -11856,14 +12051,17 @@ function sectionDividerPlugin() {
|
|
|
11856
12051
|
}
|
|
11857
12052
|
/**
|
|
11858
12053
|
* The hardcoded framework default remark (Markdown-AST) plugins, in order:
|
|
11859
|
-
* parse, frontmatter, gfm, directive, pull-quote, the OPT-IN mermaid
|
|
11860
|
-
* then the mdast→hast bridge (`remark-rehype` with
|
|
11861
|
-
* Pull-quote and mermaid run on the mdast before
|
|
11862
|
-
* directive carries its `hName`/`hProperties`,
|
|
11863
|
-
*
|
|
11864
|
-
*
|
|
11865
|
-
*
|
|
11866
|
-
*
|
|
12054
|
+
* parse, frontmatter, gfm, directive, pull-quote, the OPT-IN embed + mermaid
|
|
12055
|
+
* transforms, then the mdast→hast bridge (`remark-rehype` with
|
|
12056
|
+
* `allowDangerousHtml`). Pull-quote, embed and mermaid run on the mdast before
|
|
12057
|
+
* the bridge — pull-quote so the directive carries its `hName`/`hProperties`,
|
|
12058
|
+
* embed so the directive is replaced with its raw facade HTML, mermaid so the
|
|
12059
|
+
* fence is replaced with raw SVG HTML before Shiki could ever claim the code
|
|
12060
|
+
* block.
|
|
12061
|
+
*
|
|
12062
|
+
* @param config - Optional provider configuration; only the opt-in flags are
|
|
12063
|
+
* read here: `mermaid` and `embed` (each truthy value enables its transform at
|
|
12064
|
+
* a fixed mdast position).
|
|
11867
12065
|
* @returns The ordered default remark pluggables.
|
|
11868
12066
|
* @example
|
|
11869
12067
|
* ```ts
|
|
@@ -11878,6 +12076,7 @@ function defaultRemarkPlugins(config) {
|
|
|
11878
12076
|
remarkDirective,
|
|
11879
12077
|
pullQuotePlugin
|
|
11880
12078
|
];
|
|
12079
|
+
if (config?.embed) plugins.push(embedPlugin);
|
|
11881
12080
|
if (config?.mermaid) plugins.push([remarkMermaidDiagrams, normalizeMermaidOptions(config.mermaid)]);
|
|
11882
12081
|
plugins.push([remarkRehype, { allowDangerousHtml: true }]);
|
|
11883
12082
|
return plugins;
|
|
@@ -12351,4 +12550,4 @@ const createApp = core.createApp;
|
|
|
12351
12550
|
*/
|
|
12352
12551
|
const createPlugin = core.createPlugin;
|
|
12353
12552
|
//#endregion
|
|
12354
|
-
export { types_exports as Build, types_exports$1 as Cli, types_exports$2 as Content, types_exports$3 as Data, types_exports$4 as Deploy, types_exports$5 as Env, types_exports$6 as Head, types_exports$7 as Log, types_exports$8 as Router, types_exports$9 as Spa, browserEnv, buildArticleHead, buildPlugin, canonical, cliPlugin, cloudflareBindings, contentPlugin, createApp, createComponent, createPlugin, createUrls, dataPlugin, defineRoutes, deployPlugin, dotenv, envPlugin, feedLink, fileSystemContent, headPlugin, hreflang, i18nPlugin, jsonLd, logPlugin, meta, og, processEnv, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|
|
12553
|
+
export { types_exports as Build, types_exports$1 as Cli, types_exports$2 as Content, types_exports$3 as Data, types_exports$4 as Deploy, types_exports$5 as Env, types_exports$6 as Head, types_exports$7 as Log, types_exports$8 as Router, types_exports$9 as Spa, browserEnv, buildArticleHead, buildPlugin, canonical, cliPlugin, cloudflareBindings, contentPlugin, createApp, createComponent, createPlugin, createUrls, dataPlugin, defineRoutes, deployPlugin, dotenv, envPlugin, feedLink, fileSystemContent, headPlugin, hreflang, i18nPlugin, jsonLd, lazyEmbed, logPlugin, meta, og, processEnv, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|
package/package.json
CHANGED