@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
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { TocEntry, RenderOptions } from "../render.ts";
|
|
2
|
+
import { renderMarkdownDocument } from "../render.ts";
|
|
3
|
+
import type { DiagramRenderMeta } from "./diagram-meta.ts";
|
|
4
|
+
import { emptyDiagramMeta } from "./diagram-meta.ts";
|
|
5
|
+
import { needsAsyncDiagramCompile } from "./needs-async-compile.ts";
|
|
6
|
+
import {
|
|
7
|
+
renderMarkdownDocumentAsync,
|
|
8
|
+
type AsyncRenderOptions,
|
|
9
|
+
} from "./render-async.ts";
|
|
10
|
+
|
|
11
|
+
export interface BodySectionResult {
|
|
12
|
+
readonly html: string;
|
|
13
|
+
readonly diagrams: DiagramRenderMeta;
|
|
14
|
+
readonly outline: readonly TocEntry[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type BodySectionOptions = RenderOptions & AsyncRenderOptions;
|
|
18
|
+
|
|
19
|
+
export function renderBodySection(
|
|
20
|
+
markdown: string,
|
|
21
|
+
opts?: BodySectionOptions,
|
|
22
|
+
): BodySectionResult {
|
|
23
|
+
const rendered = renderMarkdownDocument(markdown, opts);
|
|
24
|
+
return {
|
|
25
|
+
html: rendered.html,
|
|
26
|
+
diagrams: rendered.diagrams ?? emptyDiagramMeta(),
|
|
27
|
+
outline: rendered.outline,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function renderBodySectionAsync(
|
|
32
|
+
markdown: string,
|
|
33
|
+
opts?: BodySectionOptions,
|
|
34
|
+
): Promise<BodySectionResult> {
|
|
35
|
+
const rendered = await renderMarkdownDocumentAsync(markdown, opts);
|
|
36
|
+
return {
|
|
37
|
+
html: rendered.html,
|
|
38
|
+
diagrams: rendered.diagrams ?? emptyDiagramMeta(),
|
|
39
|
+
outline: rendered.outline,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** ビルド時コンパイルが必要なら非同期、それ以外は同期。 */
|
|
44
|
+
export async function renderBodySectionForConfig(
|
|
45
|
+
markdown: string,
|
|
46
|
+
opts?: BodySectionOptions,
|
|
47
|
+
): Promise<BodySectionResult> {
|
|
48
|
+
if (needsAsyncDiagramCompile(opts?.diagrams)) {
|
|
49
|
+
return renderBodySectionAsync(markdown, opts);
|
|
50
|
+
}
|
|
51
|
+
return renderBodySection(markdown, opts);
|
|
52
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { DiagramsConfig } from "../config.ts";
|
|
2
|
+
import { isGraphvizLang } from "./compile-graphviz.ts";
|
|
3
|
+
import { extractAltText } from "./parse-diagram-fence.ts";
|
|
4
|
+
|
|
5
|
+
const FENCE_OPEN_RE = /^(`{3,}|~{3,})(\S*)\s*(.*)$/;
|
|
6
|
+
|
|
7
|
+
function isDiagramLangActive(lang: string, config: DiagramsConfig): boolean {
|
|
8
|
+
if (config.enabled === false) return false;
|
|
9
|
+
if (lang === "mermaid") return config.mermaid?.mode !== "off";
|
|
10
|
+
if (lang === "d2") return config.d2?.enabled === true;
|
|
11
|
+
if (isGraphvizLang(lang)) return config.graphviz?.enabled === true;
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Markdown 本文の図表フェンスで alt 欠落を検出する(warning のみ、ビルドは継続)。 */
|
|
16
|
+
export function validateDiagramAltWarnings(
|
|
17
|
+
body: string,
|
|
18
|
+
config: DiagramsConfig,
|
|
19
|
+
): readonly string[] {
|
|
20
|
+
const warnings: string[] = [];
|
|
21
|
+
const lines = body.split(/\r?\n/);
|
|
22
|
+
let i = 0;
|
|
23
|
+
while (i < lines.length) {
|
|
24
|
+
const open = FENCE_OPEN_RE.exec(lines[i]!);
|
|
25
|
+
if (open === null) {
|
|
26
|
+
i++;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const marker = open[1]!;
|
|
30
|
+
const lang = open[2] ?? "";
|
|
31
|
+
const meta = open[3] ?? "";
|
|
32
|
+
const close = marker[0]!;
|
|
33
|
+
const minLen = marker.length;
|
|
34
|
+
const block: string[] = [];
|
|
35
|
+
i++;
|
|
36
|
+
while (i < lines.length) {
|
|
37
|
+
const line = lines[i]!;
|
|
38
|
+
if (
|
|
39
|
+
line.length >= minLen &&
|
|
40
|
+
line.startsWith(close.repeat(minLen)) &&
|
|
41
|
+
(line.length === minLen || /^\s*$/.test(line.slice(minLen)))
|
|
42
|
+
) {
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
block.push(line);
|
|
46
|
+
i++;
|
|
47
|
+
}
|
|
48
|
+
i++;
|
|
49
|
+
if (!isDiagramLangActive(lang, config)) continue;
|
|
50
|
+
if (extractAltText(meta, block.join("\n"))) continue;
|
|
51
|
+
warnings.push(
|
|
52
|
+
`diagram (${lang}) has no alt text; add alt="..." to the fence info string or a %% alt: comment`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return warnings;
|
|
56
|
+
}
|
package/src/docs.ts
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import type { OkfConcept } from "@sorane/okf";
|
|
2
|
+
import { dirname, relative } from "node:path";
|
|
3
|
+
import type { DiagramsConfig, DocsNavSpec } from "./config.ts";
|
|
4
|
+
import type { DiagramRenderMeta } from "./diagrams/diagram-meta.ts";
|
|
5
|
+
import {
|
|
6
|
+
escapeHtml,
|
|
7
|
+
stripDuplicateTitleHeading,
|
|
8
|
+
type RenderedMarkdown,
|
|
9
|
+
renderMarkdownDocument,
|
|
10
|
+
} from "./render.ts";
|
|
11
|
+
import { needsAsyncDiagramCompile } from "./diagrams/needs-async-compile.ts";
|
|
12
|
+
import {
|
|
13
|
+
renderMarkdownDocumentAsync,
|
|
14
|
+
type AsyncRenderOptions,
|
|
15
|
+
} from "./diagrams/render-async.ts";
|
|
16
|
+
import { siteLabels } from "./site-labels.ts";
|
|
17
|
+
|
|
18
|
+
export interface DocsArticleRenderOpts extends AsyncRenderOptions {
|
|
19
|
+
readonly badgeHtml?: string;
|
|
20
|
+
readonly diagrams?: DiagramsConfig;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface DocsArticleResult {
|
|
24
|
+
readonly bodyHtml: string;
|
|
25
|
+
readonly diagrams: DiagramRenderMeta;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ArticleNav {
|
|
29
|
+
readonly prev?: { href: string; title: string };
|
|
30
|
+
readonly next?: { href: string; title: string };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const SERIF_FONT_STYLES = new Set(["GJM", "serif", "mincho"]);
|
|
34
|
+
|
|
35
|
+
function articleFontClass(concept: OkfConcept): string {
|
|
36
|
+
const font = concept.frontmatter.font;
|
|
37
|
+
if (typeof font !== "string") return "";
|
|
38
|
+
return SERIF_FONT_STYLES.has(font) ? " font-serif" : "";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function relLinkFrom(fromRel: string, toRel: string): string {
|
|
42
|
+
const from = fromRel.replace(/\\/g, "/");
|
|
43
|
+
const to = toRel.replace(/\\/g, "/");
|
|
44
|
+
const rel = relative(dirname(from), to).split("\\").join("/");
|
|
45
|
+
return rel.length > 0 ? rel : "./";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface DocsNavItem {
|
|
49
|
+
readonly href: string;
|
|
50
|
+
readonly title: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function docsNavFor(
|
|
54
|
+
href: string,
|
|
55
|
+
items: readonly DocsNavItem[],
|
|
56
|
+
): ArticleNav | undefined {
|
|
57
|
+
const i = items.findIndex((item) => item.href === href);
|
|
58
|
+
if (i < 0) return undefined;
|
|
59
|
+
const prev =
|
|
60
|
+
i > 0 ? { href: items[i - 1]!.href, title: items[i - 1]!.title } : undefined;
|
|
61
|
+
const next =
|
|
62
|
+
i < items.length - 1
|
|
63
|
+
? { href: items[i + 1]!.href, title: items[i + 1]!.title }
|
|
64
|
+
: undefined;
|
|
65
|
+
if (!prev && !next) return undefined;
|
|
66
|
+
return { prev, next };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function docsSidebarHtml(
|
|
70
|
+
items: readonly DocsNavItem[],
|
|
71
|
+
currentHref: string,
|
|
72
|
+
fromRel: string,
|
|
73
|
+
): string {
|
|
74
|
+
if (items.length === 0) return "";
|
|
75
|
+
const links = items
|
|
76
|
+
.map((item) => {
|
|
77
|
+
const href = relLinkFrom(fromRel, item.href);
|
|
78
|
+
const current = item.href === currentHref ? ' aria-current="page"' : "";
|
|
79
|
+
return (
|
|
80
|
+
`<li class="docs-nav-item">` +
|
|
81
|
+
`<a href="${escapeHtml(href)}" class="docs-nav-link"${current}>${escapeHtml(item.title)}</a>` +
|
|
82
|
+
`</li>`
|
|
83
|
+
);
|
|
84
|
+
})
|
|
85
|
+
.join("\n");
|
|
86
|
+
return (
|
|
87
|
+
`<nav class="docs-sidebar-nav" aria-label="ドキュメント">\n` +
|
|
88
|
+
`<ul class="docs-nav-list">\n${links}\n</ul>\n` +
|
|
89
|
+
`</nav>\n`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function pageTocHtml(outline: RenderedMarkdown["outline"], lang: string): string {
|
|
94
|
+
const entries = outline.filter((e) => e.depth >= 2 && e.depth <= 4);
|
|
95
|
+
if (entries.length < 2) return "";
|
|
96
|
+
const label = siteLabels(lang).toc;
|
|
97
|
+
const items = entries
|
|
98
|
+
.map((e) => {
|
|
99
|
+
const cls = e.depth > 2 ? ` class="page-toc-depth-${e.depth}"` : "";
|
|
100
|
+
return (
|
|
101
|
+
`<li${cls}><a href="#${escapeHtml(e.id)}">${escapeHtml(e.text)}</a></li>`
|
|
102
|
+
);
|
|
103
|
+
})
|
|
104
|
+
.join("\n");
|
|
105
|
+
return (
|
|
106
|
+
`<nav class="page-toc" aria-label="${escapeHtml(label)}">\n` +
|
|
107
|
+
`<p class="page-toc-title">${escapeHtml(label)}</p>\n` +
|
|
108
|
+
`<ul class="page-toc-list">\n${items}\n</ul>\n` +
|
|
109
|
+
`</nav>\n`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function docsPagerHtml(nav: ArticleNav | undefined, lang: string): string {
|
|
114
|
+
if (!nav?.prev && !nav?.next) return "";
|
|
115
|
+
const labels = siteLabels(lang);
|
|
116
|
+
const prev = nav.prev
|
|
117
|
+
? `<a class="docs-pager-card docs-pager-prev" href="${escapeHtml(nav.prev.href)}">` +
|
|
118
|
+
`<span class="docs-pager-label">${escapeHtml(labels.prevPage)}</span>` +
|
|
119
|
+
`<span class="docs-pager-title">${escapeHtml(nav.prev.title)}</span>` +
|
|
120
|
+
`</a>`
|
|
121
|
+
: `<span class="docs-pager-spacer" aria-hidden="true"></span>`;
|
|
122
|
+
const next = nav.next
|
|
123
|
+
? `<a class="docs-pager-card docs-pager-next" href="${escapeHtml(nav.next.href)}">` +
|
|
124
|
+
`<span class="docs-pager-label">${escapeHtml(labels.nextPage)}</span>` +
|
|
125
|
+
`<span class="docs-pager-title">${escapeHtml(nav.next.title)}</span>` +
|
|
126
|
+
`</a>`
|
|
127
|
+
: `<span class="docs-pager-spacer" aria-hidden="true"></span>`;
|
|
128
|
+
return (
|
|
129
|
+
`<nav class="docs-pager" aria-label="${escapeHtml(labels.pageNav)}">\n` +
|
|
130
|
+
`${prev}\n${next}\n` +
|
|
131
|
+
`</nav>\n`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function renderDocsArticleBody(
|
|
136
|
+
concept: OkfConcept,
|
|
137
|
+
rendered: RenderedMarkdown,
|
|
138
|
+
nav: ArticleNav | undefined,
|
|
139
|
+
lang: string,
|
|
140
|
+
opts?: DocsArticleRenderOpts,
|
|
141
|
+
): string {
|
|
142
|
+
const badge = opts?.badgeHtml ?? "";
|
|
143
|
+
const header = `<header>\n<h1>${escapeHtml(concept.title)}</h1>\n${badge}</header>\n`;
|
|
144
|
+
const toc = pageTocHtml(rendered.outline, lang);
|
|
145
|
+
return (
|
|
146
|
+
`<article class="article-page docs-page${articleFontClass(concept)}">\n` +
|
|
147
|
+
`${header}\n` +
|
|
148
|
+
`${toc}` +
|
|
149
|
+
`<div class="article-body docs-content">\n${rendered.html}</div>\n` +
|
|
150
|
+
`${docsPagerHtml(nav, lang)}\n` +
|
|
151
|
+
`</article>\n`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function renderDocsIndexBody(opts: {
|
|
156
|
+
readonly siteTitle: string;
|
|
157
|
+
readonly description?: string;
|
|
158
|
+
readonly introHtml?: string;
|
|
159
|
+
readonly docsNav: readonly DocsNavItem[];
|
|
160
|
+
readonly profileUrl?: string;
|
|
161
|
+
readonly githubUrl?: string;
|
|
162
|
+
readonly lang?: string;
|
|
163
|
+
}): string {
|
|
164
|
+
const labels = siteLabels(opts.lang ?? "ja");
|
|
165
|
+
const links = [
|
|
166
|
+
opts.profileUrl
|
|
167
|
+
? `<a href="${escapeHtml(opts.profileUrl)}" class="blog-profile-link">${escapeHtml(labels.profile)}</a>`
|
|
168
|
+
: "",
|
|
169
|
+
opts.githubUrl
|
|
170
|
+
? `<a href="${escapeHtml(opts.githubUrl)}" class="blog-profile-link">${escapeHtml(labels.github)}</a>`
|
|
171
|
+
: "",
|
|
172
|
+
]
|
|
173
|
+
.filter(Boolean)
|
|
174
|
+
.join(" ");
|
|
175
|
+
const profile = links ? links : "";
|
|
176
|
+
const intro = opts.introHtml ? `<div class="docs-intro">${opts.introHtml}</div>` : "";
|
|
177
|
+
const items = opts.docsNav
|
|
178
|
+
.map(
|
|
179
|
+
(item) =>
|
|
180
|
+
`<li class="docs-index-item">` +
|
|
181
|
+
`<a href="${escapeHtml(item.href)}" class="docs-index-link">${escapeHtml(item.title)}</a>` +
|
|
182
|
+
`</li>`,
|
|
183
|
+
)
|
|
184
|
+
.join("\n");
|
|
185
|
+
const navSection =
|
|
186
|
+
items.length > 0
|
|
187
|
+
? `<section class="docs-index-nav">\n` +
|
|
188
|
+
`<h2>${escapeHtml(labels.documentation)}</h2>\n` +
|
|
189
|
+
`<ul class="docs-index-list">\n${items}\n</ul>\n` +
|
|
190
|
+
`</section>\n`
|
|
191
|
+
: "";
|
|
192
|
+
return (
|
|
193
|
+
`<div class="docs-index">\n` +
|
|
194
|
+
`<header class="docs-index-header">\n` +
|
|
195
|
+
`<h1>${escapeHtml(opts.siteTitle)}</h1>\n` +
|
|
196
|
+
(opts.description ? `<p class="blog-lead">${escapeHtml(opts.description)}</p>\n` : "") +
|
|
197
|
+
(profile ? `<div class="blog-profile">${profile}</div>\n` : "") +
|
|
198
|
+
`${intro}` +
|
|
199
|
+
`</header>\n` +
|
|
200
|
+
`${navSection}` +
|
|
201
|
+
`</div>\n`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function resolveDocsNav(
|
|
206
|
+
nav: readonly DocsNavSpec[] | undefined,
|
|
207
|
+
titleByHref: ReadonlyMap<string, string>,
|
|
208
|
+
): DocsNavItem[] {
|
|
209
|
+
if (!nav || nav.length === 0) return [];
|
|
210
|
+
return nav.map((spec) => {
|
|
211
|
+
const href = typeof spec === "string" ? spec : spec.href;
|
|
212
|
+
const title =
|
|
213
|
+
(typeof spec === "object" && spec.title) ||
|
|
214
|
+
titleByHref.get(href) ||
|
|
215
|
+
href.replace(/\.html$/i, "");
|
|
216
|
+
return { href, title };
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function renderDocsArticleFromConceptWithMeta(
|
|
221
|
+
concept: OkfConcept,
|
|
222
|
+
nav: ArticleNav | undefined,
|
|
223
|
+
lang: string,
|
|
224
|
+
opts?: DocsArticleRenderOpts,
|
|
225
|
+
): DocsArticleResult {
|
|
226
|
+
const body = stripDuplicateTitleHeading(concept.body, concept.title);
|
|
227
|
+
const rendered = renderMarkdownDocument(body, opts);
|
|
228
|
+
return {
|
|
229
|
+
bodyHtml: renderDocsArticleBody(concept, rendered, nav, lang, opts),
|
|
230
|
+
diagrams: rendered.diagrams ?? { mermaid: 0, d2: 0, graphviz: 0 },
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function renderDocsArticleFromConceptWithMetaForConfig(
|
|
235
|
+
concept: OkfConcept,
|
|
236
|
+
nav: ArticleNav | undefined,
|
|
237
|
+
lang: string,
|
|
238
|
+
opts?: DocsArticleRenderOpts,
|
|
239
|
+
): Promise<DocsArticleResult> {
|
|
240
|
+
const body = stripDuplicateTitleHeading(concept.body, concept.title);
|
|
241
|
+
const rendered = needsAsyncDiagramCompile(opts?.diagrams)
|
|
242
|
+
? await renderMarkdownDocumentAsync(body, opts)
|
|
243
|
+
: renderMarkdownDocument(body, opts);
|
|
244
|
+
return {
|
|
245
|
+
bodyHtml: renderDocsArticleBody(concept, rendered, nav, lang, opts),
|
|
246
|
+
diagrams: rendered.diagrams ?? { mermaid: 0, d2: 0, graphviz: 0 },
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function renderDocsArticleFromConcept(
|
|
251
|
+
concept: OkfConcept,
|
|
252
|
+
nav: ArticleNav | undefined,
|
|
253
|
+
lang: string,
|
|
254
|
+
opts?: DocsArticleRenderOpts,
|
|
255
|
+
): string {
|
|
256
|
+
return renderDocsArticleFromConceptWithMeta(concept, nav, lang, opts).bodyHtml;
|
|
257
|
+
}
|
package/src/emit-page.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import type { ParsedConcept } from "@sorane/okf";
|
|
4
|
+
import { conceptToOkfMarkdown } from "@sorane/okf";
|
|
5
|
+
import { buildPage } from "./ssg.ts";
|
|
6
|
+
import type { SoraneConfig } from "./config.ts";
|
|
7
|
+
import { resolveOgImageUrl } from "./og-meta.ts";
|
|
8
|
+
import { extractDescription } from "./ssg.ts";
|
|
9
|
+
|
|
10
|
+
function pageOgImage(
|
|
11
|
+
frontmatter: Record<string, unknown>,
|
|
12
|
+
siteOgImage: string | undefined,
|
|
13
|
+
baseUrl: string,
|
|
14
|
+
): string | undefined {
|
|
15
|
+
const raw =
|
|
16
|
+
typeof frontmatter.og_image === "string"
|
|
17
|
+
? frontmatter.og_image
|
|
18
|
+
: typeof frontmatter.ogImage === "string"
|
|
19
|
+
? frontmatter.ogImage
|
|
20
|
+
: siteOgImage;
|
|
21
|
+
return resolveOgImageUrl(baseUrl, raw);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface EmitPageOptions {
|
|
25
|
+
readonly cwd: string;
|
|
26
|
+
readonly config: SoraneConfig;
|
|
27
|
+
readonly outDir: string;
|
|
28
|
+
readonly outRel: string;
|
|
29
|
+
readonly concept: ParsedConcept["concept"];
|
|
30
|
+
readonly bodyHtml: string;
|
|
31
|
+
readonly baseUrl: string;
|
|
32
|
+
readonly fontCss?: string;
|
|
33
|
+
readonly extraHead?: string[];
|
|
34
|
+
readonly isIndex?: boolean;
|
|
35
|
+
readonly showArchiveNav?: boolean;
|
|
36
|
+
readonly searchPath?: string;
|
|
37
|
+
readonly pageKind?: "website" | "article";
|
|
38
|
+
readonly docsLayout?: boolean;
|
|
39
|
+
readonly docsSidebarHtml?: string;
|
|
40
|
+
readonly headerSearchHtml?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function emitPage(opts: EmitPageOptions): { mdOutRel: string; canonicalUrl?: string } {
|
|
44
|
+
const outAbs = join(opts.outDir, opts.outRel);
|
|
45
|
+
mkdirSync(dirname(outAbs), { recursive: true });
|
|
46
|
+
|
|
47
|
+
const depth = opts.outRel.replace(/\\/g, "/").split("/").length - 1;
|
|
48
|
+
const rootPrefix = depth > 0 ? "../".repeat(depth) : "./";
|
|
49
|
+
|
|
50
|
+
const description =
|
|
51
|
+
opts.concept.description ??
|
|
52
|
+
extractDescription(opts.concept.body) ??
|
|
53
|
+
(opts.isIndex ? opts.config.site.description : undefined);
|
|
54
|
+
const canonicalUrl =
|
|
55
|
+
opts.baseUrl.length > 0 ? `${opts.baseUrl.replace(/\/$/, "")}/${opts.outRel}` : undefined;
|
|
56
|
+
|
|
57
|
+
const mdOutRel = opts.outRel.replace(/\.html$/, ".md");
|
|
58
|
+
writeFileSync(join(opts.outDir, mdOutRel), conceptToOkfMarkdown(opts.concept), "utf8");
|
|
59
|
+
|
|
60
|
+
const extraHead = [
|
|
61
|
+
...(opts.extraHead ?? []),
|
|
62
|
+
...(opts.fontCss ? [opts.fontCss] : []),
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const html = buildPage({
|
|
66
|
+
title: opts.concept.title,
|
|
67
|
+
siteTitle: opts.config.site.title,
|
|
68
|
+
bodyHtml: opts.bodyHtml,
|
|
69
|
+
rootPrefix,
|
|
70
|
+
description,
|
|
71
|
+
canonicalUrl,
|
|
72
|
+
lang: opts.config.site.lang,
|
|
73
|
+
feedPath: "feed.xml",
|
|
74
|
+
showArchiveNav: opts.showArchiveNav,
|
|
75
|
+
searchPath: opts.searchPath,
|
|
76
|
+
pageKind: opts.pageKind ?? (opts.isIndex ? "website" : "article"),
|
|
77
|
+
machineSources: [{ href: mdOutRel, type: "text/markdown" }],
|
|
78
|
+
extraHead: extraHead.length > 0 ? extraHead : undefined,
|
|
79
|
+
docsLayout: opts.docsLayout,
|
|
80
|
+
docsSidebarHtml: opts.docsSidebarHtml,
|
|
81
|
+
headerSearchHtml: opts.headerSearchHtml,
|
|
82
|
+
ogImageUrl: pageOgImage(opts.concept.frontmatter, opts.config.site.og_image, opts.baseUrl),
|
|
83
|
+
});
|
|
84
|
+
writeFileSync(outAbs, html, "utf8");
|
|
85
|
+
|
|
86
|
+
return { mdOutRel, canonicalUrl };
|
|
87
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export { runBuild, type BuildOptions, type BuildResult } from "./build.ts";
|
|
2
|
+
export { mergeConfig, DEFAULT_CONFIG, resolvePermalink, type SoraneConfig } from "./config.ts";
|
|
3
|
+
export { migrateToOkf, parseBumpProfileArg, type MigrateToOkfOptions } from "./migrate.ts";
|
|
4
|
+
export { validateHeadingWarnings } from "./validate-heading-structure.ts";
|
|
5
|
+
export { processStaticAssets, type StaticAssetPassResult } from "./static-assets.ts";
|
|
6
|
+
export {
|
|
7
|
+
signRasterWithC2pa,
|
|
8
|
+
probeC2paManifest,
|
|
9
|
+
c2patoolAvailable,
|
|
10
|
+
resolveC2paCredentials,
|
|
11
|
+
isC2paRasterPath,
|
|
12
|
+
} from "./c2pa-pass.ts";
|
|
13
|
+
export {
|
|
14
|
+
loadAssetProvenance,
|
|
15
|
+
resolveC2paCreateIntent,
|
|
16
|
+
type AssetProvenanceEntry,
|
|
17
|
+
} from "./asset-provenance.ts";
|
|
18
|
+
export {
|
|
19
|
+
buildPage,
|
|
20
|
+
extractDescription,
|
|
21
|
+
renderArticleBody,
|
|
22
|
+
renderIndexBody,
|
|
23
|
+
rootPrefixFromRel,
|
|
24
|
+
relLinkFrom,
|
|
25
|
+
buildSearchMount,
|
|
26
|
+
buildSearchHead,
|
|
27
|
+
isSearchView,
|
|
28
|
+
sanitizeListDescription,
|
|
29
|
+
articleFontClass,
|
|
30
|
+
renderFeaturedExcerpt,
|
|
31
|
+
buildWebSiteJsonLd,
|
|
32
|
+
} from "./ssg.ts";
|
|
33
|
+
export { siteLabels, type SiteLabels } from "./site-labels.ts";
|
|
34
|
+
export type { FeaturedMode } from "./config.ts";
|
|
35
|
+
export {
|
|
36
|
+
renderMarkdown,
|
|
37
|
+
rewriteLinks,
|
|
38
|
+
escapeHtml,
|
|
39
|
+
stripDuplicateTitleHeading,
|
|
40
|
+
} from "./render.ts";
|
|
41
|
+
export { buildCatalogJsonLd, type CatalogEntry } from "./catalog.ts";
|
|
42
|
+
export {
|
|
43
|
+
buildRobotsTxt,
|
|
44
|
+
buildSitemapXml,
|
|
45
|
+
buildLlmsTxt,
|
|
46
|
+
type SiteEntry,
|
|
47
|
+
} from "./site-meta.ts";
|
|
48
|
+
export { validateDiagramAltWarnings } from "./diagrams/validate-diagram-alt.ts";
|
|
49
|
+
export { resolveOgImageUrl, ogLocaleFromLang } from "./og-meta.ts";
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { resolveDigitalSourceType } from "@sorane/okf";
|
|
3
|
+
import type { AssetProvenanceEntry } from "./asset-provenance.ts";
|
|
4
|
+
|
|
5
|
+
const IMAGE_METADATA_RE = /\.(jpe?g|png|webp)$/i;
|
|
6
|
+
|
|
7
|
+
export function isImageMetadataPath(filePath: string): boolean {
|
|
8
|
+
return IMAGE_METADATA_RE.test(filePath);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function hasImageMetadataFields(entry: AssetProvenanceEntry | undefined): boolean {
|
|
12
|
+
if (!entry) return false;
|
|
13
|
+
return Boolean(
|
|
14
|
+
entry.digitalSourceType ||
|
|
15
|
+
entry.aiDisclosureNote ||
|
|
16
|
+
(entry.aiSystems && entry.aiSystems.length > 0),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** ExifTool 引数を組み立てる(in-place 書き込み用)。 */
|
|
21
|
+
export function buildIptcXmpExiftoolArgs(entry: AssetProvenanceEntry): string[] {
|
|
22
|
+
const args: string[] = ["-overwrite_original"];
|
|
23
|
+
|
|
24
|
+
if (entry.digitalSourceType) {
|
|
25
|
+
const resolved = resolveDigitalSourceType(entry.digitalSourceType);
|
|
26
|
+
if (resolved) {
|
|
27
|
+
args.push(`-XMP-iptcExt:DigitalSourceType=${resolved.uri}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (entry.aiDisclosureNote) {
|
|
32
|
+
args.push(`-XMP-iptcExt:AIPromptInformation=${entry.aiDisclosureNote}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (entry.aiSystems && entry.aiSystems.length > 0) {
|
|
36
|
+
const first = entry.aiSystems[0]!;
|
|
37
|
+
const label = first.provider ? `${first.name} (${first.provider})` : first.name;
|
|
38
|
+
args.push(`-XMP-iptcExt:AISystemUsed=${label}`);
|
|
39
|
+
if (first.version) {
|
|
40
|
+
args.push(`-XMP-iptcExt:AISystemVersionUsed=${first.version}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return args;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** ExifTool で IPTC Extension XMP を画像に埋め込む。 */
|
|
48
|
+
export function embedIptcXmp(
|
|
49
|
+
filePath: string,
|
|
50
|
+
entry: AssetProvenanceEntry,
|
|
51
|
+
opts: { readonly binary?: string } = {},
|
|
52
|
+
): { readonly ok: boolean; readonly message?: string } {
|
|
53
|
+
const args = buildIptcXmpExiftoolArgs(entry);
|
|
54
|
+
if (args.length <= 1) {
|
|
55
|
+
return { ok: false, message: "no IPTC XMP fields to write" };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const binary = opts.binary ?? "exiftool";
|
|
59
|
+
const result = spawnSync(binary, [...args, filePath], {
|
|
60
|
+
encoding: "utf8",
|
|
61
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (result.status !== 0) {
|
|
65
|
+
const msg = (result.stderr || result.stdout || "").trim();
|
|
66
|
+
return { ok: false, message: msg.length > 0 ? msg : `exiftool exited ${result.status}` };
|
|
67
|
+
}
|
|
68
|
+
return { ok: true };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function exiftoolAvailable(binary = "exiftool"): boolean {
|
|
72
|
+
const result = spawnSync(binary, ["-ver"], {
|
|
73
|
+
encoding: "utf8",
|
|
74
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
75
|
+
});
|
|
76
|
+
return result.status === 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 出力画像に期待する XMP が付いているか簡易確認。 */
|
|
80
|
+
export function probeIptcXmp(
|
|
81
|
+
filePath: string,
|
|
82
|
+
expectedUri: string | undefined,
|
|
83
|
+
binary = "exiftool",
|
|
84
|
+
): boolean {
|
|
85
|
+
const result = spawnSync(
|
|
86
|
+
binary,
|
|
87
|
+
["-s", "-s", "-s", "-G1", "-XMP-iptcExt:DigitalSourceType", filePath],
|
|
88
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
|
|
89
|
+
);
|
|
90
|
+
if (result.status !== 0) return false;
|
|
91
|
+
const value = (result.stdout || "").trim();
|
|
92
|
+
if (!expectedUri) return value.length > 0;
|
|
93
|
+
return value === expectedUri || value.endsWith(expectedUri.split("/").pop() ?? "");
|
|
94
|
+
}
|