@moku-labs/web 1.10.0 → 1.11.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 +55 -5
- package/dist/browser.mjs +8 -3
- package/dist/index.cjs +235 -56
- package/dist/index.d.cts +76 -6
- package/dist/index.d.mts +76 -6
- package/dist/index.mjs +235 -57
- package/package.json +1 -1
package/dist/browser.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EmitFn } from "@moku-labs/core";
|
|
2
|
-
import { ComponentChildren, VNode } from "preact";
|
|
2
|
+
import { ComponentChildren, FunctionComponent, VNode } from "preact";
|
|
3
3
|
import { BundledTheme, ThemeRegistrationAny } from "shiki";
|
|
4
4
|
import { Pluggable, Processor } from "unified";
|
|
5
5
|
|
|
@@ -1927,7 +1927,7 @@ type DataProvider = {
|
|
|
1927
1927
|
*/
|
|
1928
1928
|
declare const dataPlugin: import("@moku-labs/core").PluginInstance<"data", DataConfig, DataState, DataProvider, {}> & Record<never, never>;
|
|
1929
1929
|
declare namespace types_d_exports {
|
|
1930
|
-
export { Api, Article, ArticleCard, ComputedFields, Config, ContentApiContext, ContentEvents, ContentProvider, ContentProviderState, FileSystemContentOptions, Frontmatter, LoadAllOptions, MermaidDiagramOptions, State };
|
|
1930
|
+
export { Api, Article, ArticleCard, ComputedFields, Config, ContentApiContext, ContentEvents, ContentProvider, ContentProviderState, EmbedFacade, EmbedFacadeProps, EmbedOptions, FileSystemContentOptions, Frontmatter, LoadAllOptions, MermaidDiagramOptions, State };
|
|
1931
1931
|
}
|
|
1932
1932
|
/**
|
|
1933
1933
|
* YAML frontmatter parsed from each article file.
|
|
@@ -2071,6 +2071,55 @@ type MermaidDiagramOptions = {
|
|
|
2071
2071
|
*/
|
|
2072
2072
|
renderDiagrams?: (sources: readonly string[], mermaidConfig?: Record<string, unknown>) => Promise<readonly string[]>;
|
|
2073
2073
|
};
|
|
2074
|
+
/**
|
|
2075
|
+
* Props handed to an `::embed` facade component (the click-to-activate placeholder
|
|
2076
|
+
* the framework renders to static markup at build time). `width`/`height` are the
|
|
2077
|
+
* parsed pixel dimensions when the directive set them; `attributes` is the full raw
|
|
2078
|
+
* directive attribute bag, so a custom facade can read arbitrary extra options
|
|
2079
|
+
* (e.g. `::embed{… poster="/p.jpg" label="Play"}`).
|
|
2080
|
+
*
|
|
2081
|
+
* @example
|
|
2082
|
+
* ```tsx
|
|
2083
|
+
* const Facade = ({ title, attributes }: EmbedFacadeProps) => (
|
|
2084
|
+
* <button type="button" class="lazy-embed-button">
|
|
2085
|
+
* {attributes.poster ? <img src={attributes.poster} alt="" /> : null}
|
|
2086
|
+
* <span class="lazy-embed-title">{title}</span>
|
|
2087
|
+
* </button>
|
|
2088
|
+
* );
|
|
2089
|
+
* ```
|
|
2090
|
+
*/
|
|
2091
|
+
type EmbedFacadeProps = {
|
|
2092
|
+
/** The embed target exactly as written in the directive (the provider resolves it later). */src: string; /** The human-readable embed title (default label + iframe title). */
|
|
2093
|
+
title: string; /** Reserved-box width in pixels, when the directive set `width`/`height`. */
|
|
2094
|
+
width?: number; /** Reserved-box height in pixels, when the directive set `width`/`height`. */
|
|
2095
|
+
height?: number; /** The full raw directive attribute bag (custom options live here). */
|
|
2096
|
+
attributes: Readonly<Record<string, string>>;
|
|
2097
|
+
};
|
|
2098
|
+
/**
|
|
2099
|
+
* A consumer-supplied facade component: a Preact function component over
|
|
2100
|
+
* {@link EmbedFacadeProps}, rendered (at build time, to static markup) as the
|
|
2101
|
+
* facade's inner content — inside the framework-owned `<figure>` that carries the
|
|
2102
|
+
* island hooks + reserved-box sizing. Defaults to the built-in `EmbedFacadeButton`.
|
|
2103
|
+
*/
|
|
2104
|
+
type EmbedFacade = FunctionComponent<EmbedFacadeProps>;
|
|
2105
|
+
/**
|
|
2106
|
+
* Options for the `::embed` lazy-iframe feature (the `embed` key of
|
|
2107
|
+
* {@link FileSystemContentOptions}). `embed: true` uses the default facade;
|
|
2108
|
+
* `embed: { facade }` swaps in a consumer Preact component for the placeholder.
|
|
2109
|
+
*
|
|
2110
|
+
* @example
|
|
2111
|
+
* ```ts
|
|
2112
|
+
* fileSystemContent({ contentDir: "./content", trustedContent: true, embed: { facade: MyFacade } });
|
|
2113
|
+
* ```
|
|
2114
|
+
*/
|
|
2115
|
+
type EmbedOptions = {
|
|
2116
|
+
/**
|
|
2117
|
+
* Consumer Preact component rendering the facade's inner content (SSR'd to
|
|
2118
|
+
* static markup at build — no client JS). Receives {@link EmbedFacadeProps}.
|
|
2119
|
+
* Defaults to the built-in `EmbedFacadeButton`.
|
|
2120
|
+
*/
|
|
2121
|
+
facade?: EmbedFacade;
|
|
2122
|
+
};
|
|
2074
2123
|
/**
|
|
2075
2124
|
* Options for the node filesystem provider {@link ContentProvider} `fileSystemContent`.
|
|
2076
2125
|
* These are the markdown-pipeline + source concerns that used to live on the content
|
|
@@ -2110,10 +2159,11 @@ type FileSystemContentOptions = {
|
|
|
2110
2159
|
* into static click-to-activate facades (no iframe — and none of the target's
|
|
2111
2160
|
* network/JS cost — until the reader clicks). Pair with the `lazyEmbed` SPA
|
|
2112
2161
|
* island, which swaps the facade for the real `<iframe loading="lazy">`.
|
|
2113
|
-
*
|
|
2114
|
-
*
|
|
2162
|
+
* `true` enables with the default facade; an object passes {@link EmbedOptions}
|
|
2163
|
+
* (e.g. a consumer `facade` Preact component). Requires `trustedContent: true`
|
|
2164
|
+
* (the facade is raw HTML the sanitize pass would strip). Defaults to disabled.
|
|
2115
2165
|
*/
|
|
2116
|
-
embed?: boolean;
|
|
2166
|
+
embed?: boolean | EmbedOptions;
|
|
2117
2167
|
};
|
|
2118
2168
|
/**
|
|
2119
2169
|
* Internal mutable state of the filesystem provider: the lazy unified processor and
|
package/dist/browser.mjs
CHANGED
|
@@ -4466,7 +4466,11 @@ function activateEmbed(figure) {
|
|
|
4466
4466
|
}
|
|
4467
4467
|
/**
|
|
4468
4468
|
* Shared click handler (module-level so mount/unmount detach the same
|
|
4469
|
-
* reference):
|
|
4469
|
+
* reference): any click on the not-yet-active facade activates the embed. It
|
|
4470
|
+
* fires on the whole facade — not a specific button class — so a consumer's
|
|
4471
|
+
* custom facade markup (see content `embed.facade`) works without re-wiring;
|
|
4472
|
+
* the default facade's `<button>` keeps it keyboard-accessible. Once active
|
|
4473
|
+
* (`data-embed-active`), clicks fall through to the live iframe.
|
|
4470
4474
|
*
|
|
4471
4475
|
* @param event - The click event from the facade figure.
|
|
4472
4476
|
* @example
|
|
@@ -4475,9 +4479,10 @@ function activateEmbed(figure) {
|
|
|
4475
4479
|
* ```
|
|
4476
4480
|
*/
|
|
4477
4481
|
function onFacadeClick(event) {
|
|
4478
|
-
if (!event.target.closest("button.lazy-embed-button")) return;
|
|
4479
4482
|
const figure = event.currentTarget;
|
|
4480
|
-
if (figure instanceof HTMLElement)
|
|
4483
|
+
if (!(figure instanceof HTMLElement)) return;
|
|
4484
|
+
if (figure.dataset.embedActive !== void 0) return;
|
|
4485
|
+
activateEmbed(figure);
|
|
4481
4486
|
}
|
|
4482
4487
|
/**
|
|
4483
4488
|
* Lazy-embed island: facade button click → real `<iframe loading="lazy">`.
|
package/dist/index.cjs
CHANGED
|
@@ -36,6 +36,7 @@ remark_parse = require_convention.__toESM(remark_parse, 1);
|
|
|
36
36
|
let remark_rehype = require("remark-rehype");
|
|
37
37
|
remark_rehype = require_convention.__toESM(remark_rehype, 1);
|
|
38
38
|
let unist_util_visit = require("unist-util-visit");
|
|
39
|
+
let preact_jsx_runtime = require("preact/jsx-runtime");
|
|
39
40
|
let hast_util_sanitize = require("hast-util-sanitize");
|
|
40
41
|
let reading_time = require("reading-time");
|
|
41
42
|
reading_time = require_convention.__toESM(reading_time, 1);
|
|
@@ -4053,19 +4054,24 @@ function readCachedContent(ctx) {
|
|
|
4053
4054
|
//#endregion
|
|
4054
4055
|
//#region src/plugins/build/phases/content-images.ts
|
|
4055
4056
|
/**
|
|
4056
|
-
* @file build phase — content-images. Copies each article's co-located
|
|
4057
|
-
* (`<contentDir>/<slug
|
|
4058
|
-
*
|
|
4059
|
-
*
|
|
4057
|
+
* @file build phase — content-images. Copies each article's co-located asset
|
|
4058
|
+
* directories (`<contentDir>/<slug>/<dir>/`, e.g. `images/` or a pre-built
|
|
4059
|
+
* `game/` embed bundle) to a single shared output location
|
|
4060
|
+
* (`<outDir>/<slug>/<dir>/`) reused by every locale, matching the absolute
|
|
4061
|
+
* `/<slug>/<dir>/...` URLs the content renderer emits (image src + `::embed`
|
|
4062
|
+
* src). Dot- and underscore-prefixed dirs are treated as private and skipped.
|
|
4063
|
+
* Gated by `config.images`.
|
|
4060
4064
|
*/
|
|
4061
|
-
/** Conventional per-article image subdirectory name (alongside `<slug>/<locale>.md`). */
|
|
4062
|
-
const ARTICLE_IMAGE_DIR = "images";
|
|
4063
4065
|
/**
|
|
4064
|
-
* Copy every article's co-located
|
|
4065
|
-
*
|
|
4066
|
+
* Copy every article's co-located asset directories to `<outDir>/<slug>/<dir>/`.
|
|
4067
|
+
* Each direct subdirectory of `<contentDir>/<slug>/` rides along (the
|
|
4068
|
+
* conventional `images/` dir plus any other bundle, like an `::embed` game),
|
|
4069
|
+
* except `.`/`_`-prefixed dirs (private). The `.md` source files are never
|
|
4070
|
+
* copied (only directories are). No-op when `config.images` is false or the
|
|
4071
|
+
* content directory does not exist.
|
|
4066
4072
|
*
|
|
4067
4073
|
* @param ctx - Plugin context (provides `config`, `log`, `require`).
|
|
4068
|
-
* @returns The number of
|
|
4074
|
+
* @returns The number of articles that had at least one asset directory copied.
|
|
4069
4075
|
* @example
|
|
4070
4076
|
* ```ts
|
|
4071
4077
|
* const copied = await copyContentImages(ctx);
|
|
@@ -4084,14 +4090,20 @@ async function copyContentImages(ctx) {
|
|
|
4084
4090
|
});
|
|
4085
4091
|
return 0;
|
|
4086
4092
|
}
|
|
4087
|
-
const
|
|
4093
|
+
const articleDirectories = await (0, node_fs_promises.readdir)(contentDir, { withFileTypes: true });
|
|
4088
4094
|
let copied = 0;
|
|
4089
|
-
for (const
|
|
4090
|
-
if (!
|
|
4091
|
-
const
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
+
for (const article of articleDirectories) {
|
|
4096
|
+
if (!article.isDirectory()) continue;
|
|
4097
|
+
const articleDir = node_path$1.default.join(contentDir, article.name);
|
|
4098
|
+
const assetDirectories = await (0, node_fs_promises.readdir)(articleDir, { withFileTypes: true });
|
|
4099
|
+
let copiedAny = false;
|
|
4100
|
+
for (const asset of assetDirectories) {
|
|
4101
|
+
if (!asset.isDirectory()) continue;
|
|
4102
|
+
if (asset.name.startsWith(".") || asset.name.startsWith("_")) continue;
|
|
4103
|
+
await (0, node_fs_promises.cp)(node_path$1.default.join(articleDir, asset.name), node_path$1.default.join(ctx.config.outDir, article.name, asset.name), { recursive: true });
|
|
4104
|
+
copiedAny = true;
|
|
4105
|
+
}
|
|
4106
|
+
if (copiedAny) copied += 1;
|
|
4095
4107
|
}
|
|
4096
4108
|
ctx.log.debug("build:content-images", { copied });
|
|
4097
4109
|
return copied;
|
|
@@ -11352,7 +11364,11 @@ function activateEmbed(figure) {
|
|
|
11352
11364
|
}
|
|
11353
11365
|
/**
|
|
11354
11366
|
* Shared click handler (module-level so mount/unmount detach the same
|
|
11355
|
-
* reference):
|
|
11367
|
+
* reference): any click on the not-yet-active facade activates the embed. It
|
|
11368
|
+
* fires on the whole facade — not a specific button class — so a consumer's
|
|
11369
|
+
* custom facade markup (see content `embed.facade`) works without re-wiring;
|
|
11370
|
+
* the default facade's `<button>` keeps it keyboard-accessible. Once active
|
|
11371
|
+
* (`data-embed-active`), clicks fall through to the live iframe.
|
|
11356
11372
|
*
|
|
11357
11373
|
* @param event - The click event from the facade figure.
|
|
11358
11374
|
* @example
|
|
@@ -11361,9 +11377,10 @@ function activateEmbed(figure) {
|
|
|
11361
11377
|
* ```
|
|
11362
11378
|
*/
|
|
11363
11379
|
function onFacadeClick(event) {
|
|
11364
|
-
if (!event.target.closest("button.lazy-embed-button")) return;
|
|
11365
11380
|
const figure = event.currentTarget;
|
|
11366
|
-
if (figure instanceof HTMLElement)
|
|
11381
|
+
if (!(figure instanceof HTMLElement)) return;
|
|
11382
|
+
if (figure.dataset.embedActive !== void 0) return;
|
|
11383
|
+
activateEmbed(figure);
|
|
11367
11384
|
}
|
|
11368
11385
|
/**
|
|
11369
11386
|
* Lazy-embed island: facade button click → real `<iframe loading="lazy">`.
|
|
@@ -11666,15 +11683,45 @@ function parseFrontmatter(raw, config) {
|
|
|
11666
11683
|
};
|
|
11667
11684
|
}
|
|
11668
11685
|
//#endregion
|
|
11686
|
+
//#region src/plugins/content/pipeline/embed-facade.tsx
|
|
11687
|
+
/** CSS class on the facade's activation button. */
|
|
11688
|
+
const EMBED_BUTTON_CLASS = "lazy-embed-button";
|
|
11689
|
+
/** CSS class on the title span inside the activation button. */
|
|
11690
|
+
const EMBED_TITLE_CLASS = "lazy-embed-title";
|
|
11691
|
+
/**
|
|
11692
|
+
* Default `::embed` facade inner content: a single labelled `<button>` carrying
|
|
11693
|
+
* the embed title. The companion `lazyEmbed` island activates the embed on a
|
|
11694
|
+
* click anywhere in the facade, so the button is the keyboard-accessible
|
|
11695
|
+
* control. Provided as the default and as a composable building block for custom
|
|
11696
|
+
* facades.
|
|
11697
|
+
*
|
|
11698
|
+
* @param props - The embed facade props (only `title` is used by the default).
|
|
11699
|
+
* @returns The facade inner-content VNode.
|
|
11700
|
+
* @example
|
|
11701
|
+
* ```tsx
|
|
11702
|
+
* // Compose the default inside a richer custom facade:
|
|
11703
|
+
* const MyFacade = (p: EmbedFacadeProps) => (
|
|
11704
|
+
* <div class="poster"><img src={p.attributes.poster} alt="" /><EmbedFacadeButton {...p} /></div>
|
|
11705
|
+
* );
|
|
11706
|
+
* ```
|
|
11707
|
+
*/
|
|
11708
|
+
function EmbedFacadeButton(props) {
|
|
11709
|
+
return /* @__PURE__ */ (0, preact_jsx_runtime.jsx)("button", {
|
|
11710
|
+
type: "button",
|
|
11711
|
+
class: EMBED_BUTTON_CLASS,
|
|
11712
|
+
"aria-label": `Load embed: ${props.title}`,
|
|
11713
|
+
children: /* @__PURE__ */ (0, preact_jsx_runtime.jsx)("span", {
|
|
11714
|
+
class: EMBED_TITLE_CLASS,
|
|
11715
|
+
children: props.title
|
|
11716
|
+
})
|
|
11717
|
+
});
|
|
11718
|
+
}
|
|
11719
|
+
//#endregion
|
|
11669
11720
|
//#region src/plugins/content/pipeline/embed.ts
|
|
11670
11721
|
/** CSS class on the `<figure>` facade wrapping each embed. */
|
|
11671
11722
|
const EMBED_FIGURE_CLASS = "lazy-embed";
|
|
11672
11723
|
/** `data-component` name binding the facade to the `lazyEmbed` SPA island. */
|
|
11673
11724
|
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
11725
|
/**
|
|
11679
11726
|
* Type guard for an `::embed` leaf directive.
|
|
11680
11727
|
*
|
|
@@ -11702,83 +11749,162 @@ function escapeAttribute(value) {
|
|
|
11702
11749
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
|
|
11703
11750
|
}
|
|
11704
11751
|
/**
|
|
11705
|
-
* Validate an embed `src
|
|
11706
|
-
* root-relative
|
|
11707
|
-
*
|
|
11752
|
+
* Validate an embed `src`. Three forms are embeddable: an `http(s)` URL, a
|
|
11753
|
+
* root-relative path (`/x`), or a co-located relative path (`./x`, `../x`,
|
|
11754
|
+
* `x/…`) resolved later against `/<slug>/`. Everything else — protocol-relative
|
|
11755
|
+
* (`//host`), `javascript:`, `data:`, any other scheme — is rejected.
|
|
11708
11756
|
*
|
|
11709
11757
|
* @param src - The raw `src` attribute value.
|
|
11710
|
-
* @returns `true` when the URL is embeddable.
|
|
11758
|
+
* @returns `true` when the URL/path is embeddable.
|
|
11711
11759
|
* @example
|
|
11712
11760
|
* ```ts
|
|
11713
11761
|
* isEmbeddableUrl("https://game.example.com/"); // true
|
|
11762
|
+
* isEmbeddableUrl("./game/index.html"); // true (co-located)
|
|
11714
11763
|
* isEmbeddableUrl("javascript:alert(1)"); // false
|
|
11715
11764
|
* ```
|
|
11716
11765
|
*/
|
|
11717
11766
|
function isEmbeddableUrl(src) {
|
|
11718
|
-
if (src
|
|
11719
|
-
|
|
11767
|
+
if (src === "") return false;
|
|
11768
|
+
if (src.startsWith("//")) return false;
|
|
11769
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(src)) return /^https?:\/\//i.test(src);
|
|
11770
|
+
return true;
|
|
11720
11771
|
}
|
|
11721
11772
|
/**
|
|
11722
|
-
*
|
|
11723
|
-
*
|
|
11724
|
-
* is the
|
|
11773
|
+
* Parse + validate the optional `width`/`height` directive attributes. Both
|
|
11774
|
+
* must be supplied together, each a positive integer count of pixels; the pair
|
|
11775
|
+
* is used to reserve the facade box at its true aspect ratio. Returns
|
|
11776
|
+
* `undefined` when neither is set.
|
|
11725
11777
|
*
|
|
11726
|
-
* @param
|
|
11727
|
-
* @param
|
|
11778
|
+
* @param width - Raw `width` attribute (or undefined).
|
|
11779
|
+
* @param height - Raw `height` attribute (or undefined).
|
|
11780
|
+
* @returns The parsed dimensions, or `undefined` when both are absent.
|
|
11781
|
+
* @throws {Error} When only one of the pair is set, or a value is not a
|
|
11782
|
+
* positive integer.
|
|
11783
|
+
* @example
|
|
11784
|
+
* ```ts
|
|
11785
|
+
* parseDimensions("400", "711"); // { width: 400, height: 711 }
|
|
11786
|
+
* parseDimensions(undefined, undefined); // undefined
|
|
11787
|
+
* ```
|
|
11788
|
+
*/
|
|
11789
|
+
function parseDimensions(width, height) {
|
|
11790
|
+
const hasWidth = width !== void 0 && width !== null && width !== "";
|
|
11791
|
+
const hasHeight = height !== void 0 && height !== null && height !== "";
|
|
11792
|
+
if (!hasWidth && !hasHeight) return void 0;
|
|
11793
|
+
if (!hasWidth || !hasHeight) throw new Error("[web] content: `::embed` width and height must be set together (got only one).");
|
|
11794
|
+
if (!/^\d+$/.test(width) || !/^\d+$/.test(height) || width === "0" || height === "0") throw new Error(`[web] content: \`::embed\` width/height must be positive integers in pixels (got "${width}"×"${height}").`);
|
|
11795
|
+
return {
|
|
11796
|
+
width: Number(width),
|
|
11797
|
+
height: Number(height)
|
|
11798
|
+
};
|
|
11799
|
+
}
|
|
11800
|
+
/**
|
|
11801
|
+
* Collect the directive's raw attribute bag into a plain string record, dropping
|
|
11802
|
+
* `null`/`undefined` values (so a custom facade can read arbitrary extra options).
|
|
11803
|
+
*
|
|
11804
|
+
* @param attributes - The raw directive attributes (or undefined).
|
|
11805
|
+
* @returns A string-valued attribute record.
|
|
11806
|
+
* @example
|
|
11807
|
+
* ```ts
|
|
11808
|
+
* collectAttributes({ src: "x", poster: "/p.jpg", flag: null }); // { src: "x", poster: "/p.jpg" }
|
|
11809
|
+
* ```
|
|
11810
|
+
*/
|
|
11811
|
+
function collectAttributes(attributes) {
|
|
11812
|
+
const out = {};
|
|
11813
|
+
for (const [key, value] of Object.entries(attributes ?? {})) if (typeof value === "string") out[key] = value;
|
|
11814
|
+
return out;
|
|
11815
|
+
}
|
|
11816
|
+
/**
|
|
11817
|
+
* Build the static facade HTML for one embed: the framework-owned `<figure>`
|
|
11818
|
+
* (island hooks in data attributes; optional reserved-box `aspect-ratio`/`max-width`
|
|
11819
|
+
* inline style when dimensions are given) wrapping the facade component's inner
|
|
11820
|
+
* content, SSR'd to static markup. The wrapper carries `data-embed-src` (raw —
|
|
11821
|
+
* the provider resolves a relative src) so neither the island contract nor the
|
|
11822
|
+
* URL rewrite depend on the consumer's markup.
|
|
11823
|
+
*
|
|
11824
|
+
* @param facade - The facade component (default {@link EmbedFacadeButton}).
|
|
11825
|
+
* @param props - The facade props (`src`, `title`, optional `width`/`height`, raw `attributes`).
|
|
11826
|
+
* @param dimensions - Optional reserved-box pixel dimensions.
|
|
11728
11827
|
* @returns The facade HTML string.
|
|
11729
11828
|
* @example
|
|
11730
11829
|
* ```ts
|
|
11731
|
-
* embedFacadeHtml("https://
|
|
11830
|
+
* embedFacadeHtml(EmbedFacadeButton, { src: "https://g/", title: "G", attributes: {} });
|
|
11831
|
+
* ```
|
|
11832
|
+
*/
|
|
11833
|
+
function embedFacadeHtml(facade, props, dimensions) {
|
|
11834
|
+
return `<figure class="${EMBED_FIGURE_CLASS}" data-component="${EMBED_COMPONENT_NAME}" data-embed-src="${escapeAttribute(props.src)}" data-embed-title="${escapeAttribute(props.title)}"${dimensions ? ` data-embed-width="${dimensions.width}" data-embed-height="${dimensions.height}" style="aspect-ratio: ${dimensions.width} / ${dimensions.height}; max-width: ${dimensions.width}px;"` : ""}>${(0, preact_render_to_string.renderToString)((0, preact.h)(facade, props))}</figure>`;
|
|
11835
|
+
}
|
|
11836
|
+
/**
|
|
11837
|
+
* Normalize the provider's `embed` config value (`boolean | options`) to a plain
|
|
11838
|
+
* {@link EmbedOptions} object for the transform factory.
|
|
11839
|
+
*
|
|
11840
|
+
* @param embed - The raw `FileSystemContentOptions.embed` value (truthy).
|
|
11841
|
+
* @returns The options object (`{}` for the bare `true` form).
|
|
11842
|
+
* @example
|
|
11843
|
+
* ```ts
|
|
11844
|
+
* normalizeEmbedOptions(true); // {}
|
|
11845
|
+
* normalizeEmbedOptions({ facade: MyFacade });
|
|
11732
11846
|
* ```
|
|
11733
11847
|
*/
|
|
11734
|
-
function
|
|
11735
|
-
|
|
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>`;
|
|
11848
|
+
function normalizeEmbedOptions(embed) {
|
|
11849
|
+
return typeof embed === "boolean" ? {} : embed;
|
|
11738
11850
|
}
|
|
11739
11851
|
/**
|
|
11740
11852
|
* Mdast transformer rewriting every `::embed` leaf directive to its facade
|
|
11741
|
-
* HTML node. A directive missing `src`/`title`,
|
|
11742
|
-
*
|
|
11853
|
+
* HTML node. A directive missing `src`/`title`, carrying a non-embeddable URL,
|
|
11854
|
+
* or carrying invalid `width`/`height`, fails the build with the offending
|
|
11855
|
+
* value quoted.
|
|
11743
11856
|
*
|
|
11857
|
+
* @param facade - The facade component to render the inner content with.
|
|
11744
11858
|
* @param tree - The mdast tree to mutate.
|
|
11745
|
-
* @throws {Error} When an `::embed` directive is missing `src` or `title`,
|
|
11746
|
-
*
|
|
11859
|
+
* @throws {Error} When an `::embed` directive is missing `src` or `title`, its
|
|
11860
|
+
* `src` is not embeddable, or its dimensions are invalid.
|
|
11747
11861
|
* @example
|
|
11748
11862
|
* ```ts
|
|
11749
|
-
* embedTransform(tree);
|
|
11863
|
+
* embedTransform(EmbedFacadeButton, tree);
|
|
11750
11864
|
* ```
|
|
11751
11865
|
*/
|
|
11752
|
-
function embedTransform(tree) {
|
|
11866
|
+
function embedTransform(facade, tree) {
|
|
11753
11867
|
(0, unist_util_visit.visit)(tree, (node, index, parent) => {
|
|
11754
11868
|
if (!isEmbedDirective(node)) return;
|
|
11755
11869
|
if (parent === void 0 || index === void 0) return;
|
|
11756
11870
|
const src = node.attributes?.src ?? "";
|
|
11757
11871
|
const title = node.attributes?.title ?? "";
|
|
11758
11872
|
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
|
|
11873
|
+
if (!isEmbeddableUrl(src)) throw new Error(`[web] content: \`::embed\` src must be an http(s) URL, a root-relative path, or a co-located relative path (got "${src}").`);
|
|
11874
|
+
const dimensions = parseDimensions(node.attributes?.width, node.attributes?.height);
|
|
11760
11875
|
const html = {
|
|
11761
11876
|
type: "html",
|
|
11762
|
-
value: embedFacadeHtml(
|
|
11877
|
+
value: embedFacadeHtml(facade, {
|
|
11878
|
+
src,
|
|
11879
|
+
title,
|
|
11880
|
+
...dimensions ? {
|
|
11881
|
+
width: dimensions.width,
|
|
11882
|
+
height: dimensions.height
|
|
11883
|
+
} : {},
|
|
11884
|
+
attributes: collectAttributes(node.attributes)
|
|
11885
|
+
}, dimensions)
|
|
11763
11886
|
};
|
|
11764
11887
|
parent.children[index] = html;
|
|
11765
11888
|
});
|
|
11766
11889
|
}
|
|
11767
11890
|
/**
|
|
11768
|
-
* Remark transform: rewrites `::embed{src="…" title="…"}` leaf directives
|
|
11769
|
-
* static click-to-activate facades (no iframe until the reader clicks — see
|
|
11891
|
+
* Remark transform factory: rewrites `::embed{src="…" title="…"}` leaf directives
|
|
11892
|
+
* into static click-to-activate facades (no iframe until the reader clicks — see
|
|
11770
11893
|
* 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
|
-
*
|
|
11894
|
+
* `trustedContent: true` because the facade is raw HTML the sanitize pass would
|
|
11895
|
+
* strip. The facade's inner content is rendered by `options.facade` (a consumer
|
|
11896
|
+
* Preact component) or the built-in {@link EmbedFacadeButton}.
|
|
11773
11897
|
*
|
|
11898
|
+
* @param options - Embed options (the optional `facade` component).
|
|
11774
11899
|
* @returns An mdast tree transformer.
|
|
11775
11900
|
* @example
|
|
11776
11901
|
* ```ts
|
|
11777
|
-
* unified().use(embedPlugin);
|
|
11902
|
+
* unified().use(embedPlugin, { facade: MyFacade });
|
|
11778
11903
|
* ```
|
|
11779
11904
|
*/
|
|
11780
|
-
function embedPlugin() {
|
|
11781
|
-
|
|
11905
|
+
function embedPlugin(options = {}) {
|
|
11906
|
+
const facade = options.facade ?? EmbedFacadeButton;
|
|
11907
|
+
return (tree) => embedTransform(facade, tree);
|
|
11782
11908
|
}
|
|
11783
11909
|
//#endregion
|
|
11784
11910
|
//#region src/plugins/content/pipeline/mermaid.ts
|
|
@@ -12089,7 +12215,7 @@ function defaultRemarkPlugins(config) {
|
|
|
12089
12215
|
remark_directive.default,
|
|
12090
12216
|
pullQuotePlugin
|
|
12091
12217
|
];
|
|
12092
|
-
if (config?.embed) plugins.push(embedPlugin);
|
|
12218
|
+
if (config?.embed) plugins.push([embedPlugin, normalizeEmbedOptions(config.embed)]);
|
|
12093
12219
|
if (config?.mermaid) plugins.push([remarkMermaidDiagrams, normalizeMermaidOptions(config.mermaid)]);
|
|
12094
12220
|
plugins.push([remark_rehype.default, { allowDangerousHtml: true }]);
|
|
12095
12221
|
return plugins;
|
|
@@ -12331,6 +12457,8 @@ function calculateReadingTime(text) {
|
|
|
12331
12457
|
*/
|
|
12332
12458
|
/** Matches an `<img>` `src` that points at the co-located `images/` dir (relative or root-relative). */
|
|
12333
12459
|
const RELATIVE_IMAGE_SRC = /(<img\b[^>]*?\bsrc=")(?:\.?\/)?images\//g;
|
|
12460
|
+
/** Matches the `data-embed-src` of an `::embed` facade (value captured for path resolution). */
|
|
12461
|
+
const EMBED_SRC_ATTR = /(\bdata-embed-src=")([^"]*)(")/g;
|
|
12334
12462
|
/**
|
|
12335
12463
|
* Build a canonical article URL for a locale + slug.
|
|
12336
12464
|
*
|
|
@@ -12361,6 +12489,56 @@ function rewriteImageUrls(html, slug) {
|
|
|
12361
12489
|
return html.replaceAll(RELATIVE_IMAGE_SRC, `$1/${slug}/images/`);
|
|
12362
12490
|
}
|
|
12363
12491
|
/**
|
|
12492
|
+
* Resolve an `::embed` `src` to the URL the iframe should load. Absolute targets
|
|
12493
|
+
* (`http(s)://…`, root-relative `/…`) pass through unchanged; a co-located
|
|
12494
|
+
* relative path (`./game/index.html`, `../x`, `game/x`) is resolved against the
|
|
12495
|
+
* article base `/<slug>/` into the single shared absolute path the content-assets
|
|
12496
|
+
* build phase copies the bundle to — so it loads identically from every locale
|
|
12497
|
+
* page (mirroring how co-located images resolve). Any `?query`/`#hash` is
|
|
12498
|
+
* preserved verbatim.
|
|
12499
|
+
*
|
|
12500
|
+
* @param value - The raw `data-embed-src` value.
|
|
12501
|
+
* @param slug - Article directory name.
|
|
12502
|
+
* @returns The resolved embed URL.
|
|
12503
|
+
* @example
|
|
12504
|
+
* ```ts
|
|
12505
|
+
* resolveEmbedSource("./game/index.html", "post"); // "/post/game/index.html"
|
|
12506
|
+
* resolveEmbedSource("https://x.dev/", "post"); // "https://x.dev/"
|
|
12507
|
+
* ```
|
|
12508
|
+
*/
|
|
12509
|
+
function resolveEmbedSource(value, slug) {
|
|
12510
|
+
if (/^https?:\/\//i.test(value) || value.startsWith("/")) return value;
|
|
12511
|
+
const tailIndex = value.search(/[?#]/);
|
|
12512
|
+
const rawPath = tailIndex === -1 ? value : value.slice(0, tailIndex);
|
|
12513
|
+
const tail = tailIndex === -1 ? "" : value.slice(tailIndex);
|
|
12514
|
+
const out = [];
|
|
12515
|
+
for (const segment of `${slug}/${rawPath}`.split("/")) {
|
|
12516
|
+
if (segment === "" || segment === ".") continue;
|
|
12517
|
+
if (segment === "..") {
|
|
12518
|
+
out.pop();
|
|
12519
|
+
continue;
|
|
12520
|
+
}
|
|
12521
|
+
out.push(segment);
|
|
12522
|
+
}
|
|
12523
|
+
const trailingSlash = rawPath === "" || rawPath.endsWith("/") ? "/" : "";
|
|
12524
|
+
return `/${out.join("/")}${trailingSlash}${tail}`;
|
|
12525
|
+
}
|
|
12526
|
+
/**
|
|
12527
|
+
* Rewrite every `::embed` facade's relative `data-embed-src` to its shared
|
|
12528
|
+
* absolute `/<slug>/…` path (no-op for already-absolute targets).
|
|
12529
|
+
*
|
|
12530
|
+
* @param html - The rendered article HTML.
|
|
12531
|
+
* @param slug - Article directory name.
|
|
12532
|
+
* @returns The HTML with embed `src`s resolved.
|
|
12533
|
+
* @example
|
|
12534
|
+
* ```ts
|
|
12535
|
+
* rewriteEmbedUrls('<figure data-embed-src="./g/">', "post"); // '… data-embed-src="/post/g/"'
|
|
12536
|
+
* ```
|
|
12537
|
+
*/
|
|
12538
|
+
function rewriteEmbedUrls(html, slug) {
|
|
12539
|
+
return html.replaceAll(EMBED_SRC_ATTR, (_match, prefix, value, suffix) => `${prefix}${resolveEmbedSource(value, slug)}${suffix}`);
|
|
12540
|
+
}
|
|
12541
|
+
/**
|
|
12364
12542
|
* Discover slug-like subdirectories of the content root (direct children not
|
|
12365
12543
|
* starting with `.` or `_`), sorted alphabetically for deterministic ordering.
|
|
12366
12544
|
*
|
|
@@ -12439,7 +12617,7 @@ function fileSystemContent(options) {
|
|
|
12439
12617
|
state.dirtyPaths.delete(filePath);
|
|
12440
12618
|
const { frontmatter, body } = parseFrontmatter(raw, options);
|
|
12441
12619
|
const processor = ensureProcessor(state, options);
|
|
12442
|
-
const html = rewriteImageUrls(String(await processor.process(body)), slug);
|
|
12620
|
+
const html = rewriteEmbedUrls(rewriteImageUrls(String(await processor.process(body)), slug), slug);
|
|
12443
12621
|
const { readingTime, wordCount } = calculateReadingTime(body);
|
|
12444
12622
|
return {
|
|
12445
12623
|
frontmatter,
|
|
@@ -12593,6 +12771,7 @@ Object.defineProperty(exports, "Deploy", {
|
|
|
12593
12771
|
return types_exports$4;
|
|
12594
12772
|
}
|
|
12595
12773
|
});
|
|
12774
|
+
exports.EmbedFacadeButton = EmbedFacadeButton;
|
|
12596
12775
|
Object.defineProperty(exports, "Env", {
|
|
12597
12776
|
enumerable: true,
|
|
12598
12777
|
get: function() {
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ComponentChildren, VNode } from "preact";
|
|
1
|
+
import { ComponentChildren, FunctionComponent, VNode } from "preact";
|
|
2
2
|
import { EmitFn } from "@moku-labs/core";
|
|
3
3
|
import { BundledTheme, ThemeRegistrationAny } from "shiki";
|
|
4
4
|
import { Pluggable, Processor } from "unified";
|
|
@@ -2671,7 +2671,7 @@ type Api$1 = {
|
|
|
2671
2671
|
*/
|
|
2672
2672
|
declare const cliPlugin: import("@moku-labs/core").PluginInstance<"cli", Config$1, State$1, Api$1, {}> & Record<never, never>;
|
|
2673
2673
|
declare namespace types_d_exports$2 {
|
|
2674
|
-
export { Api, Article, ArticleCard, ComputedFields, Config, ContentApiContext, ContentEvents, ContentProvider, ContentProviderState, FileSystemContentOptions, Frontmatter, LoadAllOptions, MermaidDiagramOptions, State };
|
|
2674
|
+
export { Api, Article, ArticleCard, ComputedFields, Config, ContentApiContext, ContentEvents, ContentProvider, ContentProviderState, EmbedFacade, EmbedFacadeProps, EmbedOptions, FileSystemContentOptions, Frontmatter, LoadAllOptions, MermaidDiagramOptions, State };
|
|
2675
2675
|
}
|
|
2676
2676
|
/**
|
|
2677
2677
|
* YAML frontmatter parsed from each article file.
|
|
@@ -2815,6 +2815,55 @@ type MermaidDiagramOptions = {
|
|
|
2815
2815
|
*/
|
|
2816
2816
|
renderDiagrams?: (sources: readonly string[], mermaidConfig?: Record<string, unknown>) => Promise<readonly string[]>;
|
|
2817
2817
|
};
|
|
2818
|
+
/**
|
|
2819
|
+
* Props handed to an `::embed` facade component (the click-to-activate placeholder
|
|
2820
|
+
* the framework renders to static markup at build time). `width`/`height` are the
|
|
2821
|
+
* parsed pixel dimensions when the directive set them; `attributes` is the full raw
|
|
2822
|
+
* directive attribute bag, so a custom facade can read arbitrary extra options
|
|
2823
|
+
* (e.g. `::embed{… poster="/p.jpg" label="Play"}`).
|
|
2824
|
+
*
|
|
2825
|
+
* @example
|
|
2826
|
+
* ```tsx
|
|
2827
|
+
* const Facade = ({ title, attributes }: EmbedFacadeProps) => (
|
|
2828
|
+
* <button type="button" class="lazy-embed-button">
|
|
2829
|
+
* {attributes.poster ? <img src={attributes.poster} alt="" /> : null}
|
|
2830
|
+
* <span class="lazy-embed-title">{title}</span>
|
|
2831
|
+
* </button>
|
|
2832
|
+
* );
|
|
2833
|
+
* ```
|
|
2834
|
+
*/
|
|
2835
|
+
type EmbedFacadeProps = {
|
|
2836
|
+
/** The embed target exactly as written in the directive (the provider resolves it later). */src: string; /** The human-readable embed title (default label + iframe title). */
|
|
2837
|
+
title: string; /** Reserved-box width in pixels, when the directive set `width`/`height`. */
|
|
2838
|
+
width?: number; /** Reserved-box height in pixels, when the directive set `width`/`height`. */
|
|
2839
|
+
height?: number; /** The full raw directive attribute bag (custom options live here). */
|
|
2840
|
+
attributes: Readonly<Record<string, string>>;
|
|
2841
|
+
};
|
|
2842
|
+
/**
|
|
2843
|
+
* A consumer-supplied facade component: a Preact function component over
|
|
2844
|
+
* {@link EmbedFacadeProps}, rendered (at build time, to static markup) as the
|
|
2845
|
+
* facade's inner content — inside the framework-owned `<figure>` that carries the
|
|
2846
|
+
* island hooks + reserved-box sizing. Defaults to the built-in `EmbedFacadeButton`.
|
|
2847
|
+
*/
|
|
2848
|
+
type EmbedFacade = FunctionComponent<EmbedFacadeProps>;
|
|
2849
|
+
/**
|
|
2850
|
+
* Options for the `::embed` lazy-iframe feature (the `embed` key of
|
|
2851
|
+
* {@link FileSystemContentOptions}). `embed: true` uses the default facade;
|
|
2852
|
+
* `embed: { facade }` swaps in a consumer Preact component for the placeholder.
|
|
2853
|
+
*
|
|
2854
|
+
* @example
|
|
2855
|
+
* ```ts
|
|
2856
|
+
* fileSystemContent({ contentDir: "./content", trustedContent: true, embed: { facade: MyFacade } });
|
|
2857
|
+
* ```
|
|
2858
|
+
*/
|
|
2859
|
+
type EmbedOptions = {
|
|
2860
|
+
/**
|
|
2861
|
+
* Consumer Preact component rendering the facade's inner content (SSR'd to
|
|
2862
|
+
* static markup at build — no client JS). Receives {@link EmbedFacadeProps}.
|
|
2863
|
+
* Defaults to the built-in `EmbedFacadeButton`.
|
|
2864
|
+
*/
|
|
2865
|
+
facade?: EmbedFacade;
|
|
2866
|
+
};
|
|
2818
2867
|
/**
|
|
2819
2868
|
* Options for the node filesystem provider {@link ContentProvider} `fileSystemContent`.
|
|
2820
2869
|
* These are the markdown-pipeline + source concerns that used to live on the content
|
|
@@ -2854,10 +2903,11 @@ type FileSystemContentOptions = {
|
|
|
2854
2903
|
* into static click-to-activate facades (no iframe — and none of the target's
|
|
2855
2904
|
* network/JS cost — until the reader clicks). Pair with the `lazyEmbed` SPA
|
|
2856
2905
|
* island, which swaps the facade for the real `<iframe loading="lazy">`.
|
|
2857
|
-
*
|
|
2858
|
-
*
|
|
2906
|
+
* `true` enables with the default facade; an object passes {@link EmbedOptions}
|
|
2907
|
+
* (e.g. a consumer `facade` Preact component). Requires `trustedContent: true`
|
|
2908
|
+
* (the facade is raw HTML the sanitize pass would strip). Defaults to disabled.
|
|
2859
2909
|
*/
|
|
2860
|
-
embed?: boolean;
|
|
2910
|
+
embed?: boolean | EmbedOptions;
|
|
2861
2911
|
};
|
|
2862
2912
|
/**
|
|
2863
2913
|
* Internal mutable state of the filesystem provider: the lazy unified processor and
|
|
@@ -3597,6 +3647,26 @@ declare function cloudflareBindings(): EnvProvider;
|
|
|
3597
3647
|
*/
|
|
3598
3648
|
declare function fileSystemContent(options: FileSystemContentOptions): ContentProvider;
|
|
3599
3649
|
//#endregion
|
|
3650
|
+
//#region src/plugins/content/pipeline/embed-facade.d.ts
|
|
3651
|
+
/**
|
|
3652
|
+
* Default `::embed` facade inner content: a single labelled `<button>` carrying
|
|
3653
|
+
* the embed title. The companion `lazyEmbed` island activates the embed on a
|
|
3654
|
+
* click anywhere in the facade, so the button is the keyboard-accessible
|
|
3655
|
+
* control. Provided as the default and as a composable building block for custom
|
|
3656
|
+
* facades.
|
|
3657
|
+
*
|
|
3658
|
+
* @param props - The embed facade props (only `title` is used by the default).
|
|
3659
|
+
* @returns The facade inner-content VNode.
|
|
3660
|
+
* @example
|
|
3661
|
+
* ```tsx
|
|
3662
|
+
* // Compose the default inside a richer custom facade:
|
|
3663
|
+
* const MyFacade = (p: EmbedFacadeProps) => (
|
|
3664
|
+
* <div class="poster"><img src={p.attributes.poster} alt="" /><EmbedFacadeButton {...p} /></div>
|
|
3665
|
+
* );
|
|
3666
|
+
* ```
|
|
3667
|
+
*/
|
|
3668
|
+
declare function EmbedFacadeButton(props: EmbedFacadeProps): VNode;
|
|
3669
|
+
//#endregion
|
|
3600
3670
|
//#region src/index.d.ts
|
|
3601
3671
|
/**
|
|
3602
3672
|
* Create and initialize a `@moku-labs/web` application — the Layer-3 entry point.
|
|
@@ -3703,4 +3773,4 @@ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs
|
|
|
3703
3773
|
*/
|
|
3704
3774
|
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>]>>;
|
|
3705
3775
|
//#endregion
|
|
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 };
|
|
3776
|
+
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, type EmbedFacade, EmbedFacadeButton, type EmbedFacadeProps, type EmbedOptions, 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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EmitFn } from "@moku-labs/core";
|
|
2
|
-
import { ComponentChildren, VNode } from "preact";
|
|
2
|
+
import { ComponentChildren, FunctionComponent, VNode } from "preact";
|
|
3
3
|
import { Pluggable, Processor } from "unified";
|
|
4
4
|
import { BundledTheme, ThemeRegistrationAny } from "shiki";
|
|
5
5
|
|
|
@@ -2671,7 +2671,7 @@ type Api$1 = {
|
|
|
2671
2671
|
*/
|
|
2672
2672
|
declare const cliPlugin: import("@moku-labs/core").PluginInstance<"cli", Config$1, State$1, Api$1, {}> & Record<never, never>;
|
|
2673
2673
|
declare namespace types_d_exports$2 {
|
|
2674
|
-
export { Api, Article, ArticleCard, ComputedFields, Config, ContentApiContext, ContentEvents, ContentProvider, ContentProviderState, FileSystemContentOptions, Frontmatter, LoadAllOptions, MermaidDiagramOptions, State };
|
|
2674
|
+
export { Api, Article, ArticleCard, ComputedFields, Config, ContentApiContext, ContentEvents, ContentProvider, ContentProviderState, EmbedFacade, EmbedFacadeProps, EmbedOptions, FileSystemContentOptions, Frontmatter, LoadAllOptions, MermaidDiagramOptions, State };
|
|
2675
2675
|
}
|
|
2676
2676
|
/**
|
|
2677
2677
|
* YAML frontmatter parsed from each article file.
|
|
@@ -2815,6 +2815,55 @@ type MermaidDiagramOptions = {
|
|
|
2815
2815
|
*/
|
|
2816
2816
|
renderDiagrams?: (sources: readonly string[], mermaidConfig?: Record<string, unknown>) => Promise<readonly string[]>;
|
|
2817
2817
|
};
|
|
2818
|
+
/**
|
|
2819
|
+
* Props handed to an `::embed` facade component (the click-to-activate placeholder
|
|
2820
|
+
* the framework renders to static markup at build time). `width`/`height` are the
|
|
2821
|
+
* parsed pixel dimensions when the directive set them; `attributes` is the full raw
|
|
2822
|
+
* directive attribute bag, so a custom facade can read arbitrary extra options
|
|
2823
|
+
* (e.g. `::embed{… poster="/p.jpg" label="Play"}`).
|
|
2824
|
+
*
|
|
2825
|
+
* @example
|
|
2826
|
+
* ```tsx
|
|
2827
|
+
* const Facade = ({ title, attributes }: EmbedFacadeProps) => (
|
|
2828
|
+
* <button type="button" class="lazy-embed-button">
|
|
2829
|
+
* {attributes.poster ? <img src={attributes.poster} alt="" /> : null}
|
|
2830
|
+
* <span class="lazy-embed-title">{title}</span>
|
|
2831
|
+
* </button>
|
|
2832
|
+
* );
|
|
2833
|
+
* ```
|
|
2834
|
+
*/
|
|
2835
|
+
type EmbedFacadeProps = {
|
|
2836
|
+
/** The embed target exactly as written in the directive (the provider resolves it later). */src: string; /** The human-readable embed title (default label + iframe title). */
|
|
2837
|
+
title: string; /** Reserved-box width in pixels, when the directive set `width`/`height`. */
|
|
2838
|
+
width?: number; /** Reserved-box height in pixels, when the directive set `width`/`height`. */
|
|
2839
|
+
height?: number; /** The full raw directive attribute bag (custom options live here). */
|
|
2840
|
+
attributes: Readonly<Record<string, string>>;
|
|
2841
|
+
};
|
|
2842
|
+
/**
|
|
2843
|
+
* A consumer-supplied facade component: a Preact function component over
|
|
2844
|
+
* {@link EmbedFacadeProps}, rendered (at build time, to static markup) as the
|
|
2845
|
+
* facade's inner content — inside the framework-owned `<figure>` that carries the
|
|
2846
|
+
* island hooks + reserved-box sizing. Defaults to the built-in `EmbedFacadeButton`.
|
|
2847
|
+
*/
|
|
2848
|
+
type EmbedFacade = FunctionComponent<EmbedFacadeProps>;
|
|
2849
|
+
/**
|
|
2850
|
+
* Options for the `::embed` lazy-iframe feature (the `embed` key of
|
|
2851
|
+
* {@link FileSystemContentOptions}). `embed: true` uses the default facade;
|
|
2852
|
+
* `embed: { facade }` swaps in a consumer Preact component for the placeholder.
|
|
2853
|
+
*
|
|
2854
|
+
* @example
|
|
2855
|
+
* ```ts
|
|
2856
|
+
* fileSystemContent({ contentDir: "./content", trustedContent: true, embed: { facade: MyFacade } });
|
|
2857
|
+
* ```
|
|
2858
|
+
*/
|
|
2859
|
+
type EmbedOptions = {
|
|
2860
|
+
/**
|
|
2861
|
+
* Consumer Preact component rendering the facade's inner content (SSR'd to
|
|
2862
|
+
* static markup at build — no client JS). Receives {@link EmbedFacadeProps}.
|
|
2863
|
+
* Defaults to the built-in `EmbedFacadeButton`.
|
|
2864
|
+
*/
|
|
2865
|
+
facade?: EmbedFacade;
|
|
2866
|
+
};
|
|
2818
2867
|
/**
|
|
2819
2868
|
* Options for the node filesystem provider {@link ContentProvider} `fileSystemContent`.
|
|
2820
2869
|
* These are the markdown-pipeline + source concerns that used to live on the content
|
|
@@ -2854,10 +2903,11 @@ type FileSystemContentOptions = {
|
|
|
2854
2903
|
* into static click-to-activate facades (no iframe — and none of the target's
|
|
2855
2904
|
* network/JS cost — until the reader clicks). Pair with the `lazyEmbed` SPA
|
|
2856
2905
|
* island, which swaps the facade for the real `<iframe loading="lazy">`.
|
|
2857
|
-
*
|
|
2858
|
-
*
|
|
2906
|
+
* `true` enables with the default facade; an object passes {@link EmbedOptions}
|
|
2907
|
+
* (e.g. a consumer `facade` Preact component). Requires `trustedContent: true`
|
|
2908
|
+
* (the facade is raw HTML the sanitize pass would strip). Defaults to disabled.
|
|
2859
2909
|
*/
|
|
2860
|
-
embed?: boolean;
|
|
2910
|
+
embed?: boolean | EmbedOptions;
|
|
2861
2911
|
};
|
|
2862
2912
|
/**
|
|
2863
2913
|
* Internal mutable state of the filesystem provider: the lazy unified processor and
|
|
@@ -3597,6 +3647,26 @@ declare function cloudflareBindings(): EnvProvider;
|
|
|
3597
3647
|
*/
|
|
3598
3648
|
declare function fileSystemContent(options: FileSystemContentOptions): ContentProvider;
|
|
3599
3649
|
//#endregion
|
|
3650
|
+
//#region src/plugins/content/pipeline/embed-facade.d.ts
|
|
3651
|
+
/**
|
|
3652
|
+
* Default `::embed` facade inner content: a single labelled `<button>` carrying
|
|
3653
|
+
* the embed title. The companion `lazyEmbed` island activates the embed on a
|
|
3654
|
+
* click anywhere in the facade, so the button is the keyboard-accessible
|
|
3655
|
+
* control. Provided as the default and as a composable building block for custom
|
|
3656
|
+
* facades.
|
|
3657
|
+
*
|
|
3658
|
+
* @param props - The embed facade props (only `title` is used by the default).
|
|
3659
|
+
* @returns The facade inner-content VNode.
|
|
3660
|
+
* @example
|
|
3661
|
+
* ```tsx
|
|
3662
|
+
* // Compose the default inside a richer custom facade:
|
|
3663
|
+
* const MyFacade = (p: EmbedFacadeProps) => (
|
|
3664
|
+
* <div class="poster"><img src={p.attributes.poster} alt="" /><EmbedFacadeButton {...p} /></div>
|
|
3665
|
+
* );
|
|
3666
|
+
* ```
|
|
3667
|
+
*/
|
|
3668
|
+
declare function EmbedFacadeButton(props: EmbedFacadeProps): VNode;
|
|
3669
|
+
//#endregion
|
|
3600
3670
|
//#region src/index.d.ts
|
|
3601
3671
|
/**
|
|
3602
3672
|
* Create and initialize a `@moku-labs/web` application — the Layer-3 entry point.
|
|
@@ -3703,4 +3773,4 @@ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs
|
|
|
3703
3773
|
*/
|
|
3704
3774
|
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>]>>;
|
|
3705
3775
|
//#endregion
|
|
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 };
|
|
3776
|
+
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, type EmbedFacade, EmbedFacadeButton, type EmbedFacadeProps, type EmbedOptions, 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
|
@@ -24,6 +24,7 @@ import remarkGfm from "remark-gfm";
|
|
|
24
24
|
import remarkParse from "remark-parse";
|
|
25
25
|
import remarkRehype from "remark-rehype";
|
|
26
26
|
import { visit } from "unist-util-visit";
|
|
27
|
+
import { jsx } from "preact/jsx-runtime";
|
|
27
28
|
import { defaultSchema } from "hast-util-sanitize";
|
|
28
29
|
import readingTime from "reading-time";
|
|
29
30
|
//#region src/plugins/env/api.ts
|
|
@@ -4040,19 +4041,24 @@ function readCachedContent(ctx) {
|
|
|
4040
4041
|
//#endregion
|
|
4041
4042
|
//#region src/plugins/build/phases/content-images.ts
|
|
4042
4043
|
/**
|
|
4043
|
-
* @file build phase — content-images. Copies each article's co-located
|
|
4044
|
-
* (`<contentDir>/<slug
|
|
4045
|
-
*
|
|
4046
|
-
*
|
|
4044
|
+
* @file build phase — content-images. Copies each article's co-located asset
|
|
4045
|
+
* directories (`<contentDir>/<slug>/<dir>/`, e.g. `images/` or a pre-built
|
|
4046
|
+
* `game/` embed bundle) to a single shared output location
|
|
4047
|
+
* (`<outDir>/<slug>/<dir>/`) reused by every locale, matching the absolute
|
|
4048
|
+
* `/<slug>/<dir>/...` URLs the content renderer emits (image src + `::embed`
|
|
4049
|
+
* src). Dot- and underscore-prefixed dirs are treated as private and skipped.
|
|
4050
|
+
* Gated by `config.images`.
|
|
4047
4051
|
*/
|
|
4048
|
-
/** Conventional per-article image subdirectory name (alongside `<slug>/<locale>.md`). */
|
|
4049
|
-
const ARTICLE_IMAGE_DIR = "images";
|
|
4050
4052
|
/**
|
|
4051
|
-
* Copy every article's co-located
|
|
4052
|
-
*
|
|
4053
|
+
* Copy every article's co-located asset directories to `<outDir>/<slug>/<dir>/`.
|
|
4054
|
+
* Each direct subdirectory of `<contentDir>/<slug>/` rides along (the
|
|
4055
|
+
* conventional `images/` dir plus any other bundle, like an `::embed` game),
|
|
4056
|
+
* except `.`/`_`-prefixed dirs (private). The `.md` source files are never
|
|
4057
|
+
* copied (only directories are). No-op when `config.images` is false or the
|
|
4058
|
+
* content directory does not exist.
|
|
4053
4059
|
*
|
|
4054
4060
|
* @param ctx - Plugin context (provides `config`, `log`, `require`).
|
|
4055
|
-
* @returns The number of
|
|
4061
|
+
* @returns The number of articles that had at least one asset directory copied.
|
|
4056
4062
|
* @example
|
|
4057
4063
|
* ```ts
|
|
4058
4064
|
* const copied = await copyContentImages(ctx);
|
|
@@ -4071,14 +4077,20 @@ async function copyContentImages(ctx) {
|
|
|
4071
4077
|
});
|
|
4072
4078
|
return 0;
|
|
4073
4079
|
}
|
|
4074
|
-
const
|
|
4080
|
+
const articleDirectories = await readdir(contentDir, { withFileTypes: true });
|
|
4075
4081
|
let copied = 0;
|
|
4076
|
-
for (const
|
|
4077
|
-
if (!
|
|
4078
|
-
const
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
+
for (const article of articleDirectories) {
|
|
4083
|
+
if (!article.isDirectory()) continue;
|
|
4084
|
+
const articleDir = path.join(contentDir, article.name);
|
|
4085
|
+
const assetDirectories = await readdir(articleDir, { withFileTypes: true });
|
|
4086
|
+
let copiedAny = false;
|
|
4087
|
+
for (const asset of assetDirectories) {
|
|
4088
|
+
if (!asset.isDirectory()) continue;
|
|
4089
|
+
if (asset.name.startsWith(".") || asset.name.startsWith("_")) continue;
|
|
4090
|
+
await cp(path.join(articleDir, asset.name), path.join(ctx.config.outDir, article.name, asset.name), { recursive: true });
|
|
4091
|
+
copiedAny = true;
|
|
4092
|
+
}
|
|
4093
|
+
if (copiedAny) copied += 1;
|
|
4082
4094
|
}
|
|
4083
4095
|
ctx.log.debug("build:content-images", { copied });
|
|
4084
4096
|
return copied;
|
|
@@ -11339,7 +11351,11 @@ function activateEmbed(figure) {
|
|
|
11339
11351
|
}
|
|
11340
11352
|
/**
|
|
11341
11353
|
* Shared click handler (module-level so mount/unmount detach the same
|
|
11342
|
-
* reference):
|
|
11354
|
+
* reference): any click on the not-yet-active facade activates the embed. It
|
|
11355
|
+
* fires on the whole facade — not a specific button class — so a consumer's
|
|
11356
|
+
* custom facade markup (see content `embed.facade`) works without re-wiring;
|
|
11357
|
+
* the default facade's `<button>` keeps it keyboard-accessible. Once active
|
|
11358
|
+
* (`data-embed-active`), clicks fall through to the live iframe.
|
|
11343
11359
|
*
|
|
11344
11360
|
* @param event - The click event from the facade figure.
|
|
11345
11361
|
* @example
|
|
@@ -11348,9 +11364,10 @@ function activateEmbed(figure) {
|
|
|
11348
11364
|
* ```
|
|
11349
11365
|
*/
|
|
11350
11366
|
function onFacadeClick(event) {
|
|
11351
|
-
if (!event.target.closest("button.lazy-embed-button")) return;
|
|
11352
11367
|
const figure = event.currentTarget;
|
|
11353
|
-
if (figure instanceof HTMLElement)
|
|
11368
|
+
if (!(figure instanceof HTMLElement)) return;
|
|
11369
|
+
if (figure.dataset.embedActive !== void 0) return;
|
|
11370
|
+
activateEmbed(figure);
|
|
11354
11371
|
}
|
|
11355
11372
|
/**
|
|
11356
11373
|
* Lazy-embed island: facade button click → real `<iframe loading="lazy">`.
|
|
@@ -11653,15 +11670,45 @@ function parseFrontmatter(raw, config) {
|
|
|
11653
11670
|
};
|
|
11654
11671
|
}
|
|
11655
11672
|
//#endregion
|
|
11673
|
+
//#region src/plugins/content/pipeline/embed-facade.tsx
|
|
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
|
+
* Default `::embed` facade inner content: a single labelled `<button>` carrying
|
|
11680
|
+
* the embed title. The companion `lazyEmbed` island activates the embed on a
|
|
11681
|
+
* click anywhere in the facade, so the button is the keyboard-accessible
|
|
11682
|
+
* control. Provided as the default and as a composable building block for custom
|
|
11683
|
+
* facades.
|
|
11684
|
+
*
|
|
11685
|
+
* @param props - The embed facade props (only `title` is used by the default).
|
|
11686
|
+
* @returns The facade inner-content VNode.
|
|
11687
|
+
* @example
|
|
11688
|
+
* ```tsx
|
|
11689
|
+
* // Compose the default inside a richer custom facade:
|
|
11690
|
+
* const MyFacade = (p: EmbedFacadeProps) => (
|
|
11691
|
+
* <div class="poster"><img src={p.attributes.poster} alt="" /><EmbedFacadeButton {...p} /></div>
|
|
11692
|
+
* );
|
|
11693
|
+
* ```
|
|
11694
|
+
*/
|
|
11695
|
+
function EmbedFacadeButton(props) {
|
|
11696
|
+
return /* @__PURE__ */ jsx("button", {
|
|
11697
|
+
type: "button",
|
|
11698
|
+
class: EMBED_BUTTON_CLASS,
|
|
11699
|
+
"aria-label": `Load embed: ${props.title}`,
|
|
11700
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
11701
|
+
class: EMBED_TITLE_CLASS,
|
|
11702
|
+
children: props.title
|
|
11703
|
+
})
|
|
11704
|
+
});
|
|
11705
|
+
}
|
|
11706
|
+
//#endregion
|
|
11656
11707
|
//#region src/plugins/content/pipeline/embed.ts
|
|
11657
11708
|
/** CSS class on the `<figure>` facade wrapping each embed. */
|
|
11658
11709
|
const EMBED_FIGURE_CLASS = "lazy-embed";
|
|
11659
11710
|
/** `data-component` name binding the facade to the `lazyEmbed` SPA island. */
|
|
11660
11711
|
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
11712
|
/**
|
|
11666
11713
|
* Type guard for an `::embed` leaf directive.
|
|
11667
11714
|
*
|
|
@@ -11689,83 +11736,162 @@ function escapeAttribute(value) {
|
|
|
11689
11736
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
|
|
11690
11737
|
}
|
|
11691
11738
|
/**
|
|
11692
|
-
* Validate an embed `src
|
|
11693
|
-
* root-relative
|
|
11694
|
-
*
|
|
11739
|
+
* Validate an embed `src`. Three forms are embeddable: an `http(s)` URL, a
|
|
11740
|
+
* root-relative path (`/x`), or a co-located relative path (`./x`, `../x`,
|
|
11741
|
+
* `x/…`) resolved later against `/<slug>/`. Everything else — protocol-relative
|
|
11742
|
+
* (`//host`), `javascript:`, `data:`, any other scheme — is rejected.
|
|
11695
11743
|
*
|
|
11696
11744
|
* @param src - The raw `src` attribute value.
|
|
11697
|
-
* @returns `true` when the URL is embeddable.
|
|
11745
|
+
* @returns `true` when the URL/path is embeddable.
|
|
11698
11746
|
* @example
|
|
11699
11747
|
* ```ts
|
|
11700
11748
|
* isEmbeddableUrl("https://game.example.com/"); // true
|
|
11749
|
+
* isEmbeddableUrl("./game/index.html"); // true (co-located)
|
|
11701
11750
|
* isEmbeddableUrl("javascript:alert(1)"); // false
|
|
11702
11751
|
* ```
|
|
11703
11752
|
*/
|
|
11704
11753
|
function isEmbeddableUrl(src) {
|
|
11705
|
-
if (src
|
|
11706
|
-
|
|
11754
|
+
if (src === "") return false;
|
|
11755
|
+
if (src.startsWith("//")) return false;
|
|
11756
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(src)) return /^https?:\/\//i.test(src);
|
|
11757
|
+
return true;
|
|
11707
11758
|
}
|
|
11708
11759
|
/**
|
|
11709
|
-
*
|
|
11710
|
-
*
|
|
11711
|
-
* is the
|
|
11760
|
+
* Parse + validate the optional `width`/`height` directive attributes. Both
|
|
11761
|
+
* must be supplied together, each a positive integer count of pixels; the pair
|
|
11762
|
+
* is used to reserve the facade box at its true aspect ratio. Returns
|
|
11763
|
+
* `undefined` when neither is set.
|
|
11712
11764
|
*
|
|
11713
|
-
* @param
|
|
11714
|
-
* @param
|
|
11765
|
+
* @param width - Raw `width` attribute (or undefined).
|
|
11766
|
+
* @param height - Raw `height` attribute (or undefined).
|
|
11767
|
+
* @returns The parsed dimensions, or `undefined` when both are absent.
|
|
11768
|
+
* @throws {Error} When only one of the pair is set, or a value is not a
|
|
11769
|
+
* positive integer.
|
|
11770
|
+
* @example
|
|
11771
|
+
* ```ts
|
|
11772
|
+
* parseDimensions("400", "711"); // { width: 400, height: 711 }
|
|
11773
|
+
* parseDimensions(undefined, undefined); // undefined
|
|
11774
|
+
* ```
|
|
11775
|
+
*/
|
|
11776
|
+
function parseDimensions(width, height) {
|
|
11777
|
+
const hasWidth = width !== void 0 && width !== null && width !== "";
|
|
11778
|
+
const hasHeight = height !== void 0 && height !== null && height !== "";
|
|
11779
|
+
if (!hasWidth && !hasHeight) return void 0;
|
|
11780
|
+
if (!hasWidth || !hasHeight) throw new Error("[web] content: `::embed` width and height must be set together (got only one).");
|
|
11781
|
+
if (!/^\d+$/.test(width) || !/^\d+$/.test(height) || width === "0" || height === "0") throw new Error(`[web] content: \`::embed\` width/height must be positive integers in pixels (got "${width}"×"${height}").`);
|
|
11782
|
+
return {
|
|
11783
|
+
width: Number(width),
|
|
11784
|
+
height: Number(height)
|
|
11785
|
+
};
|
|
11786
|
+
}
|
|
11787
|
+
/**
|
|
11788
|
+
* Collect the directive's raw attribute bag into a plain string record, dropping
|
|
11789
|
+
* `null`/`undefined` values (so a custom facade can read arbitrary extra options).
|
|
11790
|
+
*
|
|
11791
|
+
* @param attributes - The raw directive attributes (or undefined).
|
|
11792
|
+
* @returns A string-valued attribute record.
|
|
11793
|
+
* @example
|
|
11794
|
+
* ```ts
|
|
11795
|
+
* collectAttributes({ src: "x", poster: "/p.jpg", flag: null }); // { src: "x", poster: "/p.jpg" }
|
|
11796
|
+
* ```
|
|
11797
|
+
*/
|
|
11798
|
+
function collectAttributes(attributes) {
|
|
11799
|
+
const out = {};
|
|
11800
|
+
for (const [key, value] of Object.entries(attributes ?? {})) if (typeof value === "string") out[key] = value;
|
|
11801
|
+
return out;
|
|
11802
|
+
}
|
|
11803
|
+
/**
|
|
11804
|
+
* Build the static facade HTML for one embed: the framework-owned `<figure>`
|
|
11805
|
+
* (island hooks in data attributes; optional reserved-box `aspect-ratio`/`max-width`
|
|
11806
|
+
* inline style when dimensions are given) wrapping the facade component's inner
|
|
11807
|
+
* content, SSR'd to static markup. The wrapper carries `data-embed-src` (raw —
|
|
11808
|
+
* the provider resolves a relative src) so neither the island contract nor the
|
|
11809
|
+
* URL rewrite depend on the consumer's markup.
|
|
11810
|
+
*
|
|
11811
|
+
* @param facade - The facade component (default {@link EmbedFacadeButton}).
|
|
11812
|
+
* @param props - The facade props (`src`, `title`, optional `width`/`height`, raw `attributes`).
|
|
11813
|
+
* @param dimensions - Optional reserved-box pixel dimensions.
|
|
11715
11814
|
* @returns The facade HTML string.
|
|
11716
11815
|
* @example
|
|
11717
11816
|
* ```ts
|
|
11718
|
-
* embedFacadeHtml("https://
|
|
11817
|
+
* embedFacadeHtml(EmbedFacadeButton, { src: "https://g/", title: "G", attributes: {} });
|
|
11818
|
+
* ```
|
|
11819
|
+
*/
|
|
11820
|
+
function embedFacadeHtml(facade, props, dimensions) {
|
|
11821
|
+
return `<figure class="${EMBED_FIGURE_CLASS}" data-component="${EMBED_COMPONENT_NAME}" data-embed-src="${escapeAttribute(props.src)}" data-embed-title="${escapeAttribute(props.title)}"${dimensions ? ` data-embed-width="${dimensions.width}" data-embed-height="${dimensions.height}" style="aspect-ratio: ${dimensions.width} / ${dimensions.height}; max-width: ${dimensions.width}px;"` : ""}>${renderToString(h(facade, props))}</figure>`;
|
|
11822
|
+
}
|
|
11823
|
+
/**
|
|
11824
|
+
* Normalize the provider's `embed` config value (`boolean | options`) to a plain
|
|
11825
|
+
* {@link EmbedOptions} object for the transform factory.
|
|
11826
|
+
*
|
|
11827
|
+
* @param embed - The raw `FileSystemContentOptions.embed` value (truthy).
|
|
11828
|
+
* @returns The options object (`{}` for the bare `true` form).
|
|
11829
|
+
* @example
|
|
11830
|
+
* ```ts
|
|
11831
|
+
* normalizeEmbedOptions(true); // {}
|
|
11832
|
+
* normalizeEmbedOptions({ facade: MyFacade });
|
|
11719
11833
|
* ```
|
|
11720
11834
|
*/
|
|
11721
|
-
function
|
|
11722
|
-
|
|
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>`;
|
|
11835
|
+
function normalizeEmbedOptions(embed) {
|
|
11836
|
+
return typeof embed === "boolean" ? {} : embed;
|
|
11725
11837
|
}
|
|
11726
11838
|
/**
|
|
11727
11839
|
* Mdast transformer rewriting every `::embed` leaf directive to its facade
|
|
11728
|
-
* HTML node. A directive missing `src`/`title`,
|
|
11729
|
-
*
|
|
11840
|
+
* HTML node. A directive missing `src`/`title`, carrying a non-embeddable URL,
|
|
11841
|
+
* or carrying invalid `width`/`height`, fails the build with the offending
|
|
11842
|
+
* value quoted.
|
|
11730
11843
|
*
|
|
11844
|
+
* @param facade - The facade component to render the inner content with.
|
|
11731
11845
|
* @param tree - The mdast tree to mutate.
|
|
11732
|
-
* @throws {Error} When an `::embed` directive is missing `src` or `title`,
|
|
11733
|
-
*
|
|
11846
|
+
* @throws {Error} When an `::embed` directive is missing `src` or `title`, its
|
|
11847
|
+
* `src` is not embeddable, or its dimensions are invalid.
|
|
11734
11848
|
* @example
|
|
11735
11849
|
* ```ts
|
|
11736
|
-
* embedTransform(tree);
|
|
11850
|
+
* embedTransform(EmbedFacadeButton, tree);
|
|
11737
11851
|
* ```
|
|
11738
11852
|
*/
|
|
11739
|
-
function embedTransform(tree) {
|
|
11853
|
+
function embedTransform(facade, tree) {
|
|
11740
11854
|
visit(tree, (node, index, parent) => {
|
|
11741
11855
|
if (!isEmbedDirective(node)) return;
|
|
11742
11856
|
if (parent === void 0 || index === void 0) return;
|
|
11743
11857
|
const src = node.attributes?.src ?? "";
|
|
11744
11858
|
const title = node.attributes?.title ?? "";
|
|
11745
11859
|
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
|
|
11860
|
+
if (!isEmbeddableUrl(src)) throw new Error(`[web] content: \`::embed\` src must be an http(s) URL, a root-relative path, or a co-located relative path (got "${src}").`);
|
|
11861
|
+
const dimensions = parseDimensions(node.attributes?.width, node.attributes?.height);
|
|
11747
11862
|
const html = {
|
|
11748
11863
|
type: "html",
|
|
11749
|
-
value: embedFacadeHtml(
|
|
11864
|
+
value: embedFacadeHtml(facade, {
|
|
11865
|
+
src,
|
|
11866
|
+
title,
|
|
11867
|
+
...dimensions ? {
|
|
11868
|
+
width: dimensions.width,
|
|
11869
|
+
height: dimensions.height
|
|
11870
|
+
} : {},
|
|
11871
|
+
attributes: collectAttributes(node.attributes)
|
|
11872
|
+
}, dimensions)
|
|
11750
11873
|
};
|
|
11751
11874
|
parent.children[index] = html;
|
|
11752
11875
|
});
|
|
11753
11876
|
}
|
|
11754
11877
|
/**
|
|
11755
|
-
* Remark transform: rewrites `::embed{src="…" title="…"}` leaf directives
|
|
11756
|
-
* static click-to-activate facades (no iframe until the reader clicks — see
|
|
11878
|
+
* Remark transform factory: rewrites `::embed{src="…" title="…"}` leaf directives
|
|
11879
|
+
* into static click-to-activate facades (no iframe until the reader clicks — see
|
|
11757
11880
|
* 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
|
-
*
|
|
11881
|
+
* `trustedContent: true` because the facade is raw HTML the sanitize pass would
|
|
11882
|
+
* strip. The facade's inner content is rendered by `options.facade` (a consumer
|
|
11883
|
+
* Preact component) or the built-in {@link EmbedFacadeButton}.
|
|
11760
11884
|
*
|
|
11885
|
+
* @param options - Embed options (the optional `facade` component).
|
|
11761
11886
|
* @returns An mdast tree transformer.
|
|
11762
11887
|
* @example
|
|
11763
11888
|
* ```ts
|
|
11764
|
-
* unified().use(embedPlugin);
|
|
11889
|
+
* unified().use(embedPlugin, { facade: MyFacade });
|
|
11765
11890
|
* ```
|
|
11766
11891
|
*/
|
|
11767
|
-
function embedPlugin() {
|
|
11768
|
-
|
|
11892
|
+
function embedPlugin(options = {}) {
|
|
11893
|
+
const facade = options.facade ?? EmbedFacadeButton;
|
|
11894
|
+
return (tree) => embedTransform(facade, tree);
|
|
11769
11895
|
}
|
|
11770
11896
|
//#endregion
|
|
11771
11897
|
//#region src/plugins/content/pipeline/mermaid.ts
|
|
@@ -12076,7 +12202,7 @@ function defaultRemarkPlugins(config) {
|
|
|
12076
12202
|
remarkDirective,
|
|
12077
12203
|
pullQuotePlugin
|
|
12078
12204
|
];
|
|
12079
|
-
if (config?.embed) plugins.push(embedPlugin);
|
|
12205
|
+
if (config?.embed) plugins.push([embedPlugin, normalizeEmbedOptions(config.embed)]);
|
|
12080
12206
|
if (config?.mermaid) plugins.push([remarkMermaidDiagrams, normalizeMermaidOptions(config.mermaid)]);
|
|
12081
12207
|
plugins.push([remarkRehype, { allowDangerousHtml: true }]);
|
|
12082
12208
|
return plugins;
|
|
@@ -12318,6 +12444,8 @@ function calculateReadingTime(text) {
|
|
|
12318
12444
|
*/
|
|
12319
12445
|
/** Matches an `<img>` `src` that points at the co-located `images/` dir (relative or root-relative). */
|
|
12320
12446
|
const RELATIVE_IMAGE_SRC = /(<img\b[^>]*?\bsrc=")(?:\.?\/)?images\//g;
|
|
12447
|
+
/** Matches the `data-embed-src` of an `::embed` facade (value captured for path resolution). */
|
|
12448
|
+
const EMBED_SRC_ATTR = /(\bdata-embed-src=")([^"]*)(")/g;
|
|
12321
12449
|
/**
|
|
12322
12450
|
* Build a canonical article URL for a locale + slug.
|
|
12323
12451
|
*
|
|
@@ -12348,6 +12476,56 @@ function rewriteImageUrls(html, slug) {
|
|
|
12348
12476
|
return html.replaceAll(RELATIVE_IMAGE_SRC, `$1/${slug}/images/`);
|
|
12349
12477
|
}
|
|
12350
12478
|
/**
|
|
12479
|
+
* Resolve an `::embed` `src` to the URL the iframe should load. Absolute targets
|
|
12480
|
+
* (`http(s)://…`, root-relative `/…`) pass through unchanged; a co-located
|
|
12481
|
+
* relative path (`./game/index.html`, `../x`, `game/x`) is resolved against the
|
|
12482
|
+
* article base `/<slug>/` into the single shared absolute path the content-assets
|
|
12483
|
+
* build phase copies the bundle to — so it loads identically from every locale
|
|
12484
|
+
* page (mirroring how co-located images resolve). Any `?query`/`#hash` is
|
|
12485
|
+
* preserved verbatim.
|
|
12486
|
+
*
|
|
12487
|
+
* @param value - The raw `data-embed-src` value.
|
|
12488
|
+
* @param slug - Article directory name.
|
|
12489
|
+
* @returns The resolved embed URL.
|
|
12490
|
+
* @example
|
|
12491
|
+
* ```ts
|
|
12492
|
+
* resolveEmbedSource("./game/index.html", "post"); // "/post/game/index.html"
|
|
12493
|
+
* resolveEmbedSource("https://x.dev/", "post"); // "https://x.dev/"
|
|
12494
|
+
* ```
|
|
12495
|
+
*/
|
|
12496
|
+
function resolveEmbedSource(value, slug) {
|
|
12497
|
+
if (/^https?:\/\//i.test(value) || value.startsWith("/")) return value;
|
|
12498
|
+
const tailIndex = value.search(/[?#]/);
|
|
12499
|
+
const rawPath = tailIndex === -1 ? value : value.slice(0, tailIndex);
|
|
12500
|
+
const tail = tailIndex === -1 ? "" : value.slice(tailIndex);
|
|
12501
|
+
const out = [];
|
|
12502
|
+
for (const segment of `${slug}/${rawPath}`.split("/")) {
|
|
12503
|
+
if (segment === "" || segment === ".") continue;
|
|
12504
|
+
if (segment === "..") {
|
|
12505
|
+
out.pop();
|
|
12506
|
+
continue;
|
|
12507
|
+
}
|
|
12508
|
+
out.push(segment);
|
|
12509
|
+
}
|
|
12510
|
+
const trailingSlash = rawPath === "" || rawPath.endsWith("/") ? "/" : "";
|
|
12511
|
+
return `/${out.join("/")}${trailingSlash}${tail}`;
|
|
12512
|
+
}
|
|
12513
|
+
/**
|
|
12514
|
+
* Rewrite every `::embed` facade's relative `data-embed-src` to its shared
|
|
12515
|
+
* absolute `/<slug>/…` path (no-op for already-absolute targets).
|
|
12516
|
+
*
|
|
12517
|
+
* @param html - The rendered article HTML.
|
|
12518
|
+
* @param slug - Article directory name.
|
|
12519
|
+
* @returns The HTML with embed `src`s resolved.
|
|
12520
|
+
* @example
|
|
12521
|
+
* ```ts
|
|
12522
|
+
* rewriteEmbedUrls('<figure data-embed-src="./g/">', "post"); // '… data-embed-src="/post/g/"'
|
|
12523
|
+
* ```
|
|
12524
|
+
*/
|
|
12525
|
+
function rewriteEmbedUrls(html, slug) {
|
|
12526
|
+
return html.replaceAll(EMBED_SRC_ATTR, (_match, prefix, value, suffix) => `${prefix}${resolveEmbedSource(value, slug)}${suffix}`);
|
|
12527
|
+
}
|
|
12528
|
+
/**
|
|
12351
12529
|
* Discover slug-like subdirectories of the content root (direct children not
|
|
12352
12530
|
* starting with `.` or `_`), sorted alphabetically for deterministic ordering.
|
|
12353
12531
|
*
|
|
@@ -12426,7 +12604,7 @@ function fileSystemContent(options) {
|
|
|
12426
12604
|
state.dirtyPaths.delete(filePath);
|
|
12427
12605
|
const { frontmatter, body } = parseFrontmatter(raw, options);
|
|
12428
12606
|
const processor = ensureProcessor(state, options);
|
|
12429
|
-
const html = rewriteImageUrls(String(await processor.process(body)), slug);
|
|
12607
|
+
const html = rewriteEmbedUrls(rewriteImageUrls(String(await processor.process(body)), slug), slug);
|
|
12430
12608
|
const { readingTime, wordCount } = calculateReadingTime(body);
|
|
12431
12609
|
return {
|
|
12432
12610
|
frontmatter,
|
|
@@ -12550,4 +12728,4 @@ const createApp = core.createApp;
|
|
|
12550
12728
|
*/
|
|
12551
12729
|
const createPlugin = core.createPlugin;
|
|
12552
12730
|
//#endregion
|
|
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 };
|
|
12731
|
+
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, EmbedFacadeButton, 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