@riebeckite/core 0.0.11 → 0.0.13

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/README.md CHANGED
@@ -1,94 +1,94 @@
1
- # @riebeckite/core
2
-
3
- The portable framework contract for Riebeckite: configuration, content sources,
4
- the content manager, the Markdown/HTML pipeline, plugin and theme contracts,
5
- diagnostics, and observability.
6
-
7
- [日本語](./README_ja.md)
8
-
9
- ## Overview
10
-
11
- `@riebeckite/core` owns content loading and interpretation and the contracts
12
- that extensions build against. It is intentionally free of HonoX, Vite, and any
13
- specific plugin, theme, or application: those depend on Core, never the reverse.
14
-
15
- A site configures Core through `defineConfig`, and a framework integration such
16
- as [`@riebeckite/honox`](../integrations/honox/README.md) drives the build.
17
- Plugins and themes are plain objects created by `definePlugin` and `defineTheme`.
18
-
19
- ## Installation
20
-
21
- ```sh
22
- pnpm add @riebeckite/core
23
- ```
24
-
25
- Core is also installed transitively by `@riebeckite/honox` and by the
26
- `@riebeckite/plugin-*` and `@riebeckite/theme-*` packages.
27
-
28
- ## Usage
29
-
30
- ```ts
31
- import { defineConfig } from "@riebeckite/core";
32
-
33
- export default defineConfig({
34
- site: { title: "My notes", baseUrl: "https://example.com" },
35
- content: { directory: "../../content" },
36
- });
37
- ```
38
-
39
- The content directory is resolved relative to the application root. See
40
- [Configuration](../../docs/en/configuration.md) for the full field list and the
41
- `projectRoot` / `appRoot` / `configRoot` / `contentRoot` resolution rules.
42
-
43
- ## Public API
44
-
45
- ### Configuration
46
-
47
- - `defineConfig`, `resolveConfig`, `resolveConfigModule`, `isPublished`,
48
- `isExcluded`
49
- - `ConfigValidationError`
50
-
51
- ### Content
52
-
53
- - `ContentManager` — interpretation, manifest, graph, plugin orchestration, and
54
- public-location resolution
55
- - `ContentSource`, `FileSystemContentSource`, `readContentSourceEntry`,
56
- `getContentSourceEntry`
57
- - `queryContentEntries` with the `ContentQuery*` filter and sort types
58
- - `createContentGraph`, `readOnlyContentGraph`, `buildGraphEdges`,
59
- `layoutRadialGraph`
60
- - `resolveDefaultContentLocation`, `attachmentUrl`, and the content path and
61
- extension helpers
62
- - `readContentBuildStateStatus`, `resolveContentBuildStatePath`
63
-
64
- ### Pipeline
65
-
66
- - `Pipeline`, `PipelineOptions`
67
-
68
- ### Plugins
69
-
70
- - `definePlugin`, `resolvePlugins`, `getResolvedPluginMetadata`,
71
- `PluginDependencyError`
72
- - `defineEndpoint`, `createClientEntry`, `createStyleAsset`
73
- - Plugin context, asset, endpoint, diagnostic, SEO, and pipeline types under
74
- `Plugin*`
75
-
76
- ### Themes and contracts
77
-
78
- - `defineTheme` and the `Theme*` types
79
- - `RiebeckiteConfig`, `ResolvedRiebeckiteConfig`, `SiteConfig`,
80
- `PostContent`, `PostFrontmatter`, `PublishStrategy`
81
-
82
- ### Utilities and observability
83
-
84
- - `uniqueStrings`, `escapeHtml`, `escapeHtmlAttribute`, `normalizeTag`,
85
- `calculateReadingTime`, `stripHtml`
86
- - `ConsoleLogger`, `NoopLogger`, `NoopTracer`, `SinkTracer`,
87
- `CompositeTraceSink`
88
-
89
- ## See also
90
-
91
- - [Framework Reference](../../docs/en/framework-reference.md)
92
- - [Content System](../../docs/en/content-system.md)
93
- - [Plugin System](../../docs/en/plugin-system.md) / [Theme System](../../docs/en/theme-system.md)
94
- - [Architecture](../../docs/en/architecture.md)
1
+ # @riebeckite/core
2
+
3
+ The portable framework contract for Riebeckite: configuration, content sources,
4
+ the content manager, the Markdown/HTML pipeline, plugin and theme contracts,
5
+ diagnostics, and observability.
6
+
7
+ [日本語](./README_ja.md)
8
+
9
+ ## Overview
10
+
11
+ `@riebeckite/core` owns content loading and interpretation and the contracts
12
+ that extensions build against. It is intentionally free of HonoX, Vite, and any
13
+ specific plugin, theme, or application: those depend on Core, never the reverse.
14
+
15
+ A site configures Core through `defineConfig`, and a framework integration such
16
+ as [`@riebeckite/honox`](../integrations/honox/README.md) drives the build.
17
+ Plugins and themes are plain objects created by `definePlugin` and `defineTheme`.
18
+
19
+ ## Installation
20
+
21
+ ```sh
22
+ pnpm add @riebeckite/core
23
+ ```
24
+
25
+ Core is also installed transitively by `@riebeckite/honox` and by the
26
+ `@riebeckite/plugin-*` and `@riebeckite/theme-*` packages.
27
+
28
+ ## Usage
29
+
30
+ ```ts
31
+ import { defineConfig } from "@riebeckite/core";
32
+
33
+ export default defineConfig({
34
+ site: { title: "My notes", baseUrl: "https://example.com" },
35
+ content: { directory: "../../content" },
36
+ });
37
+ ```
38
+
39
+ The content directory is resolved relative to the application root. See
40
+ [Configuration](../../docs/en/reference/configuration.md) for the full field list and the
41
+ `projectRoot` / `appRoot` / `configRoot` / `contentRoot` resolution rules.
42
+
43
+ ## Public API
44
+
45
+ ### Configuration
46
+
47
+ - `defineConfig`, `resolveConfig`, `resolveConfigModule`, `isPublished`,
48
+ `isExcluded`
49
+ - `ConfigValidationError`
50
+
51
+ ### Content
52
+
53
+ - `ContentManager` — interpretation, manifest, graph, plugin orchestration, and
54
+ public-location resolution
55
+ - `ContentSource`, `FileSystemContentSource`, `readContentSourceEntry`,
56
+ `getContentSourceEntry`
57
+ - `queryContentEntries` with the `ContentQuery*` filter and sort types
58
+ - `createContentGraph`, `readOnlyContentGraph`, `buildGraphEdges`,
59
+ `layoutRadialGraph`
60
+ - `resolveDefaultContentLocation`, `attachmentUrl`, and the content path and
61
+ extension helpers
62
+ - `readContentBuildStateStatus`, `resolveContentBuildStatePath`
63
+
64
+ ### Pipeline
65
+
66
+ - `Pipeline`, `PipelineOptions`
67
+
68
+ ### Plugins
69
+
70
+ - `definePlugin`, `resolvePlugins`, `getResolvedPluginMetadata`,
71
+ `PluginDependencyError`
72
+ - `defineEndpoint`, `createClientEntry`, `createStyleAsset`
73
+ - Plugin context, asset, endpoint, diagnostic, SEO, and pipeline types under
74
+ `Plugin*`
75
+
76
+ ### Themes and contracts
77
+
78
+ - `defineTheme` and the `Theme*` types
79
+ - `RiebeckiteConfig`, `ResolvedRiebeckiteConfig`, `SiteConfig`,
80
+ `PostContent`, `PostFrontmatter`, `PublishStrategy`
81
+
82
+ ### Utilities and observability
83
+
84
+ - `uniqueStrings`, `escapeHtml`, `escapeHtmlAttribute`, `normalizeTag`,
85
+ `calculateReadingTime`, `stripHtml`
86
+ - `ConsoleLogger`, `NoopLogger`, `NoopTracer`, `SinkTracer`,
87
+ `CompositeTraceSink`
88
+
89
+ ## See also
90
+
91
+ - [Framework Reference](../../docs/en/reference/README.md)
92
+ - [Content System](../../docs/en/framework/content-system.md)
93
+ - [Plugin System](../../docs/en/reference/plugin-api.md) / [Theme System](../../docs/en/reference/theme-api.md)
94
+ - [Architecture](../../docs/en/framework/architecture.md)
package/README_ja.md CHANGED
@@ -1,75 +1,75 @@
1
- # @riebeckite/core
2
-
3
- Riebeckite の移植可能なフレームワーク契約です。設定、コンテンツソース、ContentManager、Markdown/HTML パイプライン、Plugin と Theme の契約、診断、可観測性を担います。
4
-
5
- [English](./README.md)
6
-
7
- ## 概要
8
-
9
- `@riebeckite/core` はコンテンツの読み込みと解釈、および拡張が依存する契約を所有します。HonoX や Vite、特定の Plugin・Theme・アプリケーションには依存しません。依存の向きは常に Core へ向かい、その逆には向かいません。
10
-
11
- サイトは `defineConfig` で Core を設定し、[`@riebeckite/honox`](../integrations/honox/README_ja.md) のような integration がビルドを実行します。Plugin と Theme は `definePlugin`、`defineTheme` が返すただのオブジェクトです。
12
-
13
- ## インストール
14
-
15
- ```sh
16
- pnpm add @riebeckite/core
17
- ```
18
-
19
- `@riebeckite/honox`、`@riebeckite/plugin-*`、`@riebeckite/theme-*` をインストールすると、依存として一緒に入ります。
20
-
21
- ## 使い方
22
-
23
- ```ts
24
- import { defineConfig } from "@riebeckite/core";
25
-
26
- export default defineConfig({
27
- site: { title: "My notes", baseUrl: "https://example.com" },
28
- content: { directory: "../../content" },
29
- });
30
- ```
31
-
32
- コンテンツディレクトリはアプリケーションルート基準で解決します。設定項目の全体と `projectRoot` / `appRoot` / `configRoot` / `contentRoot` の解決規則は [Configuration](../../docs/ja/configuration.md) を参照してください。
33
-
34
- ## 公開 API
35
-
36
- ### 設定
37
-
38
- - `defineConfig`、`resolveConfig`、`resolveConfigModule`、`isPublished`、`isExcluded`
39
- - `ConfigValidationError`
40
-
41
- ### コンテンツ
42
-
43
- - `ContentManager` — 解釈、マニフェスト、グラフ、Plugin の実行、公開 URL の解決
44
- - `ContentSource`、`FileSystemContentSource`、`readContentSourceEntry`、`getContentSourceEntry`
45
- - `queryContentEntries` と `ContentQuery*` のフィルタ・ソート型
46
- - `createContentGraph`、`readOnlyContentGraph`、`buildGraphEdges`、`layoutRadialGraph`
47
- - `resolveDefaultContentLocation`、`attachmentUrl`、コンテンツパスと拡張子のヘルパー
48
- - `readContentBuildStateStatus`、`resolveContentBuildStatePath`
49
-
50
- ### パイプライン
51
-
52
- - `Pipeline`、`PipelineOptions`
53
-
54
- ### Plugin
55
-
56
- - `definePlugin`、`resolvePlugins`、`getResolvedPluginMetadata`、`PluginDependencyError`
57
- - `defineEndpoint`、`createClientEntry`、`createStyleAsset`
58
- - `Plugin*` のコンテキスト、アセット、エンドポイント、診断、SEO、パイプラインの型
59
-
60
- ### Theme と契約
61
-
62
- - `defineTheme` と `Theme*` の型
63
- - `RiebeckiteConfig`、`ResolvedRiebeckiteConfig`、`SiteConfig`、`PostContent`、`PostFrontmatter`、`PublishStrategy`
64
-
65
- ### ユーティリティと可観測性
66
-
67
- - `uniqueStrings`、`escapeHtml`、`escapeHtmlAttribute`、`normalizeTag`、`calculateReadingTime`、`stripHtml`
68
- - `ConsoleLogger`、`NoopLogger`、`NoopTracer`、`SinkTracer`、`CompositeTraceSink`
69
-
70
- ## 関連資料
71
-
72
- - [Framework Reference](../../docs/ja/framework-reference.md)
73
- - [Content System](../../docs/ja/content-system.md)
74
- - [Plugin System](../../docs/ja/plugin-system.md) / [Theme System](../../docs/ja/theme-system.md)
75
- - [Architecture](../../docs/ja/architecture.md)
1
+ # @riebeckite/core
2
+
3
+ Riebeckite の移植可能なフレームワーク契約です。設定、コンテンツソース、ContentManager、Markdown/HTML パイプライン、Plugin と Theme の契約、診断、可観測性を担います。
4
+
5
+ [English](./README.md)
6
+
7
+ ## 概要
8
+
9
+ `@riebeckite/core` はコンテンツの読み込みと解釈、および拡張が依存する契約を所有します。HonoX や Vite、特定の Plugin・Theme・アプリケーションには依存しません。依存の向きは常に Core へ向かい、その逆には向かいません。
10
+
11
+ サイトは `defineConfig` で Core を設定し、[`@riebeckite/honox`](../integrations/honox/README_ja.md) のような integration がビルドを実行します。Plugin と Theme は `definePlugin`、`defineTheme` が返すただのオブジェクトです。
12
+
13
+ ## インストール
14
+
15
+ ```sh
16
+ pnpm add @riebeckite/core
17
+ ```
18
+
19
+ `@riebeckite/honox`、`@riebeckite/plugin-*`、`@riebeckite/theme-*` をインストールすると、依存として一緒に入ります。
20
+
21
+ ## 使い方
22
+
23
+ ```ts
24
+ import { defineConfig } from "@riebeckite/core";
25
+
26
+ export default defineConfig({
27
+ site: { title: "My notes", baseUrl: "https://example.com" },
28
+ content: { directory: "../../content" },
29
+ });
30
+ ```
31
+
32
+ コンテンツディレクトリはアプリケーションルート基準で解決します。設定項目の全体と `projectRoot` / `appRoot` / `configRoot` / `contentRoot` の解決規則は [Configuration](../../docs/ja/reference/configuration.md) を参照してください。
33
+
34
+ ## 公開 API
35
+
36
+ ### 設定
37
+
38
+ - `defineConfig`、`resolveConfig`、`resolveConfigModule`、`isPublished`、`isExcluded`
39
+ - `ConfigValidationError`
40
+
41
+ ### コンテンツ
42
+
43
+ - `ContentManager` — 解釈、マニフェスト、グラフ、Plugin の実行、公開 URL の解決
44
+ - `ContentSource`、`FileSystemContentSource`、`readContentSourceEntry`、`getContentSourceEntry`
45
+ - `queryContentEntries` と `ContentQuery*` のフィルタ・ソート型
46
+ - `createContentGraph`、`readOnlyContentGraph`、`buildGraphEdges`、`layoutRadialGraph`
47
+ - `resolveDefaultContentLocation`、`attachmentUrl`、コンテンツパスと拡張子のヘルパー
48
+ - `readContentBuildStateStatus`、`resolveContentBuildStatePath`
49
+
50
+ ### パイプライン
51
+
52
+ - `Pipeline`、`PipelineOptions`
53
+
54
+ ### Plugin
55
+
56
+ - `definePlugin`、`resolvePlugins`、`getResolvedPluginMetadata`、`PluginDependencyError`
57
+ - `defineEndpoint`、`createClientEntry`、`createStyleAsset`
58
+ - `Plugin*` のコンテキスト、アセット、エンドポイント、診断、SEO、パイプラインの型
59
+
60
+ ### Theme と契約
61
+
62
+ - `defineTheme` と `Theme*` の型
63
+ - `RiebeckiteConfig`、`ResolvedRiebeckiteConfig`、`SiteConfig`、`PostContent`、`PostFrontmatter`、`PublishStrategy`
64
+
65
+ ### ユーティリティと可観測性
66
+
67
+ - `uniqueStrings`、`escapeHtml`、`escapeHtmlAttribute`、`normalizeTag`、`calculateReadingTime`、`stripHtml`
68
+ - `ConsoleLogger`、`NoopLogger`、`NoopTracer`、`SinkTracer`、`CompositeTraceSink`
69
+
70
+ ## 関連資料
71
+
72
+ - [Framework Reference](../../docs/ja/reference/README.md)
73
+ - [Content System](../../docs/ja/framework/content-system.md)
74
+ - [Plugin System](../../docs/ja/reference/plugin-api.md) / [Theme System](../../docs/ja/reference/theme-api.md)
75
+ - [Architecture](../../docs/ja/framework/architecture.md)
@@ -0,0 +1,2 @@
1
+ export type { GraphEdge, GraphLayoutNode, LinkableGraphNode, RadialGraphLayoutOptions, } from "./src/content/graph_layout.js";
2
+ export { buildGraphEdges, layoutRadialGraph, } from "./src/content/graph_layout.js";
package/dist/client.js ADDED
@@ -0,0 +1,43 @@
1
+ // src/content/graph_layout.ts
2
+ function buildGraphEdges(nodes, visibleSlugs = new Set(nodes.map((node) => node.slug))) {
3
+ const edges = /* @__PURE__ */ new Map();
4
+ for (const node of nodes) {
5
+ for (const target of node.outgoing) {
6
+ if (!visibleSlugs.has(node.slug) || !visibleSlugs.has(target)) continue;
7
+ const key = `${node.slug}\0${target}`;
8
+ edges.set(key, { source: node.slug, target });
9
+ }
10
+ }
11
+ return Array.from(edges.values());
12
+ }
13
+ function layoutRadialGraph(nodes, options) {
14
+ const layout = /* @__PURE__ */ new Map();
15
+ const centerX = options.width / 2;
16
+ const centerY = options.height / 2;
17
+ const radius = Math.min(options.width, options.height) * (options.radiusRatio ?? 0.36);
18
+ const minNodeRadius = options.minNodeRadius ?? 8;
19
+ const maxNodeRadius = options.maxNodeRadius ?? 14;
20
+ const selectedIndex = Math.max(
21
+ nodes.findIndex((node) => node.slug === options.centerSlug),
22
+ 0
23
+ );
24
+ nodes.forEach((node, index) => {
25
+ const shiftedIndex = (index - selectedIndex + nodes.length) % nodes.length;
26
+ const angle = shiftedIndex / Math.max(nodes.length, 1) * Math.PI * 2;
27
+ const linkCount = node.outgoing.length + node.backlinks.length;
28
+ const isCenter = node.slug === options.centerSlug;
29
+ layout.set(node.slug, {
30
+ x: isCenter ? centerX : centerX + Math.cos(angle) * radius,
31
+ y: isCenter ? centerY : centerY + Math.sin(angle) * radius,
32
+ radius: clamp(7 + linkCount, minNodeRadius, maxNodeRadius)
33
+ });
34
+ });
35
+ return layout;
36
+ }
37
+ function clamp(value, min, max) {
38
+ return Math.min(Math.max(value, min), max);
39
+ }
40
+ export {
41
+ buildGraphEdges,
42
+ layoutRadialGraph
43
+ };
package/dist/index.d.ts CHANGED
@@ -47,7 +47,8 @@ export type { PluginDiagnostic, PluginDiagnosticLevel, } from "./src/types/plugi
47
47
  export type { PluginEndpoint, PluginEndpointContext, PluginEndpointMethod, PluginEndpointOptions, PluginEndpointRequest, PluginEndpointResponse, } from "./src/types/plugin_endpoint.js";
