@sorane/core 0.2.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/package.json +42 -0
- package/src/ai-disclosure.ts +181 -0
- package/src/asset-provenance.ts +141 -0
- package/src/associated-media.ts +93 -0
- package/src/blog-pages.ts +175 -0
- package/src/build.ts +1109 -0
- package/src/c2pa-pass.ts +116 -0
- package/src/catalog.ts +61 -0
- package/src/config.ts +255 -0
- package/src/diagrams/compile-d2.ts +70 -0
- package/src/diagrams/compile-graphviz.ts +71 -0
- package/src/diagrams/compile-mermaid.ts +102 -0
- package/src/diagrams/diagram-hash.ts +5 -0
- package/src/diagrams/diagram-meta.ts +74 -0
- package/src/diagrams/emit-diagram-assets.ts +135 -0
- package/src/diagrams/mermaid-head.ts +6 -0
- package/src/diagrams/needs-async-compile.ts +12 -0
- package/src/diagrams/parse-diagram-fence.ts +109 -0
- package/src/diagrams/rehype-diagram-pre.ts +39 -0
- package/src/diagrams/remark-inject-built-figures.ts +32 -0
- package/src/diagrams/render-async.ts +241 -0
- package/src/diagrams/render-body-section.ts +52 -0
- package/src/diagrams/validate-diagram-alt.ts +56 -0
- package/src/docs.ts +257 -0
- package/src/emit-page.ts +87 -0
- package/src/index.ts +49 -0
- package/src/iptc-xmp-pass.ts +94 -0
- package/src/markdown-image-refs.ts +135 -0
- package/src/migrate.ts +60 -0
- package/src/not-found.ts +64 -0
- package/src/og-meta.ts +18 -0
- package/src/render.ts +233 -0
- package/src/site-labels.ts +97 -0
- package/src/site-meta.ts +138 -0
- package/src/ssg.ts +676 -0
- package/src/static-assets.ts +198 -0
- package/src/theme-assets.ts +16 -0
- package/src/validate-heading-structure.ts +51 -0
package/src/c2pa-pass.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import type { C2paConfig } from "./config.ts";
|
|
4
|
+
|
|
5
|
+
export interface C2paCredentials {
|
|
6
|
+
readonly signCert: string;
|
|
7
|
+
readonly privateKey: string;
|
|
8
|
+
readonly settingsPath?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SignRasterOptions {
|
|
12
|
+
readonly binary?: string;
|
|
13
|
+
readonly embed?: boolean;
|
|
14
|
+
readonly createIntent: string;
|
|
15
|
+
readonly credentials: C2paCredentials;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const RASTER_RE = /\.(jpe?g|png)$/i;
|
|
19
|
+
|
|
20
|
+
export function isC2paRasterPath(filePath: string): boolean {
|
|
21
|
+
return RASTER_RE.test(filePath);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveC2paCredentials(
|
|
25
|
+
config: C2paConfig | undefined,
|
|
26
|
+
): C2paCredentials | null {
|
|
27
|
+
if (config?.enabled !== true) return null;
|
|
28
|
+
|
|
29
|
+
const signCert =
|
|
30
|
+
config.certificate_path ??
|
|
31
|
+
process.env.SORANE_C2PA_CERT ??
|
|
32
|
+
process.env.C2PA_SIGN_CERT;
|
|
33
|
+
const privateKey =
|
|
34
|
+
config.private_key_path ??
|
|
35
|
+
process.env.SORANE_C2PA_KEY ??
|
|
36
|
+
process.env.C2PA_PRIVATE_KEY;
|
|
37
|
+
|
|
38
|
+
if (!signCert || !privateKey) return null;
|
|
39
|
+
if (!existsSync(signCert) || !existsSync(privateKey)) return null;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
signCert,
|
|
43
|
+
privateKey,
|
|
44
|
+
settingsPath: config.settings_path,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** c2patool 0.26+ 向けの署名環境(settings ファイルまたは PEM 環境変数)。 */
|
|
49
|
+
function buildSigningEnv(creds: C2paCredentials): NodeJS.ProcessEnv {
|
|
50
|
+
const env = { ...process.env };
|
|
51
|
+
if (creds.settingsPath && existsSync(creds.settingsPath)) {
|
|
52
|
+
env.C2PATOOL_SETTINGS = creds.settingsPath;
|
|
53
|
+
return env;
|
|
54
|
+
}
|
|
55
|
+
env.C2PA_PRIVATE_KEY = readFileSync(creds.privateKey, "utf8");
|
|
56
|
+
env.C2PA_SIGN_CERT = readFileSync(creds.signCert, "utf8");
|
|
57
|
+
delete env.C2PATOOL_SETTINGS;
|
|
58
|
+
return env;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** c2patool でラスタ画像に C2PA manifest を埋め込む。失敗時は false。 */
|
|
62
|
+
export function signRasterWithC2pa(
|
|
63
|
+
inputPath: string,
|
|
64
|
+
outputPath: string,
|
|
65
|
+
opts: SignRasterOptions,
|
|
66
|
+
): { readonly ok: boolean; readonly message?: string } {
|
|
67
|
+
const binary = opts.binary ?? "c2patool";
|
|
68
|
+
const env = buildSigningEnv(opts.credentials);
|
|
69
|
+
|
|
70
|
+
const args = [
|
|
71
|
+
inputPath,
|
|
72
|
+
"-c",
|
|
73
|
+
'{"assertions":[]}',
|
|
74
|
+
"--create",
|
|
75
|
+
opts.createIntent,
|
|
76
|
+
"-o",
|
|
77
|
+
outputPath,
|
|
78
|
+
"-f",
|
|
79
|
+
"--no_signing_verify",
|
|
80
|
+
];
|
|
81
|
+
if (opts.embed === false) args.push("-s");
|
|
82
|
+
|
|
83
|
+
const result = spawnSync(binary, args, {
|
|
84
|
+
env,
|
|
85
|
+
encoding: "utf8",
|
|
86
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (result.status !== 0) {
|
|
90
|
+
const msg = (result.stderr || result.stdout || "").trim();
|
|
91
|
+
return { ok: false, message: msg.length > 0 ? msg : `c2patool exited ${result.status}` };
|
|
92
|
+
}
|
|
93
|
+
return { ok: true };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 出力画像に C2PA manifest があるか簡易確認(c2patool --info)。 */
|
|
97
|
+
export function probeC2paManifest(
|
|
98
|
+
filePath: string,
|
|
99
|
+
binary = "c2patool",
|
|
100
|
+
): boolean {
|
|
101
|
+
const result = spawnSync(binary, [filePath, "--info"], {
|
|
102
|
+
encoding: "utf8",
|
|
103
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
104
|
+
});
|
|
105
|
+
if (result.status !== 0) return false;
|
|
106
|
+
const out = `${result.stdout}\n${result.stderr}`;
|
|
107
|
+
return /manifest|validated|active manifest/i.test(out);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function c2patoolAvailable(binary = "c2patool"): boolean {
|
|
111
|
+
const result = spawnSync(binary, ["--version"], {
|
|
112
|
+
encoding: "utf8",
|
|
113
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
114
|
+
});
|
|
115
|
+
return result.status === 0;
|
|
116
|
+
}
|
package/src/catalog.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { OkfConcept } from "@sorane/okf";
|
|
2
|
+
import { parseAiDisclosure } from "./ai-disclosure.ts";
|
|
3
|
+
|
|
4
|
+
export interface CatalogEntry {
|
|
5
|
+
readonly slug: string;
|
|
6
|
+
readonly url: string;
|
|
7
|
+
readonly concept: OkfConcept;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function buildCatalogJsonLd(
|
|
11
|
+
entries: readonly CatalogEntry[],
|
|
12
|
+
siteTitle: string,
|
|
13
|
+
baseUrl: string,
|
|
14
|
+
opts?: { readonly machineReadable?: boolean },
|
|
15
|
+
): string {
|
|
16
|
+
const machineReadable = opts?.machineReadable !== false;
|
|
17
|
+
const graph = entries.map((e) => {
|
|
18
|
+
const dataset: Record<string, unknown> = {
|
|
19
|
+
"@type": "Dataset",
|
|
20
|
+
"@id": e.url,
|
|
21
|
+
name: e.concept.title,
|
|
22
|
+
keywords: [e.concept.type, ...(e.concept.tags ?? [])],
|
|
23
|
+
distribution: [
|
|
24
|
+
{
|
|
25
|
+
"@type": "DataDownload",
|
|
26
|
+
encodingFormat: "text/markdown",
|
|
27
|
+
contentUrl: `${e.url.replace(/\.html$/, ".md")}`,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
if (e.concept.description) dataset.description = e.concept.description;
|
|
32
|
+
if (e.concept.timestamp) dataset.dateModified = e.concept.timestamp;
|
|
33
|
+
const disclosure = machineReadable
|
|
34
|
+
? parseAiDisclosure(e.concept.frontmatter)
|
|
35
|
+
: null;
|
|
36
|
+
if (disclosure) {
|
|
37
|
+
dataset.digitalSourceType = disclosure.digitalSourceType;
|
|
38
|
+
if (disclosure.systems?.length) {
|
|
39
|
+
const kw = dataset.keywords as string[];
|
|
40
|
+
dataset.keywords = [
|
|
41
|
+
...kw,
|
|
42
|
+
...disclosure.systems.map((s) => `ai-system:${s.name}`),
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return dataset;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const catalog = {
|
|
50
|
+
"@context": {
|
|
51
|
+
"@vocab": "https://schema.org/",
|
|
52
|
+
dcat: "http://www.w3.org/ns/dcat#",
|
|
53
|
+
},
|
|
54
|
+
"@type": "DataCatalog",
|
|
55
|
+
name: siteTitle,
|
|
56
|
+
url: baseUrl.length > 0 ? baseUrl : undefined,
|
|
57
|
+
dataset: graph,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return JSON.stringify(catalog, null, 2) + "\n";
|
|
61
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
export interface FontSourceSpec {
|
|
2
|
+
readonly source: string;
|
|
3
|
+
readonly weight?: string;
|
|
4
|
+
readonly embed?: "subset" | "static";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface FontRoles {
|
|
8
|
+
readonly body: readonly string[];
|
|
9
|
+
readonly heading?: readonly string[];
|
|
10
|
+
readonly code?: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type FeaturedMode = "excerpt" | "full" | "off";
|
|
14
|
+
|
|
15
|
+
export interface BlogBuildConfig {
|
|
16
|
+
/** 1ページあたりの記事数(page/N.html) */
|
|
17
|
+
readonly page_size?: number;
|
|
18
|
+
/** トップ index のアーカイブ欄に載せる件数(既定 15) */
|
|
19
|
+
readonly index_archive_limit?: number;
|
|
20
|
+
/** トップの最新記事表示: excerpt | full | off */
|
|
21
|
+
readonly featured_mode?: FeaturedMode;
|
|
22
|
+
/** featured_mode: excerpt の最大文字数 */
|
|
23
|
+
readonly excerpt_length?: number;
|
|
24
|
+
/** アーカイブリストに description を出す */
|
|
25
|
+
readonly show_list_descriptions?: boolean;
|
|
26
|
+
/** archive/index.html, archive/YYYY.html 等を生成 */
|
|
27
|
+
readonly archives?: boolean;
|
|
28
|
+
/** tag/slug.html を生成 */
|
|
29
|
+
readonly tags?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type SearchMode = "fts" | "hybrid";
|
|
33
|
+
|
|
34
|
+
export interface SearchConfig {
|
|
35
|
+
/** fts(標準)| hybrid(experimental・要埋め込みモデル) */
|
|
36
|
+
readonly mode?: SearchMode;
|
|
37
|
+
/** FTS インデックスの出力先(既定: .sorane/index.db) */
|
|
38
|
+
readonly index?: string;
|
|
39
|
+
/** 埋め込みモデル root(既定: vendor/models) */
|
|
40
|
+
readonly model?: string;
|
|
41
|
+
/** モデル ID(既定: ruri-v3-30m) */
|
|
42
|
+
readonly model_id?: string;
|
|
43
|
+
/** 大容量検索資産の配信元(R2 等)。末尾 "/" 推奨。空なら同一オリジン。 */
|
|
44
|
+
readonly asset_base_url?: string;
|
|
45
|
+
/** dist に ONNX モデルを同梱する(Pages 25MiB 制限のため本番では false 推奨) */
|
|
46
|
+
readonly bundle_model?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type DocsNavSpec = string | { readonly href: string; readonly title?: string };
|
|
50
|
+
|
|
51
|
+
export interface DocsConfig {
|
|
52
|
+
/** ドキュメントサイトのサイドバー順(href は dist 基準、例: getting-started.html) */
|
|
53
|
+
readonly nav?: readonly DocsNavSpec[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type MermaidMode = "client" | "build" | "off";
|
|
57
|
+
|
|
58
|
+
export interface DiagramsConfig {
|
|
59
|
+
readonly enabled?: boolean;
|
|
60
|
+
readonly mermaid?: {
|
|
61
|
+
readonly mode?: MermaidMode;
|
|
62
|
+
readonly version?: string;
|
|
63
|
+
readonly mmdc?: string;
|
|
64
|
+
};
|
|
65
|
+
readonly d2?: {
|
|
66
|
+
readonly enabled?: boolean;
|
|
67
|
+
readonly binary?: string;
|
|
68
|
+
};
|
|
69
|
+
readonly graphviz?: {
|
|
70
|
+
readonly enabled?: boolean;
|
|
71
|
+
readonly binary?: string;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const DEFAULT_DIAGRAMS_CONFIG: Required<DiagramsConfig> = {
|
|
76
|
+
enabled: true,
|
|
77
|
+
mermaid: { mode: "client", version: "~11.15.0", mmdc: "mmdc" },
|
|
78
|
+
d2: { enabled: false, binary: "d2" },
|
|
79
|
+
graphviz: { enabled: false, binary: "dot" },
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export interface ImageMetadataConfig {
|
|
83
|
+
readonly enabled?: boolean;
|
|
84
|
+
readonly exiftool?: string;
|
|
85
|
+
/** 既定: content/asset-provenance.yaml */
|
|
86
|
+
readonly manifest?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface C2paConfig {
|
|
90
|
+
readonly enabled?: boolean;
|
|
91
|
+
/** 既定 true(埋め込み)。false は sidecar .c2pa(開発用) */
|
|
92
|
+
readonly embed?: boolean;
|
|
93
|
+
readonly binary?: string;
|
|
94
|
+
readonly certificate_path?: string;
|
|
95
|
+
readonly private_key_path?: string;
|
|
96
|
+
readonly settings_path?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface AiDisclosureConfig {
|
|
100
|
+
readonly enabled?: boolean;
|
|
101
|
+
readonly badges?: boolean;
|
|
102
|
+
readonly json_ld?: boolean;
|
|
103
|
+
readonly machine_readable?: boolean;
|
|
104
|
+
readonly atom?: boolean;
|
|
105
|
+
readonly show_on_lists?: boolean;
|
|
106
|
+
readonly policy_url?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface FontConfigInput {
|
|
110
|
+
readonly enabled?: boolean;
|
|
111
|
+
readonly family?: string;
|
|
112
|
+
readonly source?: string;
|
|
113
|
+
readonly cache_dir?: string;
|
|
114
|
+
readonly weight?: string;
|
|
115
|
+
readonly skip_key?: string;
|
|
116
|
+
readonly roles?: FontRoles;
|
|
117
|
+
readonly sources?: Readonly<Record<string, FontSourceSpec>>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface SoraneConfig {
|
|
121
|
+
readonly site: {
|
|
122
|
+
readonly title: string;
|
|
123
|
+
readonly description: string;
|
|
124
|
+
readonly base_url: string;
|
|
125
|
+
readonly lang: string;
|
|
126
|
+
/** 既定の OGP 画像(絶対 URL またはサイトルート相対パス。要 base_url) */
|
|
127
|
+
readonly og_image?: string;
|
|
128
|
+
};
|
|
129
|
+
readonly build: {
|
|
130
|
+
readonly content_dir: string;
|
|
131
|
+
readonly out_dir: string;
|
|
132
|
+
readonly permalink: string;
|
|
133
|
+
/** 存在すれば out_dir へ再帰コピーする静的資産ディレクトリ(例: static/)。 */
|
|
134
|
+
readonly static_dir?: string;
|
|
135
|
+
readonly blog?: BlogBuildConfig;
|
|
136
|
+
readonly ai_disclosure?: AiDisclosureConfig;
|
|
137
|
+
readonly diagrams?: DiagramsConfig;
|
|
138
|
+
readonly image_metadata?: ImageMetadataConfig;
|
|
139
|
+
readonly c2pa?: C2paConfig;
|
|
140
|
+
};
|
|
141
|
+
readonly fonts: {
|
|
142
|
+
readonly enabled: boolean;
|
|
143
|
+
readonly cache_dir: string;
|
|
144
|
+
readonly skip_key: string;
|
|
145
|
+
readonly family?: string;
|
|
146
|
+
readonly source?: string;
|
|
147
|
+
readonly weight?: string;
|
|
148
|
+
readonly roles?: FontRoles;
|
|
149
|
+
readonly sources?: Readonly<Record<string, FontSourceSpec>>;
|
|
150
|
+
};
|
|
151
|
+
readonly search: SearchConfig & {
|
|
152
|
+
readonly mode: SearchMode;
|
|
153
|
+
readonly index: string;
|
|
154
|
+
readonly model: string;
|
|
155
|
+
readonly model_id: string;
|
|
156
|
+
readonly asset_base_url: string;
|
|
157
|
+
};
|
|
158
|
+
readonly docs?: DocsConfig;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const DEFAULT_CONFIG: SoraneConfig = {
|
|
162
|
+
site: {
|
|
163
|
+
title: "Sorane Site",
|
|
164
|
+
description: "OKF-native static site",
|
|
165
|
+
base_url: "",
|
|
166
|
+
lang: "ja",
|
|
167
|
+
},
|
|
168
|
+
build: {
|
|
169
|
+
content_dir: "content",
|
|
170
|
+
out_dir: "dist",
|
|
171
|
+
permalink: "{{slug}}.html",
|
|
172
|
+
blog: {
|
|
173
|
+
page_size: 50,
|
|
174
|
+
index_archive_limit: 15,
|
|
175
|
+
featured_mode: "excerpt",
|
|
176
|
+
excerpt_length: 400,
|
|
177
|
+
show_list_descriptions: false,
|
|
178
|
+
archives: true,
|
|
179
|
+
tags: true,
|
|
180
|
+
},
|
|
181
|
+
ai_disclosure: {},
|
|
182
|
+
diagrams: DEFAULT_DIAGRAMS_CONFIG,
|
|
183
|
+
image_metadata: {},
|
|
184
|
+
c2pa: { enabled: false, embed: true, binary: "c2patool" },
|
|
185
|
+
},
|
|
186
|
+
fonts: {
|
|
187
|
+
enabled: false,
|
|
188
|
+
family: "Sorane-Subset",
|
|
189
|
+
source: "assets/fonts/source.ttf",
|
|
190
|
+
cache_dir: ".sorane/cache/fonts",
|
|
191
|
+
weight: "450",
|
|
192
|
+
skip_key: "noFontEmbedding",
|
|
193
|
+
},
|
|
194
|
+
search: {
|
|
195
|
+
mode: "fts",
|
|
196
|
+
index: ".sorane/index.db",
|
|
197
|
+
model: "vendor/models",
|
|
198
|
+
model_id: "ruri-v3-30m",
|
|
199
|
+
asset_base_url: "",
|
|
200
|
+
bundle_model: true,
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
export function mergeConfig(partial: Partial<SoraneConfig>): SoraneConfig {
|
|
205
|
+
return {
|
|
206
|
+
site: { ...DEFAULT_CONFIG.site, ...partial.site },
|
|
207
|
+
build: {
|
|
208
|
+
...DEFAULT_CONFIG.build,
|
|
209
|
+
...partial.build,
|
|
210
|
+
blog: { ...DEFAULT_CONFIG.build.blog, ...partial.build?.blog },
|
|
211
|
+
ai_disclosure: partial.build?.ai_disclosure
|
|
212
|
+
? { ...DEFAULT_CONFIG.build.ai_disclosure, ...partial.build.ai_disclosure }
|
|
213
|
+
: DEFAULT_CONFIG.build.ai_disclosure,
|
|
214
|
+
diagrams: partial.build?.diagrams
|
|
215
|
+
? {
|
|
216
|
+
...DEFAULT_DIAGRAMS_CONFIG,
|
|
217
|
+
...partial.build.diagrams,
|
|
218
|
+
mermaid: {
|
|
219
|
+
...DEFAULT_DIAGRAMS_CONFIG.mermaid,
|
|
220
|
+
...partial.build.diagrams.mermaid,
|
|
221
|
+
},
|
|
222
|
+
d2: {
|
|
223
|
+
...DEFAULT_DIAGRAMS_CONFIG.d2,
|
|
224
|
+
...partial.build.diagrams.d2,
|
|
225
|
+
},
|
|
226
|
+
graphviz: {
|
|
227
|
+
...DEFAULT_DIAGRAMS_CONFIG.graphviz,
|
|
228
|
+
...partial.build.diagrams.graphviz,
|
|
229
|
+
},
|
|
230
|
+
}
|
|
231
|
+
: DEFAULT_DIAGRAMS_CONFIG,
|
|
232
|
+
image_metadata: partial.build?.image_metadata
|
|
233
|
+
? { ...DEFAULT_CONFIG.build.image_metadata, ...partial.build.image_metadata }
|
|
234
|
+
: DEFAULT_CONFIG.build.image_metadata,
|
|
235
|
+
c2pa: partial.build?.c2pa
|
|
236
|
+
? { ...DEFAULT_CONFIG.build.c2pa, ...partial.build.c2pa }
|
|
237
|
+
: DEFAULT_CONFIG.build.c2pa,
|
|
238
|
+
},
|
|
239
|
+
fonts: { ...DEFAULT_CONFIG.fonts, ...partial.fonts },
|
|
240
|
+
search: { ...DEFAULT_CONFIG.search, ...partial.search },
|
|
241
|
+
docs: partial.docs ? { nav: partial.docs.nav } : undefined,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** permalink テンプレートから出力 HTML ファイル名を決める。 */
|
|
246
|
+
export function resolvePermalink(
|
|
247
|
+
template: string,
|
|
248
|
+
slug: string,
|
|
249
|
+
timestamp?: string,
|
|
250
|
+
): string {
|
|
251
|
+
const date = timestamp?.slice(0, 10) ?? "";
|
|
252
|
+
return template
|
|
253
|
+
.replace(/\{\{slug\}\}/g, slug)
|
|
254
|
+
.replace(/\{\{date\}\}/g, date);
|
|
255
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import type { DiagramsConfig } from "../config.ts";
|
|
14
|
+
import { diagramSourceHash } from "./diagram-hash.ts";
|
|
15
|
+
|
|
16
|
+
const execFileAsync = promisify(execFile);
|
|
17
|
+
|
|
18
|
+
/** @deprecated Use diagramSourceHash */
|
|
19
|
+
export function d2SourceHash(source: string): string {
|
|
20
|
+
return diagramSourceHash(source);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isD2CompileEnabled(config?: DiagramsConfig): boolean {
|
|
24
|
+
return config?.enabled !== false && config?.d2?.enabled === true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveD2Binary(config: DiagramsConfig): string {
|
|
28
|
+
return config.d2?.binary ?? "d2";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CompileD2Options {
|
|
32
|
+
readonly source: string;
|
|
33
|
+
readonly binary: string;
|
|
34
|
+
readonly outDir: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CompileD2Result {
|
|
38
|
+
readonly hash: string;
|
|
39
|
+
readonly svgFileName: string;
|
|
40
|
+
readonly ok: boolean;
|
|
41
|
+
readonly warning?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function compileD2ToSvg(opts: CompileD2Options): Promise<CompileD2Result> {
|
|
45
|
+
const hash = diagramSourceHash(opts.source);
|
|
46
|
+
const svgFileName = `${hash}.svg`;
|
|
47
|
+
const dest = join(opts.outDir, svgFileName);
|
|
48
|
+
mkdirSync(opts.outDir, { recursive: true });
|
|
49
|
+
|
|
50
|
+
if (existsSync(dest)) {
|
|
51
|
+
return { hash, svgFileName, ok: true };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "sorane-d2-"));
|
|
55
|
+
try {
|
|
56
|
+
const input = join(tmpDir, "diagram.d2");
|
|
57
|
+
const output = join(tmpDir, "diagram.svg");
|
|
58
|
+
writeFileSync(input, opts.source, "utf8");
|
|
59
|
+
await execFileAsync(opts.binary, ["--layout", "elk", input, output], {
|
|
60
|
+
timeout: 120_000,
|
|
61
|
+
});
|
|
62
|
+
writeFileSync(dest, readFileSync(output), "utf8");
|
|
63
|
+
return { hash, svgFileName, ok: true };
|
|
64
|
+
} catch (err) {
|
|
65
|
+
const warning = err instanceof Error ? err.message : String(err);
|
|
66
|
+
return { hash, svgFileName, ok: false, warning };
|
|
67
|
+
} finally {
|
|
68
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import type { DiagramsConfig } from "../config.ts";
|
|
14
|
+
import { diagramSourceHash } from "./diagram-hash.ts";
|
|
15
|
+
|
|
16
|
+
const execFileAsync = promisify(execFile);
|
|
17
|
+
|
|
18
|
+
export function isGraphvizCompileEnabled(config?: DiagramsConfig): boolean {
|
|
19
|
+
return config?.enabled !== false && config?.graphviz?.enabled === true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveGraphvizBinary(config: DiagramsConfig): string {
|
|
23
|
+
return config.graphviz?.binary ?? "dot";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isGraphvizLang(lang: string | null | undefined): boolean {
|
|
27
|
+
return lang === "graphviz" || lang === "dot";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CompileGraphvizOptions {
|
|
31
|
+
readonly source: string;
|
|
32
|
+
readonly binary: string;
|
|
33
|
+
readonly outDir: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CompileGraphvizResult {
|
|
37
|
+
readonly hash: string;
|
|
38
|
+
readonly svgFileName: string;
|
|
39
|
+
readonly ok: boolean;
|
|
40
|
+
readonly warning?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function compileGraphvizToSvg(
|
|
44
|
+
opts: CompileGraphvizOptions,
|
|
45
|
+
): Promise<CompileGraphvizResult> {
|
|
46
|
+
const hash = diagramSourceHash(opts.source);
|
|
47
|
+
const svgFileName = `${hash}.svg`;
|
|
48
|
+
const dest = join(opts.outDir, svgFileName);
|
|
49
|
+
mkdirSync(opts.outDir, { recursive: true });
|
|
50
|
+
|
|
51
|
+
if (existsSync(dest)) {
|
|
52
|
+
return { hash, svgFileName, ok: true };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "sorane-dot-"));
|
|
56
|
+
try {
|
|
57
|
+
const input = join(tmpDir, "diagram.dot");
|
|
58
|
+
const output = join(tmpDir, "diagram.svg");
|
|
59
|
+
writeFileSync(input, opts.source, "utf8");
|
|
60
|
+
await execFileAsync(opts.binary, ["-Tsvg", input, "-o", output], {
|
|
61
|
+
timeout: 120_000,
|
|
62
|
+
});
|
|
63
|
+
writeFileSync(dest, readFileSync(output), "utf8");
|
|
64
|
+
return { hash, svgFileName, ok: true };
|
|
65
|
+
} catch (err) {
|
|
66
|
+
const warning = err instanceof Error ? err.message : String(err);
|
|
67
|
+
return { hash, svgFileName, ok: false, warning };
|
|
68
|
+
} finally {
|
|
69
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
import type { DiagramsConfig } from "../config.ts";
|
|
15
|
+
import { diagramSourceHash } from "./diagram-hash.ts";
|
|
16
|
+
|
|
17
|
+
const execFileAsync = promisify(execFile);
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
|
|
20
|
+
export function isMermaidBuildEnabled(config?: DiagramsConfig): boolean {
|
|
21
|
+
return config?.enabled !== false && config?.mermaid?.mode === "build";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveMmdcBinary(config: DiagramsConfig): string {
|
|
25
|
+
const configured = config.mermaid?.mmdc;
|
|
26
|
+
if (configured && configured.length > 0 && configured !== "mmdc") return configured;
|
|
27
|
+
const candidates = [
|
|
28
|
+
join(process.cwd(), "node_modules", ".bin", "mmdc"),
|
|
29
|
+
(() => {
|
|
30
|
+
try {
|
|
31
|
+
const pkgPath = require.resolve("@mermaid-js/mermaid-cli/package.json");
|
|
32
|
+
return join(dirname(pkgPath), "node_modules", ".bin", "mmdc");
|
|
33
|
+
} catch {
|
|
34
|
+
return "";
|
|
35
|
+
}
|
|
36
|
+
})(),
|
|
37
|
+
(() => {
|
|
38
|
+
try {
|
|
39
|
+
return require.resolve("@mermaid-js/mermaid-cli/src/cli.js");
|
|
40
|
+
} catch {
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
})(),
|
|
44
|
+
];
|
|
45
|
+
for (const candidate of candidates) {
|
|
46
|
+
if (candidate.length > 0 && existsSync(candidate)) return candidate;
|
|
47
|
+
}
|
|
48
|
+
return configured && configured.length > 0 ? configured : "mmdc";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface CompileMermaidOptions {
|
|
52
|
+
readonly source: string;
|
|
53
|
+
readonly binary: string;
|
|
54
|
+
readonly outDir: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface CompileMermaidResult {
|
|
58
|
+
readonly hash: string;
|
|
59
|
+
readonly svgFileName: string;
|
|
60
|
+
readonly ok: boolean;
|
|
61
|
+
readonly warning?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const MERMAID_CONFIG = {
|
|
65
|
+
securityLevel: "strict",
|
|
66
|
+
deterministicIds: true,
|
|
67
|
+
deterministicIDSeed: "sorane",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export async function compileMermaidToSvg(
|
|
71
|
+
opts: CompileMermaidOptions,
|
|
72
|
+
): Promise<CompileMermaidResult> {
|
|
73
|
+
const hash = diagramSourceHash(opts.source);
|
|
74
|
+
const svgFileName = `${hash}.svg`;
|
|
75
|
+
const dest = join(opts.outDir, svgFileName);
|
|
76
|
+
mkdirSync(opts.outDir, { recursive: true });
|
|
77
|
+
|
|
78
|
+
if (existsSync(dest)) {
|
|
79
|
+
return { hash, svgFileName, ok: true };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "sorane-mmdc-"));
|
|
83
|
+
try {
|
|
84
|
+
const input = join(tmpDir, "diagram.mmd");
|
|
85
|
+
const output = join(tmpDir, "diagram.svg");
|
|
86
|
+
const configPath = join(tmpDir, "mermaidConfig.json");
|
|
87
|
+
writeFileSync(input, opts.source, "utf8");
|
|
88
|
+
writeFileSync(configPath, JSON.stringify(MERMAID_CONFIG), "utf8");
|
|
89
|
+
await execFileAsync(
|
|
90
|
+
opts.binary,
|
|
91
|
+
["-i", input, "-o", output, "-c", configPath, "--quiet"],
|
|
92
|
+
{ timeout: 180_000 },
|
|
93
|
+
);
|
|
94
|
+
writeFileSync(dest, readFileSync(output), "utf8");
|
|
95
|
+
return { hash, svgFileName, ok: true };
|
|
96
|
+
} catch (err) {
|
|
97
|
+
const warning = err instanceof Error ? err.message : String(err);
|
|
98
|
+
return { hash, svgFileName, ok: false, warning };
|
|
99
|
+
} finally {
|
|
100
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
}
|