@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,198 @@
|
|
|
1
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, join, relative } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
loadAssetProvenance,
|
|
5
|
+
lookupAssetProvenance,
|
|
6
|
+
resolveC2paCreateIntent,
|
|
7
|
+
} from "./asset-provenance.ts";
|
|
8
|
+
import {
|
|
9
|
+
c2patoolAvailable,
|
|
10
|
+
isC2paRasterPath,
|
|
11
|
+
resolveC2paCredentials,
|
|
12
|
+
signRasterWithC2pa,
|
|
13
|
+
type C2paCredentials,
|
|
14
|
+
} from "./c2pa-pass.ts";
|
|
15
|
+
import {
|
|
16
|
+
embedIptcXmp,
|
|
17
|
+
exiftoolAvailable,
|
|
18
|
+
hasImageMetadataFields,
|
|
19
|
+
isImageMetadataPath,
|
|
20
|
+
} from "./iptc-xmp-pass.ts";
|
|
21
|
+
import type { MarkdownImageRef } from "./markdown-image-refs.ts";
|
|
22
|
+
import type { C2paConfig, ImageMetadataConfig } from "./config.ts";
|
|
23
|
+
|
|
24
|
+
export interface StaticAssetPassOptions {
|
|
25
|
+
readonly cwd: string;
|
|
26
|
+
readonly staticSrc: string;
|
|
27
|
+
readonly outDir: string;
|
|
28
|
+
readonly staticDirName: string;
|
|
29
|
+
readonly contentDir: string;
|
|
30
|
+
readonly c2pa?: C2paConfig;
|
|
31
|
+
readonly imageMetadata?: ImageMetadataConfig;
|
|
32
|
+
readonly skipC2pa?: boolean;
|
|
33
|
+
readonly inlineImages?: readonly MarkdownImageRef[];
|
|
34
|
+
readonly onWarning?: (message: string) => void;
|
|
35
|
+
readonly onProgress?: (message: string) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface StaticAssetPassResult {
|
|
39
|
+
readonly rasterSigned: number;
|
|
40
|
+
readonly rasterCopied: number;
|
|
41
|
+
readonly metadataEmbedded: number;
|
|
42
|
+
readonly inlineImagesCopied: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function walkFiles(root: string): string[] {
|
|
46
|
+
const out: string[] = [];
|
|
47
|
+
function visit(dir: string): void {
|
|
48
|
+
for (const name of readdirSync(dir).sort()) {
|
|
49
|
+
const abs = join(dir, name);
|
|
50
|
+
if (statSync(abs).isDirectory()) visit(abs);
|
|
51
|
+
else out.push(abs);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
visit(root);
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function processStaticAssets(
|
|
59
|
+
opts: StaticAssetPassOptions,
|
|
60
|
+
): Promise<StaticAssetPassResult> {
|
|
61
|
+
const warn = opts.onWarning ?? (() => {});
|
|
62
|
+
const log = opts.onProgress ?? (() => {});
|
|
63
|
+
|
|
64
|
+
const outRoot = join(opts.outDir, opts.staticDirName);
|
|
65
|
+
if (existsSync(opts.staticSrc)) {
|
|
66
|
+
cpSync(opts.staticSrc, outRoot, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const provenance = loadAssetProvenance(
|
|
70
|
+
opts.contentDir,
|
|
71
|
+
opts.imageMetadata?.manifest,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const imageMetadataConfig = opts.imageMetadata;
|
|
75
|
+
const imageMetadataEnabled = imageMetadataConfig?.enabled === true;
|
|
76
|
+
const exiftoolBinary = imageMetadataConfig?.exiftool ?? "exiftool";
|
|
77
|
+
let exiftoolReady = false;
|
|
78
|
+
if (imageMetadataEnabled) {
|
|
79
|
+
if (exiftoolAvailable(exiftoolBinary)) {
|
|
80
|
+
exiftoolReady = true;
|
|
81
|
+
} else {
|
|
82
|
+
warn(
|
|
83
|
+
`image_metadata.enabled but ${exiftoolBinary} not found on PATH; static images copied without XMP`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const c2paConfig = opts.c2pa;
|
|
89
|
+
const skipC2pa = opts.skipC2pa === true;
|
|
90
|
+
let credentials: C2paCredentials | null = null;
|
|
91
|
+
let c2paBinary = c2paConfig?.binary ?? "c2patool";
|
|
92
|
+
|
|
93
|
+
if (c2paConfig?.enabled === true && !skipC2pa) {
|
|
94
|
+
credentials = resolveC2paCredentials(c2paConfig);
|
|
95
|
+
if (!credentials) {
|
|
96
|
+
warn(
|
|
97
|
+
"c2pa.enabled but signing credentials missing (set certificate_path/private_key_path or SORANE_C2PA_CERT/SORANE_C2PA_KEY); static images copied unsigned",
|
|
98
|
+
);
|
|
99
|
+
} else if (!c2patoolAvailable(c2paBinary)) {
|
|
100
|
+
warn(`c2pa.enabled but ${c2paBinary} not found on PATH; static images copied unsigned`);
|
|
101
|
+
credentials = null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let rasterSigned = 0;
|
|
106
|
+
let rasterCopied = 0;
|
|
107
|
+
let metadataEmbedded = 0;
|
|
108
|
+
let inlineImagesCopied = 0;
|
|
109
|
+
|
|
110
|
+
async function tagRaster(outAbs: string, hints: Parameters<typeof lookupAssetProvenance>[1]) {
|
|
111
|
+
const entry = lookupAssetProvenance(provenance, hints);
|
|
112
|
+
const relForLog =
|
|
113
|
+
typeof hints === "string"
|
|
114
|
+
? hints
|
|
115
|
+
: (hints.publicPath ?? hints.staticRel ?? hints.markdownPath ?? "image");
|
|
116
|
+
|
|
117
|
+
if (exiftoolReady && isImageMetadataPath(outAbs) && hasImageMetadataFields(entry)) {
|
|
118
|
+
const embedded = embedIptcXmp(outAbs, entry!, { binary: exiftoolBinary });
|
|
119
|
+
if (embedded.ok) {
|
|
120
|
+
metadataEmbedded += 1;
|
|
121
|
+
log(`iptc-xmp: embedded metadata in ${relForLog}`);
|
|
122
|
+
} else {
|
|
123
|
+
warn(`iptc-xmp: failed to embed ${relForLog}: ${embedded.message ?? "unknown error"}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const relPath = typeof hints === "string" ? hints : hints.staticRel;
|
|
128
|
+
if (!relPath || !isC2paRasterPath(relPath)) return;
|
|
129
|
+
rasterCopied += 1;
|
|
130
|
+
if (!credentials) return;
|
|
131
|
+
|
|
132
|
+
const createIntent = resolveC2paCreateIntent(entry);
|
|
133
|
+
const signed = signRasterWithC2pa(
|
|
134
|
+
existsSync(outAbs) ? outAbs : join(opts.staticSrc, relPath),
|
|
135
|
+
outAbs,
|
|
136
|
+
{
|
|
137
|
+
binary: c2paBinary,
|
|
138
|
+
embed: c2paConfig?.embed !== false,
|
|
139
|
+
createIntent,
|
|
140
|
+
credentials,
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
if (signed.ok) {
|
|
144
|
+
rasterSigned += 1;
|
|
145
|
+
log(`c2pa: signed ${relForLog} (${createIntent})`);
|
|
146
|
+
} else {
|
|
147
|
+
warn(`c2pa: failed to sign ${relForLog}: ${signed.message ?? "unknown error"}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const ref of opts.inlineImages ?? []) {
|
|
152
|
+
if (ref.kind !== "content") continue;
|
|
153
|
+
const outAbs = join(opts.outDir, ref.outRel);
|
|
154
|
+
mkdirSync(dirname(outAbs), { recursive: true });
|
|
155
|
+
copyFileSync(ref.srcAbs, outAbs);
|
|
156
|
+
inlineImagesCopied += 1;
|
|
157
|
+
await tagRaster(outAbs, {
|
|
158
|
+
staticRel: ref.outRel,
|
|
159
|
+
markdownPath: ref.markdownPath,
|
|
160
|
+
publicPath: ref.publicPath,
|
|
161
|
+
contentRel: ref.publicPath,
|
|
162
|
+
sourceMdRel: ref.sourceMdRel,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!existsSync(opts.staticSrc)) {
|
|
167
|
+
if (metadataEmbedded > 0) log(`iptc-xmp: ${metadataEmbedded} image(s) tagged`);
|
|
168
|
+
if (rasterSigned > 0) log(`c2pa: ${rasterSigned} raster asset(s) signed`);
|
|
169
|
+
return { rasterSigned, rasterCopied, metadataEmbedded, inlineImagesCopied };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const inlineHintsBySrc = new Map<string, MarkdownImageRef>();
|
|
173
|
+
for (const ref of opts.inlineImages ?? []) {
|
|
174
|
+
if (!inlineHintsBySrc.has(ref.srcAbs)) inlineHintsBySrc.set(ref.srcAbs, ref);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const abs of walkFiles(opts.staticSrc)) {
|
|
178
|
+
const rel = relative(opts.staticSrc, abs).replace(/\\/g, "/");
|
|
179
|
+
const outPath = join(outRoot, rel);
|
|
180
|
+
const inline = inlineHintsBySrc.get(abs);
|
|
181
|
+
await tagRaster(outPath, {
|
|
182
|
+
staticRel: rel,
|
|
183
|
+
publicPath: `${opts.staticDirName}/${rel}`,
|
|
184
|
+
markdownPath: inline?.markdownPath,
|
|
185
|
+
sourceMdRel: inline?.sourceMdRel,
|
|
186
|
+
contentRel: inline?.publicPath,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (metadataEmbedded > 0) {
|
|
191
|
+
log(`iptc-xmp: ${metadataEmbedded} image(s) tagged`);
|
|
192
|
+
}
|
|
193
|
+
if (rasterSigned > 0) {
|
|
194
|
+
log(`c2pa: ${rasterSigned} raster asset(s) signed`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { rasterSigned, rasterCopied, metadataEmbedded, inlineImagesCopied };
|
|
198
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
/** サイト cwd または親ディレクトリからテーマ資産サブディレクトリを探す。 */
|
|
5
|
+
export function resolveThemeAssetDir(cwd: string, subdir: string): string | null {
|
|
6
|
+
const rel = join("templates", "default", "assets", subdir);
|
|
7
|
+
let dir = resolve(cwd);
|
|
8
|
+
for (let depth = 0; depth < 6; depth++) {
|
|
9
|
+
const candidate = join(dir, rel);
|
|
10
|
+
if (existsSync(candidate)) return candidate;
|
|
11
|
+
const parent = dirname(dir);
|
|
12
|
+
if (parent === dir) break;
|
|
13
|
+
dir = parent;
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const FENCE_OPEN_RE = /^(```+|~~~+)/;
|
|
2
|
+
const HEADING_RE = /^(#{1,6})\s+/;
|
|
3
|
+
|
|
4
|
+
/** Markdown 本文の見出し階層を検査する(warning のみ)。 */
|
|
5
|
+
export function validateHeadingWarnings(body: string): readonly string[] {
|
|
6
|
+
const warnings: string[] = [];
|
|
7
|
+
const lines = body.split(/\r?\n/);
|
|
8
|
+
let inFence = false;
|
|
9
|
+
let fenceMarker = "";
|
|
10
|
+
let prevLevel = 0;
|
|
11
|
+
|
|
12
|
+
for (let i = 0; i < lines.length; i++) {
|
|
13
|
+
const line = lines[i]!;
|
|
14
|
+
const fence = FENCE_OPEN_RE.exec(line);
|
|
15
|
+
if (fence) {
|
|
16
|
+
const marker = fence[1]!;
|
|
17
|
+
if (!inFence) {
|
|
18
|
+
inFence = true;
|
|
19
|
+
fenceMarker = marker;
|
|
20
|
+
} else if (line.startsWith(fenceMarker)) {
|
|
21
|
+
inFence = false;
|
|
22
|
+
fenceMarker = "";
|
|
23
|
+
}
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (inFence) continue;
|
|
27
|
+
|
|
28
|
+
const hm = HEADING_RE.exec(line);
|
|
29
|
+
if (!hm) continue;
|
|
30
|
+
const level = hm[1]!.length;
|
|
31
|
+
const lineNo = i + 1;
|
|
32
|
+
|
|
33
|
+
if (level === 1) {
|
|
34
|
+
warnings.push(
|
|
35
|
+
`heading: h1 in body (line ${lineNo}); page title is already rendered as h1`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (prevLevel === 0 && level >= 3) {
|
|
39
|
+
warnings.push(
|
|
40
|
+
`heading: first heading in body is h${level} (line ${lineNo}); prefer starting with h2`,
|
|
41
|
+
);
|
|
42
|
+
} else if (prevLevel > 0 && level > prevLevel + 1) {
|
|
43
|
+
warnings.push(
|
|
44
|
+
`heading: skip from h${prevLevel} to h${level} (line ${lineNo})`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
prevLevel = level;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return warnings;
|
|
51
|
+
}
|