48
48
  export { defineEndpoint } from "./src/types/plugin_endpoint.js";
49
49
  export type { PluginHeadTag } from "./src/types/plugin_head.js";
50
- export type { HtmlPipeline, MarkdownEmbedFragment, MarkdownPipeline, MarkdownPipelineContext, PipelinePlugin, } from "./src/types/plugin_pipeline.js";
50
+ export type { PluginPage, PluginPageContext, PluginPageType, ResolvedPluginPage, } from "./src/types/plugin_page.js";
51
+ export type { HtmlPipeline, MarkdownEmbedFragment, MarkdownExecutionContext, MarkdownPipeline, MarkdownPipelineContext, PipelinePlugin, } from "./src/types/plugin_pipeline.js";
51
52
  export type { PluginSeoExtension, RenderableFeedEntry, SeoMetadata, WebsiteSeoInput, } from "./src/types/plugin_seo.js";
52
53
  export type { PostContent, PostFrontmatter } from "./src/types/post_content.js";
53
54
  export type { PublishFrontmatter, PublishStrategy, } from "./src/types/publish_strategy.js";
package/dist/index.js CHANGED
@@ -1546,7 +1546,15 @@ var Pipeline = class {
1546
1546
  options;
1547
1547
  isBuildTime;
1548
1548
  pluginCaches = /* @__PURE__ */ new Map();
1549
- async execute(markDownContent, embedDepth = 0, embedTrail = /* @__PURE__ */ new Set(), sourceSlug) {
1549
+ async execute(markDownContent, context = {}) {
1550
+ return await this.executeWithEmbedState(
1551
+ markDownContent,
1552
+ context,
1553
+ 0,
1554
+ new Set(context.sourceSlug ? [context.sourceSlug] : [])
1555
+ );
1556
+ }
1557
+ async executeWithEmbedState(markDownContent, context, embedDepth, embedTrail) {
1550
1558
  const processor = unified();
1551
1559
  const plugins = resolvePlugins(this.options.plugins);
1552
1560
  this.use(processor, remarkParse);
@@ -1560,7 +1568,7 @@ var Pipeline = class {
1560
1568
  this.use(processor, remarkMath);
1561
1569
  this.use(processor, remarkGfm);
1562
1570
  const markdownPipelineContext = {
1563
- sourceSlug,
1571
+ sourceSlug: context.sourceSlug,
1564
1572
  contentIndex: this.contentIndex,
1565
1573
  resolvePermalink: (slug) => this.getPermalink(slug),
1566
1574
  renderNoteEmbed: this.createNoteEmbedRenderer(embedDepth, embedTrail),
@@ -1580,6 +1588,7 @@ var Pipeline = class {
1580
1588
  markdownPipelineContext
1581
1589
  );
1582
1590
  }
1591
+ this.use(processor, normalizeMarkdownLinks, markdownPipelineContext);
1583
1592
  this.use(processor, remarkRehype, { allowDangerousHtml: true });
1584
1593
  this.use(processor, rehypeRaw);
1585
1594
  for (const plugin of plugins) {
@@ -1626,11 +1635,11 @@ var Pipeline = class {
1626
1635
  if (!markdown) return null;
1627
1636
  const nextEmbedTrail = new Set(embedTrail);
1628
1637
  nextEmbedTrail.add(slug);
1629
- const content = await this.execute(
1638
+ const content = await this.executeWithEmbedState(
1630
1639
  markdown,
1640
+ { sourceSlug: slug },
1631
1641
  embedDepth + 1,
1632
- nextEmbedTrail,
1633
- slug
1642
+ nextEmbedTrail
1634
1643
  );
1635
1644
  return content.html;
1636
1645
  };
@@ -1693,6 +1702,81 @@ function selectBlockFragment(markdown, blockId) {
1693
1702
  const line = lines.find((currentLine) => blockIdRe.test(currentLine));
1694
1703
  return line?.replace(blockIdRe, "").trimEnd() || null;
1695
1704
  }
1705
+ function normalizeMarkdownLinks(context) {
1706
+ return (tree) => {
1707
+ visitMarkdownLinkNodes(tree, (node) => {
1708
+ const normalizedUrl = normalizeMarkdownLinkUrl(node.url, context);
1709
+ if (normalizedUrl) node.url = normalizedUrl;
1710
+ });
1711
+ };
1712
+ }
1713
+ function visitMarkdownLinkNodes(node, visitor) {
1714
+ const linkNode = node;
1715
+ if ((node.type === "link" || node.type === "definition") && typeof linkNode.url === "string") {
1716
+ visitor(linkNode);
1717
+ }
1718
+ for (const child of linkNode.children ?? []) {
1719
+ visitMarkdownLinkNodes(child, visitor);
1720
+ }
1721
+ }
1722
+ function normalizeMarkdownLinkUrl(url, context) {
1723
+ if (!context.sourceSlug) return null;
1724
+ if (!isMarkdownContentLink(url)) return null;
1725
+ const [pathAndQuery, hash3 = ""] = splitOnce(url, "#");
1726
+ const [rawPath, query = ""] = splitOnce(pathAndQuery, "?");
1727
+ const decodedPath = decodeUriPath(rawPath);
1728
+ if (!decodedPath?.toLowerCase().endsWith(".md")) return null;
1729
+ const resolvedSlug = resolveRelativeMarkdownSlug(
1730
+ context.sourceSlug,
1731
+ decodedPath
1732
+ );
1733
+ if (!resolvedSlug) return null;
1734
+ const indexedSlug = getExactContentSlug(context, resolvedSlug);
1735
+ if (!indexedSlug) return null;
1736
+ const suffix = `${query ? `?${query}` : ""}${hash3 ? `#${hash3}` : ""}`;
1737
+ return `${context.resolvePermalink(indexedSlug)}${suffix}`;
1738
+ }
1739
+ function getExactContentSlug(context, slug) {
1740
+ try {
1741
+ context.resolvePermalink(slug);
1742
+ return slug;
1743
+ } catch {
1744
+ return context.contentIndex.get(slug.toLowerCase()) ?? null;
1745
+ }
1746
+ }
1747
+ function isMarkdownContentLink(url) {
1748
+ if (url.startsWith("#") || url.startsWith("//")) return false;
1749
+ if (/^[a-z][a-z0-9+.-]*:/i.test(url)) return false;
1750
+ return true;
1751
+ }
1752
+ function resolveRelativeMarkdownSlug(sourceSlug, targetPath) {
1753
+ const sourceDirectory = sourceSlug.includes("/") ? sourceSlug.slice(0, sourceSlug.lastIndexOf("/")) : "";
1754
+ const baseParts = targetPath.startsWith("/") ? [] : sourceDirectory.split("/");
1755
+ const parts = [...baseParts, ...targetPath.replace(/^\/+/, "").split("/")];
1756
+ const normalizedParts = [];
1757
+ for (const part of parts) {
1758
+ if (!part || part === ".") continue;
1759
+ if (part === "..") {
1760
+ if (normalizedParts.length === 0) return null;
1761
+ normalizedParts.pop();
1762
+ continue;
1763
+ }
1764
+ normalizedParts.push(part);
1765
+ }
1766
+ return normalizedParts.join("/").replace(/\.md$/i, "");
1767
+ }
1768
+ function decodeUriPath(path4) {
1769
+ try {
1770
+ return decodeURI(path4);
1771
+ } catch {
1772
+ return null;
1773
+ }
1774
+ }
1775
+ function splitOnce(value, separator) {
1776
+ const index = value.indexOf(separator);
1777
+ if (index < 0) return [value];
1778
+ return [value.slice(0, index), value.slice(index + separator.length)];
1779
+ }
1696
1780
  function selectHeadingFragment(markdown, heading) {
1697
1781
  const lines = markdown.split(/\r?\n/);
1698
1782
  const targetHeading = heading.trim().toLowerCase();
@@ -1982,6 +2066,53 @@ var PluginRuntime = class {
1982
2066
  collectGeneratedOutputs() {
1983
2067
  return this.generatedOutputs.all();
1984
2068
  }
2069
+ async resolvePage(pathname, manifest, contentIndex) {
2070
+ const matches = [];
2071
+ const normalizedPathname = normalizePagePath(pathname);
2072
+ const context = {
2073
+ ...this.createContext(contentIndex),
2074
+ manifest,
2075
+ pathname: normalizedPathname
2076
+ };
2077
+ for (const plugin of this.plugins()) {
2078
+ for (const pageType of plugin.pageTypes ?? []) {
2079
+ const page = await pageType.resolve(
2080
+ this.createPluginContext(plugin, context)
2081
+ );
2082
+ if (page) {
2083
+ matches.push({
2084
+ page: { ...page, type: pageType.id, pluginName: plugin.name },
2085
+ priority: pageType.priority ?? 0
2086
+ });
2087
+ }
2088
+ }
2089
+ }
2090
+ if (matches.length === 0) return null;
2091
+ const highestPriority = Math.max(...matches.map((match) => match.priority));
2092
+ const highest = matches.filter(
2093
+ (match) => match.priority === highestPriority
2094
+ );
2095
+ if (highest.length !== 1) {
2096
+ throw new Error(
2097
+ `Multiple plugin page types match ${normalizedPathname} at priority ${highestPriority}: ${highest.map((match) => match.page.type).join(", ")}`
2098
+ );
2099
+ }
2100
+ return highest[0].page;
2101
+ }
2102
+ async getPagePaths(manifest, contentIndex) {
2103
+ const paths = [];
2104
+ const context = { ...this.createContext(contentIndex), manifest };
2105
+ for (const plugin of this.plugins()) {
2106
+ for (const pageType of plugin.pageTypes ?? []) {
2107
+ const declared = pageType.paths;
2108
+ if (!declared) continue;
2109
+ paths.push(
2110
+ ...typeof declared === "function" ? await declared(this.createPluginContext(plugin, context)) : declared
2111
+ );
2112
+ }
2113
+ }
2114
+ return [...new Set(paths.map(normalizePagePath))];
2115
+ }
1985
2116
  async collectDiagnostics(contentIndex) {
1986
2117
  const tracer = this.observability().tracer;
1987
2118
  return await tracer.span("diagnostics.run", {}, async () => {
@@ -2087,9 +2218,57 @@ var PluginRuntime = class {
2087
2218
  return this.pipelineOptions.observability ?? noopObservability;
2088
2219
  }
2089
2220
  plugins() {
2090
- return resolvePlugins(this.pipelineOptions.plugins);
2221
+ const plugins = resolvePlugins(this.pipelineOptions.plugins);
2222
+ const pageTypes = /* @__PURE__ */ new Map();
2223
+ for (const plugin of plugins) {
2224
+ const declaredPageTypes = plugin.pageTypes;
2225
+ if (declaredPageTypes !== void 0 && !Array.isArray(declaredPageTypes)) {
2226
+ throw new TypeError(`Plugin ${plugin.name} pageTypes must be an array`);
2227
+ }
2228
+ for (const pageType of declaredPageTypes ?? []) {
2229
+ validatePageType(plugin.name, pageType);
2230
+ const previous = pageTypes.get(pageType.id);
2231
+ if (previous) {
2232
+ throw new Error(
2233
+ `Plugin page type "${pageType.id}" is provided by both ${previous} and ${plugin.name}`
2234
+ );
2235
+ }
2236
+ pageTypes.set(pageType.id, plugin.name);
2237
+ }
2238
+ }
2239
+ return plugins;
2091
2240
  }
2092
2241
  };
2242
+ function validatePageType(pluginName, pageType) {
2243
+ if (typeof pageType !== "object" || pageType === null) {
2244
+ throw new TypeError(`Plugin ${pluginName} pageTypes must contain objects`);
2245
+ }
2246
+ const candidate = pageType;
2247
+ if (typeof candidate.id !== "string" || candidate.id.trim() === "") {
2248
+ throw new TypeError(
2249
+ `Plugin ${pluginName} page type id must be a non-empty string`
2250
+ );
2251
+ }
2252
+ if (typeof candidate.resolve !== "function") {
2253
+ throw new TypeError(
2254
+ `Plugin ${pluginName} page type "${candidate.id}" must provide a resolve function`
2255
+ );
2256
+ }
2257
+ if (candidate.paths !== void 0 && !Array.isArray(candidate.paths) && typeof candidate.paths !== "function") {
2258
+ throw new TypeError(
2259
+ `Plugin ${pluginName} page type "${candidate.id}" paths must be an array or function`
2260
+ );
2261
+ }
2262
+ if (candidate.priority !== void 0 && (typeof candidate.priority !== "number" || !Number.isFinite(candidate.priority))) {
2263
+ throw new TypeError(
2264
+ `Plugin ${pluginName} page type "${candidate.id}" priority must be a finite number`
2265
+ );
2266
+ }
2267
+ }
2268
+ function normalizePagePath(pathname) {
2269
+ const path4 = `/${pathname.split("/").filter(Boolean).join("/")}`;
2270
+ return path4 === "/" ? path4 : path4.replace(/\/$/, "");
2271
+ }
2093
2272
  function countSeverity(diagnostics, severity) {
2094
2273
  return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
2095
2274
  }
@@ -2672,7 +2851,10 @@ function createContentIdIndex(entries) {
2672
2851
  return byContentId;
2673
2852
  }
2674
2853
  function getManifestTitle(slug, title) {
2675
- return typeof title === "string" && title.trim() ? title : slug;
2854
+ if (typeof title === "string" && title.trim()) {
2855
+ return title.trim();
2856
+ }
2857
+ return slug.split("/").at(-1) ?? slug;
2676
2858
  }
2677
2859
  function uniqueAssets(assets) {
2678
2860
  return Array.from(
@@ -2785,12 +2967,9 @@ var ContentManager = class {
2785
2967
  this.pipelineOptions,
2786
2968
  this.isBuildTime
2787
2969
  );
2788
- const content = await this.pipeline.execute(
2789
- rawPost,
2790
- 0,
2791
- /* @__PURE__ */ new Set([slug]),
2792
- slug
2793
- );
2970
+ const content = await this.pipeline.execute(rawPost, {
2971
+ sourceSlug: slug
2972
+ });
2794
2973
  await this.pluginRuntime.runPostHook(
2795
2974
  "onPostParsed",
2796
2975
  slug,
@@ -2885,6 +3064,26 @@ var ContentManager = class {
2885
3064
  const manifest = await this.getManifest();
2886
3065
  return manifest.diagnostics;
2887
3066
  }
3067
+ /** Resolves a page contributed by an enabled plugin. */
3068
+ async resolvePage(pathname) {
3069
+ const [manifest, contentIndex] = await Promise.all([
3070
+ this.getManifest(),
3071
+ this.getContentIndex()
3072
+ ]);
3073
+ return await this.pluginRuntime.resolvePage(
3074
+ pathname,
3075
+ manifest,
3076
+ contentIndex
3077
+ );
3078
+ }
3079
+ /** Returns plugin page paths for SSG enumeration. */
3080
+ async getPagePaths() {
3081
+ const [manifest, contentIndex] = await Promise.all([
3082
+ this.getManifest(),
3083
+ this.getContentIndex()
3084
+ ]);
3085
+ return await this.pluginRuntime.getPagePaths(manifest, contentIndex);
3086
+ }
2888
3087
  async inspect() {
2889
3088
  const [entries, contentIndex] = await Promise.all([
2890
3089
  this.entryReader.getEntries(),
@@ -1,6 +1,7 @@
1
1
  import type { PipelineOptions } from "../pipeline.js";
2
2
  import type { ContentManifest, ContentPublicLocation } from "../types/content_manifest.js";
3
3
  import type { Diagnostic } from "../types/diagnostic.js";
4
+ import type { ResolvedPluginPage } from "../types/plugin_page.js";
4
5
  import type { PostContent } from "../types/post_content.js";
5
6
  import type { ResolvedRiebeckiteConfig } from "../types/resolved_riebeckite_config.js";
6
7
  import { type ContentBuildPreparation } from "./content_build_coordinator.js";
@@ -45,6 +46,10 @@ export declare class ContentManager {
45
46
  getBacklinks(targetSlug: string): Promise<Backlink[]>;
46
47
  getContentGraph(): Promise<ContentGraph>;
47
48
  getDiagnostics(): Promise<Diagnostic[]>;
49
+ /** Resolves a page contributed by an enabled plugin. */
50
+ resolvePage(pathname: string): Promise<ResolvedPluginPage | null>;
51
+ /** Returns plugin page paths for SSG enumeration. */
52
+ getPagePaths(): Promise<readonly string[]>;
48
53
  inspect(): Promise<ContentInspection>;
49
54
  dispose(): Promise<void>;
50
55
  getContentLocations(): Promise<ReadonlyMap<string, ContentPublicLocation>>;
@@ -1,6 +1,7 @@
1
1
  import type { ContentSource } from "./content/content_source.js";
2
2
  import type { Observability } from "./observability.js";
3
3
  import type { RiebeckitePlugin } from "./types/plugin.js";
4
+ import type { MarkdownExecutionContext } from "./types/plugin_pipeline.js";
4
5
  import type { PostContent } from "./types/post_content.js";
5
6
  export interface PipelineOptions {
6
7
  plugins?: RiebeckitePlugin[];
@@ -16,7 +17,8 @@ export declare class Pipeline {
16
17
  private isBuildTime;
17
18
  private pluginCaches;
18
19
  constructor(contentIndex: Map<string, string>, permalinks: ReadonlyMap<string, string>, getMarkdownBySlug?: (slug: string) => Promise<string>, options?: PipelineOptions, isBuildTime?: boolean);
19
- execute(markDownContent: string, embedDepth?: number, embedTrail?: Set<string>, sourceSlug?: string): Promise<PostContent>;
20
+ execute(markDownContent: string, context?: MarkdownExecutionContext): Promise<PostContent>;
21
+ private executeWithEmbedState;
20
22
  private getPermalink;
21
23
  private use;
22
24
  private createNoteEmbedRenderer;
@@ -3,6 +3,7 @@ import type { ContentLocationInput, ContentManifest, ContentManifestEntry, Conte
3
3
  import type { Diagnostic } from "../types/diagnostic.js";
4
4
  import type { GeneratedOutput } from "../types/generated_output.js";
5
5
  import { type PluginClientEntry } from "../types/plugin_asset.js";
6
+ import type { ResolvedPluginPage } from "../types/plugin_page.js";
6
7
  import type { PostContent } from "../types/post_content.js";
7
8
  /**
8
9
  * Explicit build lifecycle stages. `idle` means no build lifecycle has been
@@ -53,6 +54,8 @@ export declare class PluginRuntime {
53
54
  collectClientEntries(): PluginClientEntry[];
54
55
  /** Returns registered outputs sorted by path for a deterministic build. */
55
56
  collectGeneratedOutputs(): GeneratedOutput[];
57
+ resolvePage(pathname: string, manifest: ContentManifest, contentIndex: Map<string, string>): Promise<ResolvedPluginPage | null>;
58
+ getPagePaths(manifest: ContentManifest, contentIndex: Map<string, string>): Promise<readonly string[]>;
56
59
  collectDiagnostics(contentIndex: Map<string, string>): Promise<Diagnostic[]>;
57
60
  private runBuildLifecycle;
58
61
  private createContext;
@@ -3,6 +3,7 @@ import type { Diagnostic } from "./diagnostic.js";
3
3
  import type { PluginAsset, PluginClientEntry } from "./plugin_asset.js";
4
4
  import type { PluginContentContext, PluginContentLocationAugmentContext, PluginContentLocationResolver, PluginContentRenderer, PluginContext, PluginGeneratedHtmlInspector, PluginGraphContext, PluginLifecycleContext, PluginManifestContext, PluginPostContext } from "./plugin_context.js";
5
5
  import type { PluginEndpoint } from "./plugin_endpoint.js";
6
+ import type { PluginPageType } from "./plugin_page.js";
6
7
  import type { HtmlPipeline, MarkdownPipeline, MarkdownPipelineContext, PipelinePlugin } from "./plugin_pipeline.js";
7
8
  import type { PluginSeoExtension } from "./plugin_seo.js";
8
9
  export type RiebeckitePlugin<TOptions = unknown> = {
@@ -57,6 +58,8 @@ export type RiebeckitePlugin<TOptions = unknown> = {
57
58
  seo?: PluginSeoExtension;
58
59
  extendContentGraph?(context: PluginGraphContext): void | Promise<void>;
59
60
  renderers?: PluginContentRenderer[];
61
+ /** Framework-independent page types contributed by this plugin. */
62
+ pageTypes?: PluginPageType[];
60
63
  };
61
64
  export type PluginInput = RiebeckitePlugin | false | null | undefined;
62
65
  export type ResolvedPluginMetadata = {
@@ -0,0 +1,36 @@
1
+ import type { ContentManifest } from "./content_manifest.js";
2
+ import type { PluginContext } from "./plugin_context.js";
3
+ import type { PluginHeadTag } from "./plugin_head.js";
4
+ /** A framework-independent page supplied by a Riebeckite plugin. */
5
+ export type PluginPage = {
6
+ /** Stable identifier of the page type that produced this page. */
7
+ type: string;
8
+ /** Canonical, absolute URL path. */
9
+ pathname: string;
10
+ /** HTML for the page body. The site owns the surrounding document frame. */
11
+ body: string;
12
+ title?: string;
13
+ description?: string;
14
+ /** Optional document metadata rendered by the site's document frame. */
15
+ headTags?: readonly PluginHeadTag[];
16
+ };
17
+ export type PluginPageContext = PluginContext & {
18
+ /** The public manifest. Unpublished content is not exposed through pages. */
19
+ manifest: ContentManifest;
20
+ /** Request pathname, normalized to an absolute path. */
21
+ pathname: string;
22
+ };
23
+ export type PluginPageType = {
24
+ /** Globally unique, stable page type identifier. */
25
+ id: string;
26
+ /** Paths emitted by SSG for this type. */
27
+ paths?: readonly string[] | ((context: PluginContext & {
28
+ manifest: ContentManifest;
29
+ }) => readonly string[] | Promise<readonly string[]>);
30
+ /** Higher values win when more than one type matches a request. */
31
+ priority?: number;
32
+ resolve(context: PluginPageContext): PluginPage | null | Promise<PluginPage | null>;
33
+ };
34
+ export type ResolvedPluginPage = PluginPage & {
35
+ pluginName: string;
36
+ };
@@ -6,9 +6,14 @@ export type MarkdownPipeline = {
6
6
  use(plugin: PipelinePlugin, options?: unknown): void;
7
7
  };
8
8
  export type HtmlPipeline = MarkdownPipeline;
9
- export type MarkdownPipelineContext = {
9
+ /** Identifies the source being processed by a Markdown pipeline execution. */
10
+ export type MarkdownExecutionContext = {
10
11
  /** Slug of the content currently being rendered, when available. */
11
12
  sourceSlug?: string;
13
+ };
14
+ export type MarkdownPipelineContext = {
15
+ /** Source identity supplied for this execution, when available. */
16
+ sourceSlug?: MarkdownExecutionContext["sourceSlug"];
12
17
  contentIndex: Map<string, string>;
13
18
  resolvePermalink: (slug: string) => string;
14
19
  renderNoteEmbed?: (slug: string, fragment: MarkdownEmbedFragment | null) => Promise<string | null>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riebeckite/core",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,6 +12,12 @@
12
12
  "source": "./index.ts",
13
13
  "default": "./dist/index.js"
14
14
  },
15
+ "./client": {
16
+ "types": "./dist/client.d.ts",
17
+ "import": "./dist/client.js",
18
+ "source": "./client.ts",
19
+ "default": "./dist/client.js"
20
+ },
15
21
  "./package.json": "./package.json"
16
22
  },
17
23
  "files": [
@@ -43,7 +49,7 @@
43
49
  },
44
50
  "devDependencies": {
45
51
  "tsx": "^4.22.4",
46
- "@riebeckite/test": "0.0.11"
52
+ "@riebeckite/test": "0.0.13"
47
53
  },
48
54
  "repository": {
49
55
  "type": "git",