@riebeckite/core 0.0.18 → 0.0.20

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
@@ -37,7 +37,7 @@ export default defineConfig({
37
37
  ```
38
38
 
39
39
  The content directory is resolved relative to the application root. See
40
- [Configuration](../../docs/docs/reference/configuration.en.md) for the full field list and the
40
+ [Configuration](../../docs/docs/reference/configuration.md) for the full field list and the
41
41
  `projectRoot` / `appRoot` / `configRoot` / `contentRoot` resolution rules.
42
42
 
43
43
  ## Public API
@@ -88,8 +88,8 @@ The content directory is resolved relative to the application root. See
88
88
 
89
89
  ## See also
90
90
 
91
- - [Framework Reference](../../docs/docs/reference/README.en.md)
92
- - [Content System](../../docs/docs/framework/content-system.en.md)
93
- - [Plugin System](../../docs/docs/reference/plugin-api.en.md) / [Theme System](../../docs/docs/reference/theme-api.en.md)
94
- - [Architecture](../../docs/docs/framework/architecture.en.md)
91
+ - [Framework Reference](../../docs/docs/reference/README.md)
92
+ - [Content System](../../docs/docs/framework/content-system.md)
93
+ - [Plugin System](../../docs/docs/reference/plugin-api.md) / [Theme System](../../docs/docs/reference/theme-api.md)
94
+ - [Architecture](../../docs/docs/framework/architecture.md)
95
95
 
package/README_ja.md CHANGED
@@ -29,7 +29,7 @@ export default defineConfig({
29
29
  });
30
30
  ```
31
31
 
32
- コンテンツディレクトリはアプリケーションルート基準で解決します。設定項目の全体と `projectRoot` / `appRoot` / `configRoot` / `contentRoot` の解決規則は [Configuration](../../docs/docs/reference/configuration.md) を参照してください。
32
+ コンテンツディレクトリはアプリケーションルート基準で解決します。設定項目の全体と `projectRoot` / `appRoot` / `configRoot` / `contentRoot` の解決規則は [Configuration](../../docs/docs/reference/configuration.ja.md) を参照してください。
33
33
 
34
34
  ## 公開 API
35
35
 
@@ -69,8 +69,8 @@ export default defineConfig({
69
69
 
70
70
  ## 関連資料
71
71
 
72
- - [Framework Reference](../../docs/docs/reference/README.md)
73
- - [Content System](../../docs/docs/framework/content-system.md)
74
- - [Plugin System](../../docs/docs/reference/plugin-api.md) / [Theme System](../../docs/docs/reference/theme-api.md)
75
- - [Architecture](../../docs/docs/framework/architecture.md)
72
+ - [Framework Reference](../../docs/docs/reference/README.ja.md)
73
+ - [Content System](../../docs/docs/framework/content-system.ja.md)
74
+ - [Plugin System](../../docs/docs/reference/plugin-api.ja.md) / [Theme System](../../docs/docs/reference/theme-api.ja.md)
75
+ - [Architecture](../../docs/docs/framework/architecture.ja.md)
76
76
 
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  export { defineConfig, isExcluded, isPublished, resolveConfig, resolveConfigModule, } from "./src/config.js";
2
- export { type ConfigDeprecationNotice, collectConfigDeprecationDiagnostics, } from "./src/config_deprecations.js";
3
2
  export { ConfigValidationError } from "./src/config_validation.js";
4
3
  export { ATTACHMENTS_BASE_PATH, attachmentUrl, getExtension, isAttachmentPath, isImagePath, isMarkdownPath, normalizeContentPath, } from "./src/content/attachment.js";
5
4
  export type { ContentBuildState, FingerprintedContentEntry, } from "./src/content/content_build_state.js";
@@ -33,19 +32,19 @@ export { buildOutputInventory, determineOutputChanges, htmlOutputPath, } from ".
33
32
  export type { PublishingResolutionOptions, PublishingVisibility, ResolvedPublishingState, } from "./src/content/publishing.js";
34
33
  export { resolvePublishingBuildTime, resolvePublishingState, } from "./src/content/publishing.js";
35
34
  export { readOnlyContentGraph } from "./src/content/read_only_content_graph.js";
36
- export { createDeprecationDiagnostic, type DeprecationKind, type DeprecationNotice, formatDeprecationMessage, } from "./src/deprecation.js";
37
35
  export type { LogContext, Logger, LogLevel, Observability, ObservabilityValue, TraceAttributes, TraceEvent, Tracer, TraceSink, TraceSpan, } from "./src/observability.js";
38
36
  export { CompositeTraceSink, ConsoleLogger, NoopLogger, NoopTracer, SinkTracer, } from "./src/observability.js";
39
37
  export type { PipelineOptions } from "./src/pipeline.js";
40
38
  export { Pipeline } from "./src/pipeline.js";
41
39
  export type { PluginCache } from "./src/plugin/plugin_cache.js";
42
40
  export { PluginDependencyError, type PluginDependencyErrorKind, } from "./src/plugin/plugin_dependency_error.js";
41
+ export { PluginHookError } from "./src/plugin/plugin_lifecycle.js";
43
42
  export type { CreatePluginMemoOptions, PluginMemo, } from "./src/plugin/plugin_memo.js";
44
43
  export { createPluginMemo, stableStringify, } from "./src/plugin/plugin_memo.js";
45
44
  export { resolveBuildOutputDirectory, resolveManagedStateRoot, } from "./src/project/project_layout.js";
46
45
  export type { ConfigValidationIssue, PluginOptionsValidator, } from "./src/types/config_validation.js";
47
- export type { ContentAsset, ContentBodySlot, ContentLink, ContentLinkKind, ContentLocationInput, ContentManifest, ContentManifestEntry, ContentManifestPluginClientEntry, ContentPublicLocation, ContentRedirect, FolderLocation, PluginPageRoute, } from "./src/types/content_manifest.js";
48
- export { appendContentBodySlot } from "./src/types/content_manifest.js";
46
+ export type { ContentAsset, ContentBodySlot, ContentBodySlots, ContentLink, ContentLinkKind, ContentLocationInput, ContentManifest, ContentManifestEntry, ContentManifestPluginClientEntry, ContentPublicLocation, ContentRedirect, FolderLocation, PluginPageRoute, } from "./src/types/content_manifest.js";
47
+ export { ARTICLE_SLOT, appendContentBodySlot, } from "./src/types/content_manifest.js";
49
48
  export type { Diagnostic, DiagnosticCode, DiagnosticSeverity, } from "./src/types/diagnostic.js";
50
49
  export type { ContentAssetOutputInput, GeneratedOutput, GeneratedOutputContent, GeneratedOutputInput, GeneratedOutputSink, } from "./src/types/generated_output.js";
51
50
  export { createUnavailableGeneratedOutputSink, normalizeContentAssetOutputPath, normalizeGeneratedOutputPath, } from "./src/types/generated_output.js";
@@ -74,5 +73,7 @@ export { defineTheme } from "./src/types/theme_config.js";
74
73
  export { uniqueStrings } from "./src/utils/collections.js";
75
74
  export { attachErrorPath } from "./src/utils/error.js";
76
75
  export { escapeHtml, escapeHtmlAttribute, escapeScriptJson, } from "./src/utils/html.js";
76
+ export type { MarkdownFenceLine } from "./src/utils/markdown.js";
77
+ export { scanMarkdownFenceLines } from "./src/utils/markdown.js";
77
78
  export { normalizeTag } from "./src/utils/tags.js";
78
79
  export { calculateReadingTime, stripHtml } from "./src/utils/text.js";
package/dist/index.js CHANGED
@@ -778,46 +778,6 @@ function globToRegExp(glob) {
778
778
  return new RegExp(`^${re}$`);
779
779
  }
780
780
 
781
- // src/deprecation.ts
782
- function createDeprecationDiagnostic(notice) {
783
- return {
784
- code: `deprecated-${notice.kind}`,
785
- severity: "warning",
786
- message: formatDeprecationMessage(notice),
787
- target: notice.target,
788
- suggestion: notice.replacement ? `Use ${notice.replacement}. ${notice.action}` : notice.action,
789
- meta: { deprecation: notice }
790
- };
791
- }
792
- function formatDeprecationMessage(notice) {
793
- const lines = [
794
- `Deprecated: ${notice.target}`,
795
- `Deprecated since: ${notice.deprecatedSince}`
796
- ];
797
- if (notice.replacement) lines.push(`Use: ${notice.replacement}`);
798
- lines.push(`Migration: ${notice.action}`);
799
- if (notice.documentationUrl) lines.push(`See: ${notice.documentationUrl}`);
800
- if (notice.removedIn) lines.push(`Removal planned: ${notice.removedIn}`);
801
- return lines.join("\n");
802
- }
803
-
804
- // src/config_deprecations.ts
805
- var configDeprecations = [];
806
- function collectConfigDeprecationDiagnostics(config, notices = configDeprecations) {
807
- return notices.filter((notice) => hasConfigPath(config, notice.path)).map(createDeprecationDiagnostic);
808
- }
809
- function hasConfigPath(config, path6) {
810
- let value = config;
811
- for (const segment of path6.split(".")) {
812
- if (!isRecord2(value) || !(segment in value)) return false;
813
- value = value[segment];
814
- }
815
- return value !== void 0;
816
- }
817
- function isRecord2(value) {
818
- return typeof value === "object" && value !== null && !Array.isArray(value);
819
- }
820
-
821
781
  // src/content/image_extensions.ts
822
782
  var IMAGE_EXTENSIONS = [
823
783
  "png",
@@ -895,7 +855,7 @@ async function readContentBuildStateStatus(statePath) {
895
855
  }
896
856
  throw error;
897
857
  }
898
- if (!isRecord3(parsed)) {
858
+ if (!isRecord2(parsed)) {
899
859
  return { kind: "invalid", path: statePath, reason: "invalid-shape" };
900
860
  }
901
861
  if (parsed.version !== CONTENT_BUILD_STATE_VERSION) {
@@ -935,15 +895,15 @@ function resolveContentBuildStatePath(config, contentDirectory) {
935
895
  );
936
896
  }
937
897
  function isContentBuildState(value) {
938
- return isRecord3(value) && value.version === CONTENT_BUILD_STATE_VERSION && isContentBuildStateShape(value);
898
+ return isRecord2(value) && value.version === CONTENT_BUILD_STATE_VERSION && isContentBuildStateShape(value);
939
899
  }
940
900
  function isContentBuildStateShape(value) {
941
- if (!isRecord3(value.entries) || !isRecord3(value.contentIndex) || value.outputs !== void 0 && !isOutputDescriptors(value.outputs)) {
901
+ if (!isRecord2(value.entries) || !isRecord2(value.contentIndex) || value.outputs !== void 0 && !isOutputDescriptors(value.outputs)) {
942
902
  return false;
943
903
  }
944
904
  return Object.values(value.entries).every(
945
- (entry) => isRecord3(entry) && typeof entry.fingerprint === "string" && entry.fingerprint.length > 0 && Array.isArray(entry.aliases) && entry.aliases.every((alias) => typeof alias === "string") && Array.isArray(entry.dependencies) && entry.dependencies.every(
946
- (dependency) => isRecord3(dependency) && (dependency.kind === "content" || dependency.kind === "file") && typeof dependency.id === "string"
905
+ (entry) => isRecord2(entry) && typeof entry.fingerprint === "string" && entry.fingerprint.length > 0 && Array.isArray(entry.aliases) && entry.aliases.every((alias) => typeof alias === "string") && Array.isArray(entry.dependencies) && entry.dependencies.every(
906
+ (dependency) => isRecord2(dependency) && (dependency.kind === "content" || dependency.kind === "file") && typeof dependency.id === "string"
947
907
  )
948
908
  ) && Object.values(value.contentIndex).every(
949
909
  (entry) => typeof entry === "string"
@@ -951,7 +911,7 @@ function isContentBuildStateShape(value) {
951
911
  }
952
912
  function isOutputDescriptors(value) {
953
913
  return Array.isArray(value) && value.every(
954
- (output) => isRecord3(output) && (output.kind === "content" || output.kind === "redirect" || output.kind === "plugin-page" || output.kind === "generated") && isOutputPath(output.path) && typeof output.producer === "string" && Array.isArray(output.dependencies) && output.dependencies.every(isOutputDependency)
914
+ (output) => isRecord2(output) && (output.kind === "content" || output.kind === "redirect" || output.kind === "plugin-page" || output.kind === "generated") && isOutputPath(output.path) && typeof output.producer === "string" && Array.isArray(output.dependencies) && output.dependencies.every(isOutputDependency)
955
915
  );
956
916
  }
957
917
  function isOutputPath(value) {
@@ -961,11 +921,11 @@ function isOutputPath(value) {
961
921
  return value.split("/").every((segment) => segment !== "." && segment !== "..");
962
922
  }
963
923
  function isOutputDependency(value) {
964
- if (!isRecord3(value) || typeof value.type !== "string") return false;
924
+ if (!isRecord2(value) || typeof value.type !== "string") return false;
965
925
  if (value.type === "global" || value.type === "unknown") return true;
966
926
  return value.type === "content" && typeof value.slug === "string" || value.type === "tag" && typeof value.tag === "string" || value.type === "folder" && typeof value.folder === "string";
967
927
  }
968
- function isRecord3(value) {
928
+ function isRecord2(value) {
969
929
  return typeof value === "object" && value !== null && !Array.isArray(value);
970
930
  }
971
931
  function isNotFoundError(error) {
@@ -1727,47 +1687,65 @@ async function writeAtomically(targetPath, dir, serialized) {
1727
1687
  throw error;
1728
1688
  }
1729
1689
  }
1690
+ function isMissingFileError(error) {
1691
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
1692
+ }
1730
1693
  function createPersistentContentCache(options) {
1731
1694
  const { config, logger, tracer } = options;
1732
1695
  const baseDir = cacheDirectory(config);
1733
1696
  const versionDir = versionDirectory(baseDir);
1734
- return {
1735
- async get(key) {
1736
- if (!config.cache?.enabled) return void 0;
1697
+ const cache = {
1698
+ async lookup(key) {
1699
+ if (!config.cache?.enabled) {
1700
+ return { status: "miss", reason: "disabled" };
1701
+ }
1737
1702
  const filePath = cacheFilePath(baseDir, key);
1703
+ let raw;
1738
1704
  try {
1739
- const raw = await fs2.readFile(filePath, "utf8");
1740
- const entry = JSON.parse(raw);
1741
- if (entry.schemaVersion !== CONTENT_CACHE_SCHEMA_VERSION) {
1742
- tracer?.event("persistentContentCache.schemaMismatch", { key });
1743
- return void 0;
1744
- }
1745
- if (entry.key !== key) {
1746
- tracer?.event("persistentContentCache.keyMismatch", { key });
1747
- return void 0;
1748
- }
1749
- if (!isValidCacheEntry(entry)) {
1750
- tracer?.event("persistentContentCache.invalidShape", { key });
1751
- return void 0;
1752
- }
1753
- tracer?.event("persistentContentCache.found", { key });
1754
- return entry;
1705
+ raw = await fs2.readFile(filePath, "utf8");
1755
1706
  } catch (error) {
1756
- if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
1757
- tracer?.event("persistentContentCache.miss", { key });
1758
- return void 0;
1707
+ if (isMissingFileError(error)) {
1708
+ return { status: "miss", reason: "no-entry" };
1759
1709
  }
1760
- if (error instanceof SyntaxError) {
1761
- logger?.warn(
1762
- "Persistent content cache entry is corrupted and will be ignored.",
1763
- {
1764
- key
1765
- }
1766
- );
1767
- return void 0;
1768
- }
1769
- throw error;
1710
+ logger?.warn(
1711
+ "Persistent content cache entry could not be read and will be ignored.",
1712
+ {
1713
+ key,
1714
+ error: error instanceof Error ? error.message : String(error)
1715
+ }
1716
+ );
1717
+ return { status: "miss", reason: "unreadable" };
1770
1718
  }
1719
+ let entry;
1720
+ try {
1721
+ entry = JSON.parse(raw);
1722
+ } catch (error) {
1723
+ logger?.warn(
1724
+ "Persistent content cache entry is corrupted and will be ignored.",
1725
+ {
1726
+ key,
1727
+ error: error instanceof Error ? error.message : String(error)
1728
+ }
1729
+ );
1730
+ return { status: "miss", reason: "corrupted" };
1731
+ }
1732
+ if (typeof entry !== "object" || entry === null) {
1733
+ return { status: "miss", reason: "invalid-entry" };
1734
+ }
1735
+ if (entry.schemaVersion !== CONTENT_CACHE_SCHEMA_VERSION) {
1736
+ return { status: "miss", reason: "schema-mismatch" };
1737
+ }
1738
+ if (entry.key !== key) {
1739
+ return { status: "miss", reason: "key-mismatch" };
1740
+ }
1741
+ if (!isValidCacheEntry(entry)) {
1742
+ return { status: "miss", reason: "invalid-entry" };
1743
+ }
1744
+ return { status: "hit", entry };
1745
+ },
1746
+ async get(key) {
1747
+ const result = await cache.lookup(key);
1748
+ return result.status === "hit" ? result.entry : void 0;
1771
1749
  },
1772
1750
  async set(key, value) {
1773
1751
  if (!config.cache?.enabled) return;
@@ -1778,8 +1756,18 @@ function createPersistentContentCache(options) {
1778
1756
  "Persistent content cache values must be JSON serializable."
1779
1757
  );
1780
1758
  }
1781
- await writeAtomically(filePath, versionDir, serialized);
1782
- tracer?.event("persistentContentCache.write", { key });
1759
+ try {
1760
+ await writeAtomically(filePath, versionDir, serialized);
1761
+ tracer?.event("persistentContentCache.write", { key });
1762
+ } catch (error) {
1763
+ logger?.warn(
1764
+ "Persistent content cache entry could not be written and will be ignored.",
1765
+ {
1766
+ key,
1767
+ error: error instanceof Error ? error.message : String(error)
1768
+ }
1769
+ );
1770
+ }
1783
1771
  },
1784
1772
  async clear() {
1785
1773
  try {
@@ -1788,6 +1776,7 @@ function createPersistentContentCache(options) {
1788
1776
  }
1789
1777
  }
1790
1778
  };
1779
+ return cache;
1791
1780
  }
1792
1781
  function isValidCacheEntry(entry) {
1793
1782
  return typeof entry === "object" && entry !== null && typeof entry.key === "string" && typeof entry.schemaVersion === "number" && Array.isArray(entry.dependencies) && entry.dependencies.every(isValidDependency) && typeof entry.value === "object" && entry.value !== null && typeof entry.value.html === "string" && typeof entry.value.frontmatter === "object" && entry.value.frontmatter !== null && !Array.isArray(entry.value.frontmatter);
@@ -2136,41 +2125,15 @@ var Pipeline = class {
2136
2125
  frontmatter,
2137
2126
  pipelineFingerprint: this.pipelineFingerprint
2138
2127
  });
2139
- const cachedEntry = await this.persistentCache.get(cacheKey);
2140
- if (cachedEntry) {
2141
- const validationTracker = createContentDependencyTracker(
2142
- this.options.contentSource
2128
+ const lookup = await this.persistentCache.lookup(cacheKey);
2129
+ if (lookup.status === "hit") {
2130
+ const invalidDependency = await this.findInvalidDependency(
2131
+ lookup.entry.dependencies
2143
2132
  );
2144
- let valid = true;
2145
- for (const dep of cachedEntry.dependencies) {
2146
- if (dep.kind === "content") {
2147
- const content = await validationTracker.readContent(
2148
- dep.id,
2149
- async () => await this.getMarkdownBySlug?.(dep.id) ?? ""
2150
- );
2151
- if (fingerprintContent(content) !== dep.fingerprint) {
2152
- valid = false;
2153
- break;
2154
- }
2155
- } else if (dep.kind === "file") {
2156
- const fileContent = await validationTracker.contentSource.read({
2157
- path: dep.id
2158
- });
2159
- if (fingerprintContent(fileContent) !== dep.fingerprint) {
2160
- valid = false;
2161
- break;
2162
- }
2163
- } else if (dep.kind === "link") {
2164
- if (fingerprintContent(this.resolveLinkDependency(dep.id)) !== dep.fingerprint) {
2165
- valid = false;
2166
- break;
2167
- }
2168
- }
2169
- }
2170
- if (valid) {
2133
+ if (!invalidDependency) {
2171
2134
  this.reportContentDependencies(
2172
2135
  sourceSlug,
2173
- cachedEntry.dependencies
2136
+ lookup.entry.dependencies
2174
2137
  );
2175
2138
  this.options.observability?.tracer?.event(
2176
2139
  "persistentContentCache.hit",
@@ -2178,16 +2141,32 @@ var Pipeline = class {
2178
2141
  );
2179
2142
  this.options.onPersistentContentCacheResult?.("hit");
2180
2143
  return {
2181
- frontmatter: cachedEntry.value.frontmatter,
2182
- html: cachedEntry.value.html
2144
+ frontmatter: lookup.entry.value.frontmatter,
2145
+ html: lookup.entry.value.html
2183
2146
  };
2184
2147
  }
2148
+ this.options.observability?.tracer?.event(
2149
+ "persistentContentCache.miss",
2150
+ {
2151
+ key: cacheKey,
2152
+ slug: sourceSlug,
2153
+ reason: "dependency-changed",
2154
+ dependencyKind: invalidDependency.kind,
2155
+ dependencyId: invalidDependency.id
2156
+ }
2157
+ );
2158
+ this.options.onPersistentContentCacheResult?.("miss");
2159
+ } else {
2160
+ this.options.observability?.tracer?.event(
2161
+ "persistentContentCache.miss",
2162
+ {
2163
+ key: cacheKey,
2164
+ slug: sourceSlug,
2165
+ reason: lookup.reason
2166
+ }
2167
+ );
2168
+ this.options.onPersistentContentCacheResult?.("miss");
2185
2169
  }
2186
- this.options.observability?.tracer?.event(
2187
- "persistentContentCache.miss",
2188
- { key: cacheKey, slug: sourceSlug }
2189
- );
2190
- this.options.onPersistentContentCacheResult?.("miss");
2191
2170
  } else {
2192
2171
  this.options.observability?.tracer?.event(
2193
2172
  "persistentContentCache.bypass",
@@ -2299,6 +2278,30 @@ var Pipeline = class {
2299
2278
  reportContentDependencies(slug, dependencies) {
2300
2279
  this.options.onContentDependencies?.(slug, dependencies);
2301
2280
  }
2281
+ async findInvalidDependency(dependencies) {
2282
+ const contentSource = this.options.contentSource;
2283
+ if (!contentSource) return null;
2284
+ const validationTracker = createContentDependencyTracker(contentSource);
2285
+ for (const dep of dependencies) {
2286
+ if (dep.kind === "content") {
2287
+ const content = await validationTracker.readContent(
2288
+ dep.id,
2289
+ async () => await this.getMarkdownBySlug?.(dep.id) ?? ""
2290
+ );
2291
+ if (fingerprintContent(content) !== dep.fingerprint) return dep;
2292
+ } else if (dep.kind === "file") {
2293
+ const fileContent = await validationTracker.contentSource.read({
2294
+ path: dep.id
2295
+ });
2296
+ if (fingerprintContent(fileContent) !== dep.fingerprint) return dep;
2297
+ } else if (dep.kind === "link") {
2298
+ if (fingerprintContent(this.resolveLinkDependency(dep.id)) !== dep.fingerprint) {
2299
+ return dep;
2300
+ }
2301
+ }
2302
+ }
2303
+ return null;
2304
+ }
2302
2305
  getPermalink(slug) {
2303
2306
  const permalink = this.permalinks.get(slug);
2304
2307
  if (!permalink) {
@@ -3680,9 +3683,14 @@ var ContentEntryReader = class {
3680
3683
  if (cached) return cached;
3681
3684
  const content = this.source.read(entry).then(
3682
3685
  (value) => typeof value === "string" ? value : new TextDecoder().decode(value)
3683
- ).finally(() => this.texts.delete(entry.path));
3684
- this.texts.set(entry.path, content);
3685
- return content;
3686
+ );
3687
+ const retain = entry.path.endsWith(".md");
3688
+ const tracked = retain ? content : content.finally(() => this.texts.delete(entry.path));
3689
+ this.texts.set(entry.path, tracked);
3690
+ return tracked;
3691
+ }
3692
+ clearReadCache() {
3693
+ this.texts.clear();
3686
3694
  }
3687
3695
  };
3688
3696
 
@@ -3826,10 +3834,12 @@ var FileSystemContentSource = class {
3826
3834
  async scanWithExclusions() {
3827
3835
  const exclusions = [];
3828
3836
  try {
3837
+ const templaterTemplateFolder = await this.resolveTemplaterTemplateFolder();
3829
3838
  const entries = await this.scanDirectory(
3830
3839
  this.contentDirectory,
3831
3840
  "",
3832
- exclusions
3841
+ exclusions,
3842
+ templaterTemplateFolder
3833
3843
  );
3834
3844
  return { entries, exclusions };
3835
3845
  } catch (error) {
@@ -3847,7 +3857,7 @@ var FileSystemContentSource = class {
3847
3857
  }
3848
3858
  return await fs4.readFile(filePath);
3849
3859
  }
3850
- async scanDirectory(directory, parentPath2, exclusions) {
3860
+ async scanDirectory(directory, parentPath2, exclusions, templaterTemplateFolder) {
3851
3861
  const directoryEntries = await fs4.readdir(directory, {
3852
3862
  withFileTypes: true
3853
3863
  });
@@ -3864,9 +3874,21 @@ var FileSystemContentSource = class {
3864
3874
  exclusions.push({ path: logicalPath, pattern });
3865
3875
  continue;
3866
3876
  }
3877
+ if (templaterTemplateFolder && isTemplaterTemplatePath(logicalPath, templaterTemplateFolder)) {
3878
+ exclusions.push({
3879
+ path: logicalPath,
3880
+ pattern: `Obsidian Templater templates_folder: ${templaterTemplateFolder}`
3881
+ });
3882
+ continue;
3883
+ }
3867
3884
  if (directoryEntry.isDirectory()) {
3868
3885
  entries.push(
3869
- ...await this.scanDirectory(filePath, logicalPath, exclusions)
3886
+ ...await this.scanDirectory(
3887
+ filePath,
3888
+ logicalPath,
3889
+ exclusions,
3890
+ templaterTemplateFolder
3891
+ )
3870
3892
  );
3871
3893
  continue;
3872
3894
  }
@@ -3891,6 +3913,28 @@ var FileSystemContentSource = class {
3891
3913
  }
3892
3914
  return filePath;
3893
3915
  }
3916
+ async resolveTemplaterTemplateFolder() {
3917
+ const settingsPath = path4.join(
3918
+ this.contentDirectory,
3919
+ ".obsidian",
3920
+ "plugins",
3921
+ "templater-obsidian",
3922
+ "data.json"
3923
+ );
3924
+ let raw;
3925
+ try {
3926
+ raw = await fs4.readFile(settingsPath, "utf8");
3927
+ } catch (error) {
3928
+ if (isFileNotFoundError(error)) return void 0;
3929
+ throw error;
3930
+ }
3931
+ try {
3932
+ const settings = JSON.parse(raw);
3933
+ return normalizeTemplaterTemplateFolder(settings.templates_folder);
3934
+ } catch {
3935
+ return void 0;
3936
+ }
3937
+ }
3894
3938
  };
3895
3939
  function isIgnoredContentPath(logicalPath, exclude = []) {
3896
3940
  if (isExcluded(INTERNAL_CONTENT_IGNORE_PATTERNS, logicalPath)) return true;
@@ -3903,6 +3947,17 @@ function matchContentExcludePattern(logicalPath, exclude = []) {
3903
3947
  const withoutExtension = logicalPath.slice(0, -3);
3904
3948
  return exclude.find((pattern) => isExcluded([pattern], withoutExtension));
3905
3949
  }
3950
+ function isTemplaterTemplatePath(logicalPath, templateFolder) {
3951
+ return logicalPath === templateFolder || logicalPath.startsWith(`${templateFolder}/`);
3952
+ }
3953
+ function normalizeTemplaterTemplateFolder(value) {
3954
+ if (typeof value !== "string" || value.trim().length === 0) return void 0;
3955
+ const normalized = normalizeContentPath(value.trim()).replace(/\/+$/, "");
3956
+ if (normalized.length === 0 || normalized.startsWith("/") || path4.isAbsolute(normalized) || path4.win32.isAbsolute(normalized) || normalized.split("/").some((segment) => segment === "..")) {
3957
+ return void 0;
3958
+ }
3959
+ return normalized;
3960
+ }
3906
3961
  function normalizeLogicalPath(logicalPath) {
3907
3962
  const normalizedPath = normalizeContentPath(logicalPath);
3908
3963
  if (normalizedPath.length === 0 || normalizedPath.startsWith("/") || path4.isAbsolute(normalizedPath) || path4.win32.isAbsolute(normalizedPath) || normalizedPath.split("/").some((segment) => segment === "..")) {
@@ -3913,6 +3968,9 @@ function normalizeLogicalPath(logicalPath) {
3913
3968
  function isMissingDirectoryError(error, directory) {
3914
3969
  return error instanceof Error && "code" in error && error.code === "ENOENT" && "path" in error && error.path === directory;
3915
3970
  }
3971
+ function isFileNotFoundError(error) {
3972
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
3973
+ }
3916
3974
  function missingContentDirectoryError(directory) {
3917
3975
  const relative = path4.relative(process.cwd(), directory);
3918
3976
  const display = relative && !relative.startsWith("..") ? relative : directory;
@@ -4348,6 +4406,7 @@ var ContentManager = class {
4348
4406
  routableSlugs = null;
4349
4407
  routableSlugsPromise = null;
4350
4408
  processedContentCount = 0;
4409
+ reusedContentCount = 0;
4351
4410
  outputChangeSet = null;
4352
4411
  trackedContentDependencies = /* @__PURE__ */ new Map();
4353
4412
  async getAllPosts() {
@@ -4575,6 +4634,12 @@ var ContentManager = class {
4575
4634
  async build(options = {}) {
4576
4635
  return await this.getManifest(options);
4577
4636
  }
4637
+ getProcessedContentCount() {
4638
+ return this.processedContentCount;
4639
+ }
4640
+ getReusedContentCount() {
4641
+ return this.reusedContentCount;
4642
+ }
4578
4643
  async getOutputChangeSet(options = {}) {
4579
4644
  await this.getManifest(options);
4580
4645
  if (!this.outputChangeSet) {
@@ -4650,6 +4715,7 @@ var ContentManager = class {
4650
4715
  return { entries, contentIndex, diagnostics };
4651
4716
  }
4652
4717
  async dispose() {
4718
+ this.entryReader.clearReadCache();
4653
4719
  if (!this.contentIndex) return;
4654
4720
  await this.pluginRuntime.dispose(this.contentIndex);
4655
4721
  }
@@ -4712,6 +4778,7 @@ var ContentManager = class {
4712
4778
  if (preparation.affectedContent.dependent.has(slug)) return null;
4713
4779
  const entry = preparation.previousManifestEntriesBySlug.get(slug);
4714
4780
  if (!entry) return null;
4781
+ this.reusedContentCount += 1;
4715
4782
  this.observability().tracer.event("content.reuse", { slug });
4716
4783
  return cloneManifestEntry(entry);
4717
4784
  }
@@ -5148,6 +5215,14 @@ function resolveBuildOutputDirectory(config) {
5148
5215
  }
5149
5216
 
5150
5217
  // src/types/content_manifest.ts
5218
+ var ARTICLE_SLOT = {
5219
+ header: "article.header",
5220
+ metadata: "article.metadata",
5221
+ aside: "article.aside",
5222
+ beforeContent: "article.before-content",
5223
+ afterContent: "article.after-content",
5224
+ footer: "article.footer"
5225
+ };
5151
5226
  function appendContentBodySlot(entry, slot, html) {
5152
5227
  if (!html.trim()) return;
5153
5228
  const previous = entry.bodySlots?.[slot];
@@ -5187,6 +5262,32 @@ function escapeScriptJson(value) {
5187
5262
  return value.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
5188
5263
  }
5189
5264
 
5265
+ // src/utils/markdown.ts
5266
+ var FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})(.*)$/;
5267
+ var FENCE_CLOSE = /^ {0,3}(`{3,}|~{3,})\s*$/;
5268
+ function scanMarkdownFenceLines(source) {
5269
+ const lines = [];
5270
+ let fence = null;
5271
+ for (const value of source.split("\n")) {
5272
+ if (fence === null) {
5273
+ const open = FENCE_OPEN.exec(value);
5274
+ if (open && !(open[1][0] === "`" && open[2].includes("`"))) {
5275
+ fence = { marker: open[1][0], length: open[1].length };
5276
+ }
5277
+ lines.push({ value, inFence: false });
5278
+ continue;
5279
+ }
5280
+ const close = FENCE_CLOSE.exec(value);
5281
+ if (close && close[1][0] === fence.marker && close[1].length >= fence.length) {
5282
+ fence = null;
5283
+ lines.push({ value, inFence: false });
5284
+ continue;
5285
+ }
5286
+ lines.push({ value, inFence: true });
5287
+ }
5288
+ return lines;
5289
+ }
5290
+
5190
5291
  // src/utils/text.ts
5191
5292
  function stripHtml(html) {
5192
5293
  return html.replace(/<script\b[\s\S]*?<\/script>/gi, " ").replace(/<style\b[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ");
@@ -5199,6 +5300,7 @@ function calculateReadingTime(html) {
5199
5300
  return Math.max(1, Math.ceil(cjkCharacters / 500 + latinWords / 220));
5200
5301
  }
5201
5302
  export {
5303
+ ARTICLE_SLOT,
5202
5304
  ATTACHMENTS_BASE_PATH,
5203
5305
  CompositeTraceSink,
5204
5306
  ConfigValidationError,
@@ -5211,6 +5313,7 @@ export {
5211
5313
  NoopTracer,
5212
5314
  Pipeline,
5213
5315
  PluginDependencyError,
5316
+ PluginHookError,
5214
5317
  SinkTracer,
5215
5318
  appendContentBodySlot,
5216
5319
  attachErrorPath,
@@ -5219,11 +5322,9 @@ export {
5219
5322
  buildGraphEdges,
5220
5323
  buildOutputInventory,
5221
5324
  calculateReadingTime,
5222
- collectConfigDeprecationDiagnostics,
5223
5325
  computePipelineFingerprint,
5224
5326
  createClientEntry,
5225
5327
  createContentGraph,
5226
- createDeprecationDiagnostic,
5227
5328
  createPluginMemo,
5228
5329
  createStyleAsset,
5229
5330
  createUnavailableGeneratedOutputSink,
@@ -5237,7 +5338,6 @@ export {
5237
5338
  escapeScriptJson,
5238
5339
  extractFrontmatterAliases,
5239
5340
  fingerprintContentEntries,
5240
- formatDeprecationMessage,
5241
5341
  getContentSourceEntry,
5242
5342
  getEntryLanguage,
5243
5343
  getExtension,
@@ -5280,6 +5380,7 @@ export {
5280
5380
  resolvePublicFolderLocation,
5281
5381
  resolvePublishingBuildTime,
5282
5382
  resolvePublishingState,
5383
+ scanMarkdownFenceLines,
5283
5384
  selectEntriesByLanguage,
5284
5385
  serializePublicClientConfig,
5285
5386
  stableStringify,
@@ -11,4 +11,5 @@ export declare class ContentEntryReader {
11
11
  getEntries(): Promise<readonly ContentSourceEntry[]>;
12
12
  readText(logicalPath: string): Promise<string>;
13
13
  read(entry: ContentSourceEntry): Promise<string>;
14
+ clearReadCache(): void;
14
15
  }
@@ -44,6 +44,7 @@ export declare class ContentManager {
44
44
  private routableSlugs;
45
45
  private routableSlugsPromise;
46
46
  private processedContentCount;
47
+ private reusedContentCount;
47
48
  private outputChangeSet;
48
49
  private trackedContentDependencies;
49
50
  constructor(content: string | ContentSource, exclude?: string[], pipelineOptions?: PipelineOptions);
@@ -56,6 +57,8 @@ export declare class ContentManager {
56
57
  getManifest(options?: ContentBuildOptions): Promise<ContentManifest>;
57
58
  private buildManifest;
58
59
  build(options?: ContentBuildOptions): Promise<ContentManifest>;
60
+ getProcessedContentCount(): number;
61
+ getReusedContentCount(): number;
59
62
  getOutputChangeSet(options?: ContentBuildOptions): Promise<OutputChangeSet>;
60
63
  getBacklinks(targetSlug: string): Promise<Backlink[]>;
61
64
  getContentGraph(): Promise<ContentGraph>;
@@ -18,16 +18,21 @@ export type PersistentContentCacheOptions = {
18
18
  };
19
19
  export declare const CONTENT_CACHE_SCHEMA_VERSION = 3;
20
20
  export declare const CONTENT_CACHE_COMPATIBILITY_VERSION = 1;
21
- export type PersistentContentCache = {
22
- get(key: string): Promise<PersistentContentCacheEntry | undefined>;
23
- set(key: string, value: PersistentContentCacheEntry): Promise<void>;
24
- clear(): Promise<void>;
21
+ export type PersistentContentCacheMissReason = "disabled" | "no-entry" | "corrupted" | "unreadable" | "schema-mismatch" | "key-mismatch" | "invalid-entry";
22
+ export type PersistentContentCacheLookup = {
23
+ status: "hit";
24
+ entry: PersistentContentCacheEntry;
25
+ } | {
26
+ status: "miss";
27
+ reason: PersistentContentCacheMissReason;
25
28
  };
26
- export declare function createPersistentContentCache(options: PersistentContentCacheOptions): {
29
+ export type PersistentContentCache = {
30
+ lookup(key: string): Promise<PersistentContentCacheLookup>;
27
31
  get(key: string): Promise<PersistentContentCacheEntry | undefined>;
28
32
  set(key: string, value: PersistentContentCacheEntry): Promise<void>;
29
33
  clear(): Promise<void>;
30
34
  };
35
+ export declare function createPersistentContentCache(options: PersistentContentCacheOptions): PersistentContentCache;
31
36
  export type CacheKeyInputs = {
32
37
  slug: string;
33
38
  source: string;
@@ -17,6 +17,7 @@ export declare class FileSystemContentSource implements ContentSource {
17
17
  read(entry: ContentSourceEntry): Promise<ContentSourceContent>;
18
18
  private scanDirectory;
19
19
  private resolveFilePath;
20
+ private resolveTemplaterTemplateFolder;
20
21
  }
21
22
  export declare function isIgnoredContentPath(logicalPath: string, exclude?: readonly string[]): boolean;
22
23
  export declare function matchContentExcludePattern(logicalPath: string, exclude?: readonly string[]): string | undefined;
@@ -27,6 +27,7 @@ export declare class Pipeline {
27
27
  execute(markDownContent: string, context?: MarkdownExecutionContext): Promise<PostContent>;
28
28
  private executeWithEmbedState;
29
29
  private reportContentDependencies;
30
+ private findInvalidDependency;
30
31
  private getPermalink;
31
32
  private resolveLinkDependency;
32
33
  private recordNoteLinkDependencies;
@@ -18,13 +18,36 @@ export type ContentAsset = {
18
18
  export type ContentManifestPluginAsset = PluginAsset & {
19
19
  path: string;
20
20
  };
21
+ /**
22
+ * The standard `article.*` positions the article consumer recognizes.
23
+ *
24
+ * These constants are a convenience for Sites that prefer named references;
25
+ * writing the slot string inline (for example `name="article.metadata"`) stays
26
+ * the recommended, most readable form in TSX layouts.
27
+ */
28
+ export declare const ARTICLE_SLOT: {
29
+ readonly header: "article.header";
30
+ readonly metadata: "article.metadata";
31
+ readonly aside: "article.aside";
32
+ readonly beforeContent: "article.before-content";
33
+ readonly afterContent: "article.after-content";
34
+ readonly footer: "article.footer";
35
+ };
21
36
  /**
22
37
  * A semantic position in a Site-owned article layout.
23
38
  *
24
39
  * The standard article consumer recognizes the `article.*` positions below.
25
40
  * Other names remain valid so a custom Site can define its own layout slots.
26
41
  */
27
- export type ContentBodySlot = "article.header" | "article.metadata" | "article.aside" | "article.before-content" | "article.after-content" | "article.footer" | (string & {});
42
+ export type ContentBodySlot = (typeof ARTICLE_SLOT)[keyof typeof ARTICLE_SLOT] | (string & {});
43
+ /**
44
+ * A map of Plugin-provided HTML fragments keyed by slot name.
45
+ *
46
+ * The standard `article.*` names autocomplete while any custom slot name
47
+ * remains valid, so custom Plugins keep working. A slot is considered empty
48
+ * when its value is missing or whitespace-only.
49
+ */
50
+ export type ContentBodySlots = Partial<Record<(typeof ARTICLE_SLOT)[keyof typeof ARTICLE_SLOT], string>> & Readonly<Record<string, string | undefined>>;
28
51
  /** Client entries and their explicitly public configuration. */
29
52
  export type ContentManifestPluginClientEntry = PluginClientEntry;
30
53
  export type ContentManifestEntry = {
@@ -91,15 +114,17 @@ export type ContentLocationInput = {
91
114
  markdown: string;
92
115
  };
93
116
  export type ContentManifest = {
117
+ /**
118
+ * Raw entries for every scanned content source, including drafts, scheduled,
119
+ * private, and otherwise nonpublic entries. This is not a publication view.
120
+ */
94
121
  entries: ContentManifestEntry[];
95
122
  /**
96
- * Entries that are routable and therefore get a generated page: `public`
97
- * and `unlisted` notes. `entries` keeps every scanned note for backward
98
- * compatibility; plugins that emit generated output should read this view
99
- * so drafts and scheduled notes never reach generated output.
123
+ * Routable entries for generated pages: `public` and `unlisted` notes.
124
+ * Plugins that emit public output must use this view rather than `entries`.
100
125
  */
101
126
  publicEntries: ContentManifestEntry[];
102
- /** Entries that may appear on discovery surfaces such as navigation, search, feeds, taxonomy, and public graphs. */
127
+ /** Public entries allowed on discovery surfaces such as navigation, search, feeds, taxonomy, and public graphs. */
103
128
  discoverableEntries: ContentManifestEntry[];
104
129
  bySlug: Map<string, ContentManifestEntry>;
105
130
  /** Contains only entries with an explicit source-authored content ID. */
@@ -18,7 +18,11 @@ export type PluginPage = {
18
18
  language?: string;
19
19
  };
20
20
  export type PluginPageContext = PluginContext & {
21
- /** The public manifest. Unpublished content is not exposed through pages. */
21
+ /**
22
+ * The resolved manifest. `manifest.entries` still contains `draft` and
23
+ * `scheduled` entries, so page output must read `publicEntries` (routable) or
24
+ * `discoverableEntries` (public only) instead of `entries`.
25
+ */
22
26
  manifest: ContentManifest;
23
27
  /** Request pathname, normalized to an absolute path. */
24
28
  pathname: string;
@@ -26,7 +26,7 @@ export type RenderableFeedEntry = ContentManifestEntry & {
26
26
  };
27
27
  export type PluginSeoExtension = {
28
28
  buildArticleSeo(config: ResolvedRiebeckiteConfig, permalink: string, post: PostContent, headTags?: readonly PluginHeadTag[], language?: string): SeoMetadata;
29
- buildWebsiteSeo(config: ResolvedRiebeckiteConfig, input: WebsiteSeoInput, headTags?: readonly PluginHeadTag[]): SeoMetadata;
29
+ buildWebsiteSeo(config: ResolvedRiebeckiteConfig, input: WebsiteSeoInput, headTags?: readonly PluginHeadTag[], language?: string): SeoMetadata;
30
30
  buildAbsoluteUrl(config: ResolvedRiebeckiteConfig, pathOrUrl: string): string;
31
31
  buildPostUrl(config: ResolvedRiebeckiteConfig, permalink: string): string;
32
32
  getDescription(post: Pick<PostContent, "frontmatter" | "html">): string;
@@ -0,0 +1,5 @@
1
+ export type MarkdownFenceLine = {
2
+ readonly value: string;
3
+ readonly inFence: boolean;
4
+ };
5
+ export declare function scanMarkdownFenceLines(source: string): MarkdownFenceLine[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riebeckite/core",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -49,7 +49,7 @@
49
49
  },
50
50
  "devDependencies": {
51
51
  "tsx": "^4.22.4",
52
- "@riebeckite/test": "0.0.18"
52
+ "@riebeckite/test": "0.0.20"
53
53
  },
54
54
  "repository": {
55
55
  "type": "git",
@@ -1,8 +0,0 @@
1
- import { type DeprecationNotice } from "./deprecation.js";
2
- import type { Diagnostic } from "./types/diagnostic.js";
3
- import type { RiebeckiteConfig } from "./types/riebeckite_config.js";
4
- export type ConfigDeprecationNotice = DeprecationNotice & {
5
- readonly kind: "config";
6
- readonly path: string;
7
- };
8
- export declare function collectConfigDeprecationDiagnostics(config: RiebeckiteConfig, notices?: readonly ConfigDeprecationNotice[]): Diagnostic[];
@@ -1,14 +0,0 @@
1
- import type { Diagnostic } from "./types/diagnostic.js";
2
- export type DeprecationKind = "config" | "plugin-api" | "theme-api" | "cli" | "scaffold" | "package";
3
- export type DeprecationNotice = {
4
- readonly kind: DeprecationKind;
5
- readonly id: string;
6
- readonly target: string;
7
- readonly deprecatedSince: string;
8
- readonly action: string;
9
- readonly replacement?: string;
10
- readonly documentationUrl?: string;
11
- readonly removedIn?: string;
12
- };
13
- export declare function createDeprecationDiagnostic(notice: DeprecationNotice): Diagnostic;
14
- export declare function formatDeprecationMessage(notice: DeprecationNotice): string;