gutterpress 0.10.9-beta.1 → 0.10.9
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 +5 -3
- package/dist/api/index.js +1 -1
- package/dist/{audit-2x958vnj.js → audit-fq7s1gsz.js} +3 -3
- package/dist/{build-dqv3505a.js → build-dyat3fqd.js} +3 -3
- package/dist/checks/source/dangling-links.d.ts +18 -0
- package/dist/checks/source/index.d.ts +1 -0
- package/dist/checks/source/local-ref-parser.d.ts +13 -0
- package/dist/{cli-vhdvkqdd.js → cli-8pg731p5.js} +519 -428
- package/dist/{cli-0tgbgwnj.js → cli-rh05byck.js} +1 -1
- package/dist/{cli-fwh6rkev.js → cli-ypfvnhag.js} +1 -1
- package/dist/cli.js +14 -14
- package/dist/{doctor-55r0ghtc.js → doctor-yr85n080.js} +2 -2
- package/dist/{ext-04n7hydk.js → ext-jrg8c48s.js} +3 -3
- package/dist/{index-erwh3bww.js → index-mey5vraz.js} +508 -417
- package/dist/index.js +1 -1
- package/dist/lib/build-staging.d.ts +36 -0
- package/dist/lib/missing-asset-placeholder.d.ts +11 -0
- package/dist/{lint-c7a4j3ww.js → lint-enasqbqx.js} +3 -3
- package/dist/{new-pw3eez61.js → new-r9c99wsn.js} +3 -3
- package/dist/{preflight-77zyws1q.js → preflight-shk7vqbs.js} +3 -3
- package/dist/{preview-x7qqhmxf.js → preview-52918mxk.js} +3 -3
- package/dist/{publish-ms6abf6g.js → publish-8s60vhx7.js} +3 -3
- package/dist/{validate-vw59jcq7.js → validate-awccx32r.js} +3 -3
- package/package.json +1 -1
|
@@ -13,12 +13,12 @@ import {
|
|
|
13
13
|
stampCreator,
|
|
14
14
|
stripAnnotations,
|
|
15
15
|
warn
|
|
16
|
-
} from "./cli-
|
|
16
|
+
} from "./cli-ypfvnhag.js";
|
|
17
17
|
import {
|
|
18
18
|
DEBOUNCE,
|
|
19
19
|
UsageError,
|
|
20
20
|
resolvePort
|
|
21
|
-
} from "./cli-
|
|
21
|
+
} from "./cli-rh05byck.js";
|
|
22
22
|
import {
|
|
23
23
|
MARGIN_BOX_IGNORED_PROPERTIES,
|
|
24
24
|
RENDER_TIMEOUT_MS,
|
|
@@ -1247,26 +1247,54 @@ function targetsPageWrapper(selector) {
|
|
|
1247
1247
|
return false;
|
|
1248
1248
|
return !/\bgp-(bleed|pin)\b/.test(selector);
|
|
1249
1249
|
}
|
|
1250
|
+
var cssWideResetKeywords = new Set(["initial", "unset", "revert", "revert-layer"]);
|
|
1251
|
+
var inertValues = {
|
|
1252
|
+
"z-index": new Set(["auto"]),
|
|
1253
|
+
opacity: new Set(["1", "100%"]),
|
|
1254
|
+
"mix-blend-mode": new Set(["normal"]),
|
|
1255
|
+
"background-blend-mode": new Set(["normal"]),
|
|
1256
|
+
filter: new Set(["none"]),
|
|
1257
|
+
"backdrop-filter": new Set(["none"]),
|
|
1258
|
+
transform: new Set(["none"]),
|
|
1259
|
+
rotate: new Set(["none"]),
|
|
1260
|
+
translate: new Set(["none"]),
|
|
1261
|
+
scale: new Set(["none"]),
|
|
1262
|
+
perspective: new Set(["none"]),
|
|
1263
|
+
"box-shadow": new Set(["none"]),
|
|
1264
|
+
outline: new Set(["none"]),
|
|
1265
|
+
"outline-style": new Set(["none"]),
|
|
1266
|
+
"will-change": new Set(["auto", "none"]),
|
|
1267
|
+
"clip-path": new Set(["none"]),
|
|
1268
|
+
transition: new Set(["none"]),
|
|
1269
|
+
animation: new Set(["none"]),
|
|
1270
|
+
"animation-name": new Set(["none"]),
|
|
1271
|
+
overflow: new Set(["visible"]),
|
|
1272
|
+
"overflow-x": new Set(["visible"]),
|
|
1273
|
+
"overflow-y": new Set(["visible"])
|
|
1274
|
+
};
|
|
1275
|
+
function isInertValue(prop, value) {
|
|
1276
|
+
const v = value.trim().toLowerCase();
|
|
1277
|
+
return cssWideResetKeywords.has(v) || (inertValues[prop]?.has(v) ?? false);
|
|
1278
|
+
}
|
|
1250
1279
|
function createsStackingContext(decl) {
|
|
1251
1280
|
const p = decl.prop.toLowerCase();
|
|
1252
1281
|
const v = decl.value.trim().toLowerCase();
|
|
1282
|
+
if (isInertValue(p, v))
|
|
1283
|
+
return false;
|
|
1253
1284
|
switch (p) {
|
|
1254
|
-
case "z-index":
|
|
1255
|
-
return v !== "auto" && v !== "initial" && v !== "unset";
|
|
1256
1285
|
case "isolation":
|
|
1257
1286
|
return v === "isolate";
|
|
1287
|
+
case "contain":
|
|
1288
|
+
return /\b(paint|layout|strict|content)\b/.test(v);
|
|
1289
|
+
case "z-index":
|
|
1258
1290
|
case "opacity":
|
|
1259
|
-
return v !== "1" && v !== "100%" && v !== "initial" && v !== "unset";
|
|
1260
1291
|
case "mix-blend-mode":
|
|
1261
|
-
return v !== "normal" && v !== "initial" && v !== "unset";
|
|
1262
1292
|
case "filter":
|
|
1263
1293
|
case "backdrop-filter":
|
|
1264
1294
|
case "transform":
|
|
1265
1295
|
case "perspective":
|
|
1266
1296
|
case "will-change":
|
|
1267
|
-
return
|
|
1268
|
-
case "contain":
|
|
1269
|
-
return /\b(paint|layout|strict|content)\b/.test(v);
|
|
1297
|
+
return true;
|
|
1270
1298
|
default:
|
|
1271
1299
|
return false;
|
|
1272
1300
|
}
|
|
@@ -1276,7 +1304,7 @@ function clipsDescendants(decl) {
|
|
|
1276
1304
|
if (p !== "overflow" && p !== "overflow-x" && p !== "overflow-y")
|
|
1277
1305
|
return false;
|
|
1278
1306
|
const v = decl.value.trim().toLowerCase();
|
|
1279
|
-
return v !== "" &&
|
|
1307
|
+
return v !== "" && !isInertValue(p, v);
|
|
1280
1308
|
}
|
|
1281
1309
|
function nodeLoc(node) {
|
|
1282
1310
|
return {
|
|
@@ -1317,6 +1345,8 @@ function checkCss(css, from) {
|
|
|
1317
1345
|
root.walkAtRules((at) => reportRemoteUrls(at.params || "", at));
|
|
1318
1346
|
root.walkDecls((decl) => {
|
|
1319
1347
|
const prop = decl.prop.toLowerCase();
|
|
1348
|
+
if (isInertValue(prop, decl.value))
|
|
1349
|
+
return;
|
|
1320
1350
|
if (marginBoxIgnoredProperties.has(prop) && isInPageMarginBox(decl)) {
|
|
1321
1351
|
warnings.push({
|
|
1322
1352
|
rule: ruleRiskyProps,
|
|
@@ -4169,7 +4199,8 @@ async function runLint(opts = {}) {
|
|
|
4169
4199
|
linted++;
|
|
4170
4200
|
const warnings = checkCss(css, file);
|
|
4171
4201
|
const errors = warnings.filter((w) => w.severity === "error");
|
|
4172
|
-
|
|
4202
|
+
const risky = warnings.filter((w) => w.rule === ruleRiskyProps);
|
|
4203
|
+
riskyCount += risky.length;
|
|
4173
4204
|
if (errors.length > 0) {
|
|
4174
4205
|
log.error(` ${file}`);
|
|
4175
4206
|
for (const w of errors) {
|
|
@@ -4177,6 +4208,12 @@ async function runLint(opts = {}) {
|
|
|
4177
4208
|
}
|
|
4178
4209
|
errorCount += errors.length;
|
|
4179
4210
|
}
|
|
4211
|
+
if (risky.length > 0) {
|
|
4212
|
+
log.warn(` ${file}`);
|
|
4213
|
+
for (const w of risky) {
|
|
4214
|
+
log.warn(` ${w.line}:${w.column} ${w.message} (${w.rule})`);
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
4180
4217
|
}
|
|
4181
4218
|
if (errorCount > 0) {
|
|
4182
4219
|
log.error("CSS lint errors found");
|
|
@@ -4184,7 +4221,6 @@ async function runLint(opts = {}) {
|
|
|
4184
4221
|
}
|
|
4185
4222
|
if (riskyCount > 0) {
|
|
4186
4223
|
log.warn(`${riskyCount} risky print properties found (may cause rasterization)`);
|
|
4187
|
-
log.warn("The validator will check for actual rasterized pages after PDF generation.");
|
|
4188
4224
|
} else {
|
|
4189
4225
|
log.success("CSS lint passed");
|
|
4190
4226
|
}
|
|
@@ -5752,7 +5788,6 @@ registerCheck(check19);
|
|
|
5752
5788
|
|
|
5753
5789
|
// src/checks/source/local-refs.ts
|
|
5754
5790
|
import { existsSync as existsSync8 } from "node:fs";
|
|
5755
|
-
import { readFile as readFile13 } from "node:fs/promises";
|
|
5756
5791
|
import { dirname as dirname3, resolve as resolve6 } from "node:path";
|
|
5757
5792
|
|
|
5758
5793
|
// src/lib/asset-inline.ts
|
|
@@ -6060,6 +6095,7 @@ async function inlineShapeUrls(html, baseDir) {
|
|
|
6060
6095
|
}
|
|
6061
6096
|
|
|
6062
6097
|
// src/checks/source/local-ref-parser.ts
|
|
6098
|
+
import { readFile as readFile13 } from "node:fs/promises";
|
|
6063
6099
|
function newlineOffsets(content) {
|
|
6064
6100
|
const offsets = [];
|
|
6065
6101
|
for (let i = 0;i < content.length; i++)
|
|
@@ -6104,7 +6140,7 @@ function createRenderedLocalRefCollector(customPlugins) {
|
|
|
6104
6140
|
if (!kind)
|
|
6105
6141
|
continue;
|
|
6106
6142
|
const ref = child.attrGet(kind === "image" ? "src" : "href");
|
|
6107
|
-
if (
|
|
6143
|
+
if (ref === null)
|
|
6108
6144
|
continue;
|
|
6109
6145
|
const offset = tokenOffsets.get(child);
|
|
6110
6146
|
refs.push({
|
|
@@ -6117,6 +6153,26 @@ function createRenderedLocalRefCollector(customPlugins) {
|
|
|
6117
6153
|
return refs;
|
|
6118
6154
|
};
|
|
6119
6155
|
}
|
|
6156
|
+
async function* renderedLocalRefs(ctx, checkId, results) {
|
|
6157
|
+
const files = (ctx.markdownFiles ?? []).slice().sort();
|
|
6158
|
+
if (files.length === 0)
|
|
6159
|
+
return;
|
|
6160
|
+
const plugins = await loadPlugins(ctx.config.extensions, ctx.inputDir, (ref, error2) => {
|
|
6161
|
+
results.push(inspectionFailed(checkId, `Plugin "${ref}" could not be loaded, so local references it defines were not checked: ${error2.message}`));
|
|
6162
|
+
});
|
|
6163
|
+
const collectRenderedLocalRefs = createRenderedLocalRefCollector(plugins);
|
|
6164
|
+
for (const file of files) {
|
|
6165
|
+
let refs;
|
|
6166
|
+
try {
|
|
6167
|
+
refs = collectRenderedLocalRefs(await readFile13(file, "utf8"));
|
|
6168
|
+
} catch {
|
|
6169
|
+
results.push(inspectionFailed(checkId, `Could not read source file: ${file}`, { file }));
|
|
6170
|
+
continue;
|
|
6171
|
+
}
|
|
6172
|
+
for (const ref of refs)
|
|
6173
|
+
yield { ...ref, file };
|
|
6174
|
+
}
|
|
6175
|
+
}
|
|
6120
6176
|
|
|
6121
6177
|
// src/checks/source/local-refs.ts
|
|
6122
6178
|
var check20 = {
|
|
@@ -6126,45 +6182,29 @@ var check20 = {
|
|
|
6126
6182
|
category: "source",
|
|
6127
6183
|
phase: "pre-build",
|
|
6128
6184
|
async run(ctx) {
|
|
6129
|
-
const files = (ctx.markdownFiles ?? []).slice().sort();
|
|
6130
|
-
if (files.length === 0)
|
|
6131
|
-
return [];
|
|
6132
6185
|
const results = [];
|
|
6133
|
-
const
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
const escape = kind === "image" ? proseImageRefError(ref, ctx.inputDir) : null;
|
|
6144
|
-
if (escape) {
|
|
6145
|
-
results.push(finding(check20.id, {
|
|
6146
|
-
severity: "error",
|
|
6147
|
-
message: escape,
|
|
6148
|
-
file,
|
|
6149
|
-
line
|
|
6150
|
-
}));
|
|
6151
|
-
continue;
|
|
6152
|
-
}
|
|
6153
|
-
if (localRefExists(ref, kind, file, ctx.inputDir))
|
|
6154
|
-
continue;
|
|
6155
|
-
results.push(finding(check20.id, {
|
|
6156
|
-
severity: kind === "image" ? "warning" : "error",
|
|
6157
|
-
code: kind === "image" ? "missing-image-placeholder" : undefined,
|
|
6158
|
-
message: kind === "image" ? `Local image not found; build will substitute a magenta placeholder: ${ref}` : `Local reference not found: ${ref}`,
|
|
6159
|
-
file,
|
|
6160
|
-
line
|
|
6161
|
-
}));
|
|
6162
|
-
}
|
|
6163
|
-
} catch {
|
|
6164
|
-
results.push(inspectionFailed(check20.id, `Could not read source file: ${file}`, {
|
|
6165
|
-
file
|
|
6186
|
+
for await (const { ref, kind, line, file } of renderedLocalRefs(ctx, check20.id, results)) {
|
|
6187
|
+
if (isNonFilesystemRef(ref))
|
|
6188
|
+
continue;
|
|
6189
|
+
const escape = kind === "image" ? proseImageRefError(ref, ctx.inputDir) : null;
|
|
6190
|
+
if (escape) {
|
|
6191
|
+
results.push(finding(check20.id, {
|
|
6192
|
+
severity: "error",
|
|
6193
|
+
message: escape,
|
|
6194
|
+
file,
|
|
6195
|
+
line
|
|
6166
6196
|
}));
|
|
6197
|
+
continue;
|
|
6167
6198
|
}
|
|
6199
|
+
if (localRefExists(ref, kind, file, ctx.inputDir))
|
|
6200
|
+
continue;
|
|
6201
|
+
results.push(finding(check20.id, {
|
|
6202
|
+
severity: kind === "image" ? "warning" : "error",
|
|
6203
|
+
code: kind === "image" ? "missing-image-placeholder" : undefined,
|
|
6204
|
+
message: kind === "image" ? `Local image not found; build will substitute a magenta placeholder: ${ref}` : `Local reference not found: ${ref}`,
|
|
6205
|
+
file,
|
|
6206
|
+
line
|
|
6207
|
+
}));
|
|
6168
6208
|
}
|
|
6169
6209
|
return results;
|
|
6170
6210
|
}
|
|
@@ -6185,71 +6225,360 @@ function localRefExists(ref, kind, sourceFile, projectRoot) {
|
|
|
6185
6225
|
}
|
|
6186
6226
|
registerCheck(check20);
|
|
6187
6227
|
|
|
6188
|
-
// src/
|
|
6189
|
-
import
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6228
|
+
// src/lib/build-staging.ts
|
|
6229
|
+
import path5 from "node:path";
|
|
6230
|
+
import os from "node:os";
|
|
6231
|
+
import fsp from "node:fs/promises";
|
|
6232
|
+
|
|
6233
|
+
// src/lib/missing-asset-placeholder.ts
|
|
6234
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
6235
|
+
import { deflateSync } from "node:zlib";
|
|
6236
|
+
function placeholderOutputPath(missingOutputPath) {
|
|
6237
|
+
const hash = createHash4("sha256").update(missingOutputPath).digest("hex").slice(0, 16);
|
|
6238
|
+
return `assets/gutterpress-missing/${hash}.png`;
|
|
6239
|
+
}
|
|
6240
|
+
function decodeHtmlAttribute(value) {
|
|
6241
|
+
return value.replace(/"/g, '"').replace(/'|'/g, "'").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
6242
|
+
}
|
|
6243
|
+
function rewriteMissingImageReferences(html, replacements) {
|
|
6244
|
+
if (replacements.size === 0)
|
|
6245
|
+
return html;
|
|
6246
|
+
const replacementFor = (raw) => replacements.get(raw) ?? replacements.get(decodeHtmlAttribute(raw));
|
|
6247
|
+
const cssUrl = /url\(\s*(?:"([^"]*)"|'([^']*)'|"((?:(?!").)*)"|([^'"()\s]*))\s*\)/giy;
|
|
6248
|
+
const rewriteCssUrls = (css) => {
|
|
6249
|
+
let out = "";
|
|
6250
|
+
let copiedThrough = 0;
|
|
6251
|
+
let index = 0;
|
|
6252
|
+
while (index < css.length) {
|
|
6253
|
+
if (css.startsWith("/*", index)) {
|
|
6254
|
+
const end = css.indexOf("*/", index + 2);
|
|
6255
|
+
index = end < 0 ? css.length : end + 2;
|
|
6256
|
+
continue;
|
|
6257
|
+
}
|
|
6258
|
+
const char = css[index];
|
|
6259
|
+
if (char === '"' || char === "'") {
|
|
6260
|
+
const quote = char;
|
|
6261
|
+
index++;
|
|
6262
|
+
while (index < css.length) {
|
|
6263
|
+
if (css[index] === "\\") {
|
|
6264
|
+
index += 2;
|
|
6211
6265
|
continue;
|
|
6212
6266
|
}
|
|
6213
|
-
|
|
6267
|
+
const current = css[index++];
|
|
6268
|
+
if (current === quote)
|
|
6269
|
+
break;
|
|
6270
|
+
}
|
|
6271
|
+
continue;
|
|
6272
|
+
}
|
|
6273
|
+
const previous = index > 0 ? css[index - 1] : "";
|
|
6274
|
+
if (!/[a-z0-9_-]/i.test(previous) && css.slice(index, index + 3).toLowerCase() === "url") {
|
|
6275
|
+
cssUrl.lastIndex = index;
|
|
6276
|
+
const match = cssUrl.exec(css);
|
|
6277
|
+
if (match) {
|
|
6278
|
+
const raw = match[1] ?? match[2] ?? match[3] ?? match[4] ?? "";
|
|
6279
|
+
const replacement = replacementFor(raw);
|
|
6280
|
+
if (replacement) {
|
|
6281
|
+
out += css.slice(copiedThrough, index);
|
|
6282
|
+
out += match[3] !== undefined ? `url("${replacement}")` : `url("${replacement}")`;
|
|
6283
|
+
index = cssUrl.lastIndex;
|
|
6284
|
+
copiedThrough = index;
|
|
6214
6285
|
continue;
|
|
6215
|
-
for (const alt of extractImageAlts(line)) {
|
|
6216
|
-
if (alt.trim())
|
|
6217
|
-
continue;
|
|
6218
|
-
results.push({
|
|
6219
|
-
checkId: check21.id,
|
|
6220
|
-
severity: "warning",
|
|
6221
|
-
message: "Image is missing alt text",
|
|
6222
|
-
file,
|
|
6223
|
-
line: i + 1
|
|
6224
|
-
});
|
|
6225
6286
|
}
|
|
6226
6287
|
}
|
|
6227
|
-
}
|
|
6288
|
+
}
|
|
6289
|
+
index++;
|
|
6228
6290
|
}
|
|
6229
|
-
return
|
|
6291
|
+
return copiedThrough === 0 ? css : out + css.slice(copiedThrough);
|
|
6292
|
+
};
|
|
6293
|
+
const rewriteTag = (tag) => {
|
|
6294
|
+
let out = tag;
|
|
6295
|
+
if (/^<img\b/i.test(tag)) {
|
|
6296
|
+
out = out.replace(/(\s+src\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi, (whole, prefix, double, single, bare) => {
|
|
6297
|
+
const raw = double ?? single ?? bare ?? "";
|
|
6298
|
+
const replacement = replacementFor(raw);
|
|
6299
|
+
if (!replacement)
|
|
6300
|
+
return whole;
|
|
6301
|
+
if (double !== undefined)
|
|
6302
|
+
return `${prefix}"${replacement}"`;
|
|
6303
|
+
if (single !== undefined)
|
|
6304
|
+
return `${prefix}'${replacement}'`;
|
|
6305
|
+
return `${prefix}${replacement}`;
|
|
6306
|
+
});
|
|
6307
|
+
}
|
|
6308
|
+
if (/^<(?:img|source)\b/i.test(tag)) {
|
|
6309
|
+
out = out.replace(/(\s+srcset\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi, (whole, prefix, double, single, bare) => {
|
|
6310
|
+
const value = double ?? single ?? bare ?? "";
|
|
6311
|
+
let rewritten = "";
|
|
6312
|
+
let copiedThrough = 0;
|
|
6313
|
+
let changed = false;
|
|
6314
|
+
for (const candidate of parseSrcsetUrlCandidates(value)) {
|
|
6315
|
+
const replacement = replacementFor(candidate.url);
|
|
6316
|
+
if (!replacement)
|
|
6317
|
+
continue;
|
|
6318
|
+
rewritten += value.slice(copiedThrough, candidate.start) + replacement;
|
|
6319
|
+
copiedThrough = candidate.end;
|
|
6320
|
+
changed = true;
|
|
6321
|
+
}
|
|
6322
|
+
if (!changed)
|
|
6323
|
+
return whole;
|
|
6324
|
+
rewritten += value.slice(copiedThrough);
|
|
6325
|
+
if (double !== undefined)
|
|
6326
|
+
return `${prefix}"${rewritten}"`;
|
|
6327
|
+
if (single !== undefined)
|
|
6328
|
+
return `${prefix}'${rewritten}'`;
|
|
6329
|
+
return `${prefix}${rewritten}`;
|
|
6330
|
+
});
|
|
6331
|
+
}
|
|
6332
|
+
out = out.replace(/(\s+style\s*=\s*)(["'])(.*?)\2/gi, (whole, prefix, quote, value) => {
|
|
6333
|
+
const rewritten = rewriteCssUrls(value);
|
|
6334
|
+
return rewritten === value ? whole : `${prefix}${quote}${rewritten}${quote}`;
|
|
6335
|
+
});
|
|
6336
|
+
return out;
|
|
6337
|
+
};
|
|
6338
|
+
return rewriteActiveHtml(html, rewriteTag, (region, element) => {
|
|
6339
|
+
if (element !== "style")
|
|
6340
|
+
return region;
|
|
6341
|
+
const style = /^(<style\b[^>]*>)([\s\S]*)(<\/style\s*>)$/i.exec(region);
|
|
6342
|
+
return style ? `${rewriteTag(style[1])}${rewriteCssUrls(style[2])}${style[3]}` : region;
|
|
6343
|
+
});
|
|
6344
|
+
}
|
|
6345
|
+
function rewriteActiveHtml(html, rewriteTag, rewriteProtected = (region) => region) {
|
|
6346
|
+
const rewriteTags = (active) => active.replace(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g, rewriteTag);
|
|
6347
|
+
const protectedRegion = /<!--[\s\S]*?-->|<(script|style|pre|code|textarea)\b[^>]*>[\s\S]*?<\/\1\s*>/gi;
|
|
6348
|
+
let out = "";
|
|
6349
|
+
let last = 0;
|
|
6350
|
+
for (const match of html.matchAll(protectedRegion)) {
|
|
6351
|
+
out += rewriteTags(html.slice(last, match.index));
|
|
6352
|
+
out += rewriteProtected(match[0], match[1]?.toLowerCase());
|
|
6353
|
+
last = (match.index ?? 0) + match[0].length;
|
|
6230
6354
|
}
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6355
|
+
return out + rewriteTags(html.slice(last));
|
|
6356
|
+
}
|
|
6357
|
+
function crc32(buf) {
|
|
6358
|
+
let c = ~0;
|
|
6359
|
+
for (let i = 0;i < buf.length; i++) {
|
|
6360
|
+
c ^= buf[i];
|
|
6361
|
+
for (let k = 0;k < 8; k++)
|
|
6362
|
+
c = c >>> 1 ^ 3988292384 & -(c & 1);
|
|
6238
6363
|
}
|
|
6239
|
-
|
|
6240
|
-
|
|
6364
|
+
return ~c >>> 0;
|
|
6365
|
+
}
|
|
6366
|
+
function chunk(type, data) {
|
|
6367
|
+
const out = new Uint8Array(data.length + 12);
|
|
6368
|
+
const view = new DataView(out.buffer);
|
|
6369
|
+
view.setUint32(0, data.length);
|
|
6370
|
+
for (let i = 0;i < 4; i++)
|
|
6371
|
+
out[4 + i] = type.charCodeAt(i);
|
|
6372
|
+
out.set(data, 8);
|
|
6373
|
+
view.setUint32(data.length + 8, crc32(out.subarray(4, data.length + 8)));
|
|
6374
|
+
return out;
|
|
6375
|
+
}
|
|
6376
|
+
function placeholderPng(width = 640, height = 480, cell = 32) {
|
|
6377
|
+
const A = [217, 70, 239];
|
|
6378
|
+
const B = [26, 26, 26];
|
|
6379
|
+
const raw = new Uint8Array(height * (1 + width * 3));
|
|
6380
|
+
let p = 0;
|
|
6381
|
+
for (let y = 0;y < height; y++) {
|
|
6382
|
+
raw[p++] = 0;
|
|
6383
|
+
for (let x = 0;x < width; x++) {
|
|
6384
|
+
const c = ((x / cell | 0) + (y / cell | 0)) % 2 === 0 ? A : B;
|
|
6385
|
+
raw[p++] = c[0];
|
|
6386
|
+
raw[p++] = c[1];
|
|
6387
|
+
raw[p++] = c[2];
|
|
6388
|
+
}
|
|
6241
6389
|
}
|
|
6242
|
-
|
|
6390
|
+
const ihdr = new Uint8Array(13);
|
|
6391
|
+
const hv = new DataView(ihdr.buffer);
|
|
6392
|
+
hv.setUint32(0, width);
|
|
6393
|
+
hv.setUint32(4, height);
|
|
6394
|
+
ihdr[8] = 8;
|
|
6395
|
+
ihdr[9] = 2;
|
|
6396
|
+
const parts = [
|
|
6397
|
+
new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
|
|
6398
|
+
chunk("IHDR", ihdr),
|
|
6399
|
+
chunk("IDAT", new Uint8Array(deflateSync(raw))),
|
|
6400
|
+
chunk("IEND", new Uint8Array(0))
|
|
6401
|
+
];
|
|
6402
|
+
const total = parts.reduce((n, x) => n + x.length, 0);
|
|
6403
|
+
const png = new Uint8Array(total);
|
|
6404
|
+
let o = 0;
|
|
6405
|
+
for (const part of parts) {
|
|
6406
|
+
png.set(part, o);
|
|
6407
|
+
o += part.length;
|
|
6408
|
+
}
|
|
6409
|
+
return png;
|
|
6243
6410
|
}
|
|
6244
|
-
registerCheck(check21);
|
|
6245
6411
|
|
|
6246
|
-
// src/
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6412
|
+
// src/lib/build-staging.ts
|
|
6413
|
+
async function shipViewerHtml(htmlFile, outDir) {
|
|
6414
|
+
await fsp.mkdir(path5.join(outDir, "engine"), { recursive: true });
|
|
6415
|
+
await fsp.copyFile(await getAssetPath("engine/gutterpress-viewer.js"), path5.join(outDir, "engine/gutterpress-viewer.js"));
|
|
6416
|
+
const tag = ` <script src="engine/gutterpress-viewer.js"></script>
|
|
6417
|
+
`;
|
|
6418
|
+
const html = await fsp.readFile(htmlFile, "utf-8");
|
|
6419
|
+
await fsp.writeFile(htmlFile, /<\/head>/i.test(html) ? html.replace(/<\/head>/i, tag + "</head>") : tag + html, "utf-8");
|
|
6420
|
+
}
|
|
6421
|
+
async function stageBookAssets(options) {
|
|
6422
|
+
const { renderDir, outDir, htmlFile, imageRefs, cssAssets, onPlan, dropRelativeLinks } = options;
|
|
6423
|
+
const { copies: imageCopies, errors, destinations } = await planImageCopies(renderDir, imageRefs);
|
|
6424
|
+
const copies = [...cssAssets, ...imageCopies];
|
|
6425
|
+
onPlan?.({ unresolved: errors, copyCount: copies.length });
|
|
6426
|
+
const missingPlaceholders = copies.length ? await copyReferencedAssets(copies, outDir) : new Map;
|
|
6427
|
+
const rendered = await fsp.readFile(htmlFile, "utf8");
|
|
6428
|
+
let staged = rendered;
|
|
6429
|
+
if (missingPlaceholders.size > 0) {
|
|
6430
|
+
const rewrites = new Map(missingPlaceholders);
|
|
6431
|
+
for (const [ref, dest] of destinations) {
|
|
6432
|
+
const placeholder = missingPlaceholders.get(dest);
|
|
6433
|
+
if (placeholder)
|
|
6434
|
+
rewrites.set(ref, placeholder);
|
|
6435
|
+
}
|
|
6436
|
+
staged = rewriteMissingImageReferences(staged, rewrites);
|
|
6437
|
+
}
|
|
6438
|
+
if (dropRelativeLinks)
|
|
6439
|
+
staged = dropRelativeLinkHrefs(staged);
|
|
6440
|
+
if (staged.includes("--gp-shape:"))
|
|
6441
|
+
staged = await inlineShapeUrls(staged, outDir);
|
|
6442
|
+
if (staged !== rendered)
|
|
6443
|
+
await fsp.writeFile(htmlFile, staged, "utf8");
|
|
6444
|
+
return { missing: [...missingPlaceholders.keys()].sort() };
|
|
6445
|
+
}
|
|
6446
|
+
async function copyReferencedAssets(copies, outDir) {
|
|
6447
|
+
const dirs = new Set(copies.map((c) => path5.dirname(path5.resolve(outDir, c.to))));
|
|
6448
|
+
await Promise.all([...dirs].map((d) => fsp.mkdir(d, { recursive: true })));
|
|
6449
|
+
const missing = new Map;
|
|
6450
|
+
await Promise.all(copies.map(async (c) => {
|
|
6451
|
+
const dest = path5.resolve(outDir, c.to);
|
|
6452
|
+
try {
|
|
6453
|
+
await fsp.copyFile(c.from, dest);
|
|
6454
|
+
} catch (err) {
|
|
6455
|
+
if (err?.code === "ENOENT") {
|
|
6456
|
+
const placeholder = placeholderOutputPath(c.to);
|
|
6457
|
+
const placeholderDest = path5.resolve(outDir, placeholder);
|
|
6458
|
+
await fsp.mkdir(path5.dirname(placeholderDest), { recursive: true });
|
|
6459
|
+
await fsp.writeFile(placeholderDest, placeholderPng());
|
|
6460
|
+
missing.set(c.to, placeholder);
|
|
6461
|
+
return;
|
|
6462
|
+
}
|
|
6463
|
+
throw new BuildError(`Could not copy asset ${c.from} → ${c.to}: ` + (err instanceof Error ? err.message : String(err)), 1);
|
|
6464
|
+
}
|
|
6465
|
+
}));
|
|
6466
|
+
return missing;
|
|
6467
|
+
}
|
|
6468
|
+
function isPrintResolvableHref(href) {
|
|
6469
|
+
const value = href.trim();
|
|
6470
|
+
if (value.startsWith("#"))
|
|
6471
|
+
return true;
|
|
6472
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(value)?.[1]?.toLowerCase();
|
|
6473
|
+
return scheme !== undefined && scheme !== "file";
|
|
6474
|
+
}
|
|
6475
|
+
function dropRelativeLinkHrefs(html) {
|
|
6476
|
+
const hrefAttr = /\s+href\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i;
|
|
6477
|
+
return rewriteActiveHtml(html, (tag) => {
|
|
6478
|
+
if (!/^<a(?=[\s/>])/i.test(tag))
|
|
6479
|
+
return tag;
|
|
6480
|
+
const m = hrefAttr.exec(tag);
|
|
6481
|
+
if (!m)
|
|
6482
|
+
return tag;
|
|
6483
|
+
const href = decodeHtmlAttribute(m[1] ?? m[2] ?? m[3] ?? "");
|
|
6484
|
+
return isPrintResolvableHref(href) ? tag : tag.replace(hrefAttr, "");
|
|
6485
|
+
});
|
|
6486
|
+
}
|
|
6487
|
+
async function createStageRoot() {
|
|
6488
|
+
return fsp.mkdtemp(path5.join(os.tmpdir(), "gutterpress-stage-"));
|
|
6489
|
+
}
|
|
6490
|
+
|
|
6491
|
+
// src/checks/source/dangling-links.ts
|
|
6492
|
+
var check21 = {
|
|
6493
|
+
id: "source.links.dangling",
|
|
6494
|
+
name: "Dangling Links",
|
|
6495
|
+
description: "Flags relative links the printed book cannot open (their href is dropped at print time)",
|
|
6496
|
+
category: "source",
|
|
6497
|
+
phase: "pre-build",
|
|
6498
|
+
async run(ctx) {
|
|
6499
|
+
const results = [];
|
|
6500
|
+
for await (const { ref, kind, line, file } of renderedLocalRefs(ctx, check21.id, results)) {
|
|
6501
|
+
if (kind !== "link" || isPrintResolvableHref(ref))
|
|
6502
|
+
continue;
|
|
6503
|
+
results.push(finding(check21.id, {
|
|
6504
|
+
severity: "warning",
|
|
6505
|
+
code: "dangling-link",
|
|
6506
|
+
data: { ref },
|
|
6507
|
+
message: `Link "${ref}" cannot be opened from the printed book: a PDF has no files ` + `beside it and no address of its own, so a relative link (a chapter file of ` + `this same book included) leads nowhere. The link text stays but its href is ` + `dropped at print time. Link to a heading with #anchor, use an absolute URL, ` + `or write it as plain text.`,
|
|
6508
|
+
file,
|
|
6509
|
+
line
|
|
6510
|
+
}));
|
|
6511
|
+
}
|
|
6512
|
+
return results;
|
|
6513
|
+
}
|
|
6514
|
+
};
|
|
6515
|
+
registerCheck(check21);
|
|
6516
|
+
|
|
6517
|
+
// src/checks/source/accessibility-alt-text.ts
|
|
6518
|
+
import { readFile as readFile14 } from "node:fs/promises";
|
|
6519
|
+
var check22 = {
|
|
6520
|
+
id: "source.accessibility.alt-text",
|
|
6521
|
+
name: "Image Alt Text",
|
|
6522
|
+
description: "Checks markdown images include non-empty alt text",
|
|
6523
|
+
category: "source",
|
|
6524
|
+
phase: "pre-build",
|
|
6525
|
+
async run(ctx) {
|
|
6526
|
+
const files = (ctx.markdownFiles ?? []).slice().sort();
|
|
6527
|
+
if (files.length === 0)
|
|
6528
|
+
return [];
|
|
6529
|
+
const results = [];
|
|
6530
|
+
for (const file of files) {
|
|
6531
|
+
try {
|
|
6532
|
+
const content = await readFile14(file, "utf8");
|
|
6533
|
+
const lines = content.split(`
|
|
6534
|
+
`);
|
|
6535
|
+
let inFence = false;
|
|
6536
|
+
for (let i = 0;i < lines.length; i++) {
|
|
6537
|
+
const line = lines[i] ?? "";
|
|
6538
|
+
if (/^\s*(```|~~~)/.test(line)) {
|
|
6539
|
+
inFence = !inFence;
|
|
6540
|
+
continue;
|
|
6541
|
+
}
|
|
6542
|
+
if (inFence)
|
|
6543
|
+
continue;
|
|
6544
|
+
for (const alt of extractImageAlts(line)) {
|
|
6545
|
+
if (alt.trim())
|
|
6546
|
+
continue;
|
|
6547
|
+
results.push({
|
|
6548
|
+
checkId: check22.id,
|
|
6549
|
+
severity: "warning",
|
|
6550
|
+
message: "Image is missing alt text",
|
|
6551
|
+
file,
|
|
6552
|
+
line: i + 1
|
|
6553
|
+
});
|
|
6554
|
+
}
|
|
6555
|
+
}
|
|
6556
|
+
} catch {}
|
|
6557
|
+
}
|
|
6558
|
+
return results;
|
|
6559
|
+
}
|
|
6560
|
+
};
|
|
6561
|
+
function extractImageAlts(line) {
|
|
6562
|
+
const alts = [];
|
|
6563
|
+
const inlinePattern = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
|
6564
|
+
const referencePattern = /!\[([^\]]*)\]\[[^\]]*\]/g;
|
|
6565
|
+
for (const match of line.matchAll(inlinePattern)) {
|
|
6566
|
+
alts.push(match[1] ?? "");
|
|
6567
|
+
}
|
|
6568
|
+
for (const match of line.matchAll(referencePattern)) {
|
|
6569
|
+
alts.push(match[1] ?? "");
|
|
6570
|
+
}
|
|
6571
|
+
return alts;
|
|
6572
|
+
}
|
|
6573
|
+
registerCheck(check22);
|
|
6574
|
+
|
|
6575
|
+
// src/checks/source/accessibility-heading-order.ts
|
|
6576
|
+
import { readFile as readFile15 } from "node:fs/promises";
|
|
6577
|
+
var check23 = {
|
|
6578
|
+
id: "source.accessibility.heading-order",
|
|
6579
|
+
name: "Heading Order",
|
|
6580
|
+
description: "Checks markdown heading levels do not jump by more than one",
|
|
6581
|
+
category: "source",
|
|
6253
6582
|
phase: "pre-build",
|
|
6254
6583
|
async run(ctx) {
|
|
6255
6584
|
const files = (ctx.markdownFiles ?? []).slice().sort();
|
|
@@ -6277,7 +6606,7 @@ var check22 = {
|
|
|
6277
6606
|
const level = match[1].length;
|
|
6278
6607
|
if (prevLevel != null && level > prevLevel + 1) {
|
|
6279
6608
|
results.push({
|
|
6280
|
-
checkId:
|
|
6609
|
+
checkId: check23.id,
|
|
6281
6610
|
severity: "warning",
|
|
6282
6611
|
message: `Heading level jump from h${prevLevel} to h${level}`,
|
|
6283
6612
|
file,
|
|
@@ -6291,11 +6620,11 @@ var check22 = {
|
|
|
6291
6620
|
return results;
|
|
6292
6621
|
}
|
|
6293
6622
|
};
|
|
6294
|
-
registerCheck(
|
|
6623
|
+
registerCheck(check23);
|
|
6295
6624
|
|
|
6296
6625
|
// src/checks/source/layout-markers.ts
|
|
6297
6626
|
import { readFile as readFile16 } from "node:fs/promises";
|
|
6298
|
-
var
|
|
6627
|
+
var check24 = {
|
|
6299
6628
|
id: "source.markdown.layout-markers",
|
|
6300
6629
|
name: "Layout Markers",
|
|
6301
6630
|
description: "Reports @page/@section/@chapter marker arguments Gutterpress could not understand, plus any unknown gp-* class",
|
|
@@ -6307,7 +6636,7 @@ var check23 = {
|
|
|
6307
6636
|
return [];
|
|
6308
6637
|
const results = [];
|
|
6309
6638
|
const plugins = await loadPlugins(ctx.config.extensions, ctx.inputDir, (ref, error2) => {
|
|
6310
|
-
results.push(inspectionFailed(
|
|
6639
|
+
results.push(inspectionFailed(check24.id, `Plugin "${ref}" could not be loaded, so markers it defines were not checked: ${error2.message}`));
|
|
6311
6640
|
});
|
|
6312
6641
|
const md = createMarkdownRenderer(plugins);
|
|
6313
6642
|
for (const file of files) {
|
|
@@ -6316,7 +6645,7 @@ var check23 = {
|
|
|
6316
6645
|
const env = {};
|
|
6317
6646
|
md.render(content, env);
|
|
6318
6647
|
for (const w of env.layoutWarnings ?? []) {
|
|
6319
|
-
results.push(finding(
|
|
6648
|
+
results.push(finding(check24.id, {
|
|
6320
6649
|
severity: "warning",
|
|
6321
6650
|
message: w.message,
|
|
6322
6651
|
file,
|
|
@@ -6325,17 +6654,17 @@ var check23 = {
|
|
|
6325
6654
|
}));
|
|
6326
6655
|
}
|
|
6327
6656
|
} catch (error2) {
|
|
6328
|
-
results.push(inspectionFailed(
|
|
6657
|
+
results.push(inspectionFailed(check24.id, `Could not check layout markers in ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`, { file }));
|
|
6329
6658
|
}
|
|
6330
6659
|
}
|
|
6331
6660
|
return results;
|
|
6332
6661
|
}
|
|
6333
6662
|
};
|
|
6334
|
-
registerCheck(
|
|
6663
|
+
registerCheck(check24);
|
|
6335
6664
|
|
|
6336
6665
|
// src/checks/source/merge-markers.ts
|
|
6337
6666
|
import { readFile as readFile17 } from "node:fs/promises";
|
|
6338
|
-
import
|
|
6667
|
+
import path6 from "node:path";
|
|
6339
6668
|
|
|
6340
6669
|
// src/checks/asset/extensions.ts
|
|
6341
6670
|
var RASTER_INSPECTABLE_EXTS = [
|
|
@@ -6364,7 +6693,7 @@ var CLOSE_SENTINEL = ">>>>>>> online version";
|
|
|
6364
6693
|
function withoutOnlineTag(relPath) {
|
|
6365
6694
|
return relPath.replace(/\.online(?=\.[^./]*$|$)/, "");
|
|
6366
6695
|
}
|
|
6367
|
-
var
|
|
6696
|
+
var check25 = {
|
|
6368
6697
|
id: "source.sync.merge-markers",
|
|
6369
6698
|
name: "Combined Versions",
|
|
6370
6699
|
description: "Finds passages and files still holding two versions (yours and the online copy) after a sync",
|
|
@@ -6378,7 +6707,7 @@ var check24 = {
|
|
|
6378
6707
|
try {
|
|
6379
6708
|
content = await readFile17(file, "utf8");
|
|
6380
6709
|
} catch {
|
|
6381
|
-
results.push(inspectionFailed(
|
|
6710
|
+
results.push(inspectionFailed(check25.id, `Could not read source file: ${file}`, { file }));
|
|
6382
6711
|
continue;
|
|
6383
6712
|
}
|
|
6384
6713
|
if (!content.includes(OPEN_SENTINEL) && !content.includes(CLOSE_SENTINEL))
|
|
@@ -6390,7 +6719,7 @@ var check24 = {
|
|
|
6390
6719
|
const line = lines[i].endsWith("\r") ? lines[i].slice(0, -1) : lines[i];
|
|
6391
6720
|
if (line === OPEN_SENTINEL) {
|
|
6392
6721
|
inBlock = true;
|
|
6393
|
-
results.push(finding(
|
|
6722
|
+
results.push(finding(check25.id, {
|
|
6394
6723
|
severity: "error",
|
|
6395
6724
|
code: "two-versions-passage",
|
|
6396
6725
|
message: "This passage has two versions (yours and the online copy) — keep what you want, then delete the marker lines.",
|
|
@@ -6399,7 +6728,7 @@ var check24 = {
|
|
|
6399
6728
|
}));
|
|
6400
6729
|
} else if (line === CLOSE_SENTINEL) {
|
|
6401
6730
|
if (!inBlock) {
|
|
6402
|
-
results.push(finding(
|
|
6731
|
+
results.push(finding(check25.id, {
|
|
6403
6732
|
severity: "error",
|
|
6404
6733
|
code: "leftover-version-marker",
|
|
6405
6734
|
message: "This marker line is left over from combining two versions — delete it.",
|
|
@@ -6419,8 +6748,8 @@ var check24 = {
|
|
|
6419
6748
|
ignore: [...ASSET_SCAN_IGNORE_GLOBS]
|
|
6420
6749
|
});
|
|
6421
6750
|
for (const sibling of siblings.sort()) {
|
|
6422
|
-
const rel =
|
|
6423
|
-
results.push(finding(
|
|
6751
|
+
const rel = path6.relative(ctx.inputDir, sibling).split(path6.sep).join("/");
|
|
6752
|
+
results.push(finding(check25.id, {
|
|
6424
6753
|
severity: "warning",
|
|
6425
6754
|
code: "kept-both-versions",
|
|
6426
6755
|
message: `Two versions of ${withoutOnlineTag(rel)} are in your project — keep the one you want, then delete the other.`,
|
|
@@ -6428,12 +6757,12 @@ var check24 = {
|
|
|
6428
6757
|
}));
|
|
6429
6758
|
}
|
|
6430
6759
|
} catch (error2) {
|
|
6431
|
-
results.push(inspectionFailed(
|
|
6760
|
+
results.push(inspectionFailed(check25.id, `Could not scan for kept-both files: ${error2 instanceof Error ? error2.message : String(error2)}`));
|
|
6432
6761
|
}
|
|
6433
6762
|
return results;
|
|
6434
6763
|
}
|
|
6435
6764
|
};
|
|
6436
|
-
registerCheck(
|
|
6765
|
+
registerCheck(check25);
|
|
6437
6766
|
|
|
6438
6767
|
// src/checks/asset/image-file-size.ts
|
|
6439
6768
|
import { stat as stat3 } from "node:fs/promises";
|
|
@@ -6442,24 +6771,24 @@ import { stat as stat3 } from "node:fs/promises";
|
|
|
6442
6771
|
import { stat as stat2, readFile as readFile18 } from "node:fs/promises";
|
|
6443
6772
|
var DEFAULT_DPI = 72;
|
|
6444
6773
|
var cache = new Map;
|
|
6445
|
-
async function inspectImage(
|
|
6774
|
+
async function inspectImage(path7) {
|
|
6446
6775
|
let st;
|
|
6447
6776
|
try {
|
|
6448
|
-
st = await stat2(
|
|
6777
|
+
st = await stat2(path7);
|
|
6449
6778
|
} catch {
|
|
6450
6779
|
return null;
|
|
6451
6780
|
}
|
|
6452
|
-
const hit = cache.get(
|
|
6781
|
+
const hit = cache.get(path7);
|
|
6453
6782
|
if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size)
|
|
6454
6783
|
return hit.info;
|
|
6455
6784
|
let info2 = null;
|
|
6456
6785
|
try {
|
|
6457
|
-
const buf = await readFile18(
|
|
6786
|
+
const buf = await readFile18(path7);
|
|
6458
6787
|
info2 = parseImage(buf);
|
|
6459
6788
|
} catch {
|
|
6460
6789
|
info2 = null;
|
|
6461
6790
|
}
|
|
6462
|
-
cache.set(
|
|
6791
|
+
cache.set(path7, { mtimeMs: st.mtimeMs, size: st.size, info: info2 });
|
|
6463
6792
|
return info2;
|
|
6464
6793
|
}
|
|
6465
6794
|
function parseImage(b) {
|
|
@@ -6659,7 +6988,7 @@ async function collectImageFiles(dirs, exts, ignore = ASSET_SCAN_IGNORE_GLOBS) {
|
|
|
6659
6988
|
}
|
|
6660
6989
|
|
|
6661
6990
|
// src/checks/asset/image-file-size.ts
|
|
6662
|
-
var
|
|
6991
|
+
var check26 = {
|
|
6663
6992
|
id: "asset.image.file-size",
|
|
6664
6993
|
name: "Image File Size",
|
|
6665
6994
|
description: "Checks that image files do not exceed the maximum size limit",
|
|
@@ -6680,14 +7009,14 @@ var check25 = {
|
|
|
6680
7009
|
if (info2.size > maxSize) {
|
|
6681
7010
|
const sizeMb = (info2.size / 1e6).toFixed(1);
|
|
6682
7011
|
const maxMb = (maxSize / 1e6).toFixed(1);
|
|
6683
|
-
results.push(finding(
|
|
7012
|
+
results.push(finding(check26.id, {
|
|
6684
7013
|
severity: "warning",
|
|
6685
7014
|
message: `Image file too large: ${sizeMb}MB (max ${maxMb}MB)`,
|
|
6686
7015
|
file
|
|
6687
7016
|
}));
|
|
6688
7017
|
}
|
|
6689
7018
|
} catch {
|
|
6690
|
-
results.push(inspectionFailed(
|
|
7019
|
+
results.push(inspectionFailed(check26.id, `Could not stat image file: ${file}`, {
|
|
6691
7020
|
file
|
|
6692
7021
|
}));
|
|
6693
7022
|
}
|
|
@@ -6695,10 +7024,10 @@ var check25 = {
|
|
|
6695
7024
|
return results;
|
|
6696
7025
|
}
|
|
6697
7026
|
};
|
|
6698
|
-
registerCheck(
|
|
7027
|
+
registerCheck(check26);
|
|
6699
7028
|
|
|
6700
7029
|
// src/checks/asset/image-resolution.ts
|
|
6701
|
-
var
|
|
7030
|
+
var check27 = {
|
|
6702
7031
|
id: "asset.image.resolution",
|
|
6703
7032
|
name: "Image Resolution",
|
|
6704
7033
|
description: "Checks source image DPI from embedded density metadata",
|
|
@@ -6720,7 +7049,7 @@ var check26 = {
|
|
|
6720
7049
|
const { xDpi, yDpi } = info2;
|
|
6721
7050
|
if (xDpi > 0 && yDpi > 0 && (xDpi < minDpi || yDpi < minDpi)) {
|
|
6722
7051
|
results.push({
|
|
6723
|
-
checkId:
|
|
7052
|
+
checkId: check27.id,
|
|
6724
7053
|
severity: "warning",
|
|
6725
7054
|
message: `Image resolution too low: ${xDpi}x${yDpi} DPI (minimum ${minDpi} DPI)`,
|
|
6726
7055
|
file
|
|
@@ -6730,7 +7059,7 @@ var check26 = {
|
|
|
6730
7059
|
return results;
|
|
6731
7060
|
}
|
|
6732
7061
|
};
|
|
6733
|
-
registerCheck(
|
|
7062
|
+
registerCheck(check27);
|
|
6734
7063
|
|
|
6735
7064
|
// src/checks/asset/image-color-space.ts
|
|
6736
7065
|
var LABEL = {
|
|
@@ -6738,7 +7067,7 @@ var LABEL = {
|
|
|
6738
7067
|
gray: "Gray",
|
|
6739
7068
|
cmyk: "CMYK"
|
|
6740
7069
|
};
|
|
6741
|
-
var
|
|
7070
|
+
var check28 = {
|
|
6742
7071
|
id: "asset.image.color-space",
|
|
6743
7072
|
name: "Image Color Space",
|
|
6744
7073
|
description: "Validates image color spaces against allowed list",
|
|
@@ -6759,7 +7088,7 @@ var check27 = {
|
|
|
6759
7088
|
const cs = info2?.colorSpace;
|
|
6760
7089
|
if (cs && !allowedLower.has(cs)) {
|
|
6761
7090
|
results.push({
|
|
6762
|
-
checkId:
|
|
7091
|
+
checkId: check28.id,
|
|
6763
7092
|
severity: "warning",
|
|
6764
7093
|
message: `Image uses ${LABEL[cs] ?? cs} color space (allowed: ${allowed.join(", ")})`,
|
|
6765
7094
|
file
|
|
@@ -6769,10 +7098,10 @@ var check27 = {
|
|
|
6769
7098
|
return results;
|
|
6770
7099
|
}
|
|
6771
7100
|
};
|
|
6772
|
-
registerCheck(
|
|
7101
|
+
registerCheck(check28);
|
|
6773
7102
|
|
|
6774
7103
|
// src/checks/asset/image-alpha.ts
|
|
6775
|
-
var
|
|
7104
|
+
var check29 = {
|
|
6776
7105
|
id: "asset.image.alpha-channel",
|
|
6777
7106
|
name: "Image Alpha Channel",
|
|
6778
7107
|
description: "Checks for alpha channels in PNG/TIFF images",
|
|
@@ -6790,7 +7119,7 @@ var check28 = {
|
|
|
6790
7119
|
const info2 = await inspectImage(file);
|
|
6791
7120
|
if (info2?.hasAlpha) {
|
|
6792
7121
|
results.push({
|
|
6793
|
-
checkId:
|
|
7122
|
+
checkId: check29.id,
|
|
6794
7123
|
severity: "warning",
|
|
6795
7124
|
message: "Image contains alpha channel, which may cause print issues",
|
|
6796
7125
|
file
|
|
@@ -6800,10 +7129,10 @@ var check28 = {
|
|
|
6800
7129
|
return results;
|
|
6801
7130
|
}
|
|
6802
7131
|
};
|
|
6803
|
-
registerCheck(
|
|
7132
|
+
registerCheck(check29);
|
|
6804
7133
|
|
|
6805
7134
|
// src/checks/asset/image-tac.ts
|
|
6806
|
-
var
|
|
7135
|
+
var check30 = {
|
|
6807
7136
|
id: "asset.image.tac-raster",
|
|
6808
7137
|
name: "Image TAC (Raster)",
|
|
6809
7138
|
description: "Rasterizes and checks TAC per image using Ghostscript",
|
|
@@ -6822,7 +7151,7 @@ var check29 = {
|
|
|
6822
7151
|
const ghostscript = await resolveGhostscript();
|
|
6823
7152
|
if (!ghostscript) {
|
|
6824
7153
|
return [
|
|
6825
|
-
inspectionFailed(
|
|
7154
|
+
inspectionFailed(check30.id, "Could not inspect image ink coverage: Ghostscript executable not found")
|
|
6826
7155
|
];
|
|
6827
7156
|
}
|
|
6828
7157
|
for (const file of files) {
|
|
@@ -6840,7 +7169,7 @@ var check29 = {
|
|
|
6840
7169
|
if (nums.length === 4 && nums.every((n) => Number.isFinite(n))) {
|
|
6841
7170
|
const tac = (nums[0] + nums[1] + nums[2] + nums[3]) * 100;
|
|
6842
7171
|
if (tac > maxTac) {
|
|
6843
|
-
results.push(finding(
|
|
7172
|
+
results.push(finding(check30.id, {
|
|
6844
7173
|
severity: "warning",
|
|
6845
7174
|
message: `Image TAC exceeds limit: ${tac.toFixed(1)}% (max ${maxTac}%)`,
|
|
6846
7175
|
file,
|
|
@@ -6852,16 +7181,16 @@ var check29 = {
|
|
|
6852
7181
|
}
|
|
6853
7182
|
}
|
|
6854
7183
|
} catch {
|
|
6855
|
-
results.push(inspectionFailed(
|
|
7184
|
+
results.push(inspectionFailed(check30.id, `Could not inspect image ink coverage: ${file}`, { file }));
|
|
6856
7185
|
}
|
|
6857
7186
|
}
|
|
6858
7187
|
return results;
|
|
6859
7188
|
}
|
|
6860
7189
|
};
|
|
6861
|
-
registerCheck(
|
|
7190
|
+
registerCheck(check30);
|
|
6862
7191
|
|
|
6863
7192
|
// src/checks/asset/approved-fonts.ts
|
|
6864
|
-
var
|
|
7193
|
+
var check31 = {
|
|
6865
7194
|
id: "asset.font.approved-files",
|
|
6866
7195
|
name: "Approved Font Files",
|
|
6867
7196
|
description: "Checks font files against the approved file patterns",
|
|
@@ -6900,7 +7229,7 @@ var check30 = {
|
|
|
6900
7229
|
for (const font of allFonts) {
|
|
6901
7230
|
if (!approvedFonts.has(font)) {
|
|
6902
7231
|
results.push({
|
|
6903
|
-
checkId:
|
|
7232
|
+
checkId: check31.id,
|
|
6904
7233
|
severity: "warning",
|
|
6905
7234
|
message: "Font file not in approved list",
|
|
6906
7235
|
file: font
|
|
@@ -6910,7 +7239,7 @@ var check30 = {
|
|
|
6910
7239
|
return results;
|
|
6911
7240
|
}
|
|
6912
7241
|
};
|
|
6913
|
-
registerCheck(
|
|
7242
|
+
registerCheck(check31);
|
|
6914
7243
|
|
|
6915
7244
|
// src/checks/asset/font-license.ts
|
|
6916
7245
|
import { existsSync as existsSync9 } from "node:fs";
|
|
@@ -6925,7 +7254,7 @@ var LICENSE_NAMES = [
|
|
|
6925
7254
|
"OFL-1.1.txt",
|
|
6926
7255
|
"COPYING"
|
|
6927
7256
|
];
|
|
6928
|
-
var
|
|
7257
|
+
var check32 = {
|
|
6929
7258
|
id: "asset.font.license",
|
|
6930
7259
|
name: "Font License",
|
|
6931
7260
|
description: "Checks for license files in font directories",
|
|
@@ -6952,7 +7281,7 @@ var check31 = {
|
|
|
6952
7281
|
const hasLicense = LICENSE_NAMES.some((name) => existsSync9(resolve7(fontDir, name)));
|
|
6953
7282
|
if (!hasLicense) {
|
|
6954
7283
|
results.push({
|
|
6955
|
-
checkId:
|
|
7284
|
+
checkId: check32.id,
|
|
6956
7285
|
severity: "warning",
|
|
6957
7286
|
message: `No font license file found in directory`,
|
|
6958
7287
|
file: fontDir
|
|
@@ -6962,10 +7291,10 @@ var check31 = {
|
|
|
6962
7291
|
return results;
|
|
6963
7292
|
}
|
|
6964
7293
|
};
|
|
6965
|
-
registerCheck(
|
|
7294
|
+
registerCheck(check32);
|
|
6966
7295
|
|
|
6967
7296
|
// src/checks/heuristic/text-density.ts
|
|
6968
|
-
var
|
|
7297
|
+
var check33 = {
|
|
6969
7298
|
id: "heuristic.whitespace.text-density",
|
|
6970
7299
|
name: "Text Density",
|
|
6971
7300
|
description: "Checks characters-per-page ratio",
|
|
@@ -6994,7 +7323,7 @@ var check32 = {
|
|
|
6994
7323
|
});
|
|
6995
7324
|
if (lowPages.length > 0) {
|
|
6996
7325
|
results.push({
|
|
6997
|
-
checkId:
|
|
7326
|
+
checkId: check33.id,
|
|
6998
7327
|
severity: "info",
|
|
6999
7328
|
message: `Low text density on pages: ${lowPages.join(", ")} (below ${range.min} chars)`,
|
|
7000
7329
|
file: ctx.pdfPath
|
|
@@ -7002,7 +7331,7 @@ var check32 = {
|
|
|
7002
7331
|
}
|
|
7003
7332
|
if (highPages.length > 0) {
|
|
7004
7333
|
results.push({
|
|
7005
|
-
checkId:
|
|
7334
|
+
checkId: check33.id,
|
|
7006
7335
|
severity: "info",
|
|
7007
7336
|
message: `High text density on pages: ${highPages.join(", ")} (above ${range.max} chars)`,
|
|
7008
7337
|
file: ctx.pdfPath
|
|
@@ -7011,11 +7340,11 @@ var check32 = {
|
|
|
7011
7340
|
return results;
|
|
7012
7341
|
}
|
|
7013
7342
|
};
|
|
7014
|
-
registerCheck(
|
|
7343
|
+
registerCheck(check33);
|
|
7015
7344
|
|
|
7016
7345
|
// src/checks/heuristic/section-density.ts
|
|
7017
7346
|
import { readFile as readFile19 } from "node:fs/promises";
|
|
7018
|
-
var
|
|
7347
|
+
var check34 = {
|
|
7019
7348
|
id: "heuristic.chunking.section-density",
|
|
7020
7349
|
name: "Section Density",
|
|
7021
7350
|
description: "Checks heading/paragraph/callout density from source Markdown",
|
|
@@ -7041,7 +7370,7 @@ var check33 = {
|
|
|
7041
7370
|
const line = lines[i];
|
|
7042
7371
|
if (/^#{1,6}\s/.test(line)) {
|
|
7043
7372
|
if (paragraphCount > maxParas) {
|
|
7044
|
-
results.push(finding(
|
|
7373
|
+
results.push(finding(check34.id, {
|
|
7045
7374
|
severity: "info",
|
|
7046
7375
|
message: `Section has ${paragraphCount} paragraphs (max recommended: ${maxParas})`,
|
|
7047
7376
|
file,
|
|
@@ -7061,7 +7390,7 @@ var check33 = {
|
|
|
7061
7390
|
}
|
|
7062
7391
|
}
|
|
7063
7392
|
if (paragraphCount > maxParas) {
|
|
7064
|
-
results.push(finding(
|
|
7393
|
+
results.push(finding(check34.id, {
|
|
7065
7394
|
severity: "info",
|
|
7066
7395
|
message: `Section has ${paragraphCount} paragraphs (max recommended: ${maxParas})`,
|
|
7067
7396
|
file,
|
|
@@ -7069,7 +7398,7 @@ var check33 = {
|
|
|
7069
7398
|
}));
|
|
7070
7399
|
}
|
|
7071
7400
|
} catch {
|
|
7072
|
-
results.push(inspectionFailed(
|
|
7401
|
+
results.push(inspectionFailed(check34.id, `Could not read source file: ${file}`, {
|
|
7073
7402
|
file
|
|
7074
7403
|
}));
|
|
7075
7404
|
}
|
|
@@ -7077,10 +7406,10 @@ var check33 = {
|
|
|
7077
7406
|
return results;
|
|
7078
7407
|
}
|
|
7079
7408
|
};
|
|
7080
|
-
registerCheck(
|
|
7409
|
+
registerCheck(check34);
|
|
7081
7410
|
|
|
7082
7411
|
// src/checks/heuristic/layer-count.ts
|
|
7083
|
-
var
|
|
7412
|
+
var check35 = {
|
|
7084
7413
|
id: "heuristic.decoration.layer-count",
|
|
7085
7414
|
name: "Layer Count",
|
|
7086
7415
|
description: "Counts image objects per page",
|
|
@@ -7105,7 +7434,7 @@ var check34 = {
|
|
|
7105
7434
|
heavyPages.sort((a, b) => a - b);
|
|
7106
7435
|
return [
|
|
7107
7436
|
{
|
|
7108
|
-
checkId:
|
|
7437
|
+
checkId: check35.id,
|
|
7109
7438
|
severity: "info",
|
|
7110
7439
|
message: `Pages with many image layers (>${maxLayers}): ${heavyPages.join(", ")}`,
|
|
7111
7440
|
file: ctx.pdfPath
|
|
@@ -7115,10 +7444,10 @@ var check34 = {
|
|
|
7115
7444
|
return [];
|
|
7116
7445
|
}
|
|
7117
7446
|
};
|
|
7118
|
-
registerCheck(
|
|
7447
|
+
registerCheck(check35);
|
|
7119
7448
|
|
|
7120
7449
|
// src/checks/heuristic/placement-variance.ts
|
|
7121
|
-
var
|
|
7450
|
+
var check36 = {
|
|
7122
7451
|
id: "heuristic.layout.placement-variance",
|
|
7123
7452
|
name: "Placement Variance",
|
|
7124
7453
|
description: "Analyzes text baseline coordinates for layout consistency",
|
|
@@ -7136,7 +7465,7 @@ var check35 = {
|
|
|
7136
7465
|
const uniqueX = new Set(positions.map((p) => Math.round(p.x)));
|
|
7137
7466
|
return [
|
|
7138
7467
|
{
|
|
7139
|
-
checkId:
|
|
7468
|
+
checkId: check36.id,
|
|
7140
7469
|
severity: "info",
|
|
7141
7470
|
message: `Layout analysis: ${uniqueX.size} unique horizontal text positions across ${positions.length} text blocks.`,
|
|
7142
7471
|
file: ctx.pdfPath
|
|
@@ -7144,21 +7473,21 @@ var check35 = {
|
|
|
7144
7473
|
];
|
|
7145
7474
|
}
|
|
7146
7475
|
};
|
|
7147
|
-
registerCheck(
|
|
7476
|
+
registerCheck(check36);
|
|
7148
7477
|
|
|
7149
7478
|
// src/checks/tool-check.ts
|
|
7150
7479
|
async function checkToolAvailability(config, opts = {}) {
|
|
7151
7480
|
const { checks: checks2 } = selectChecks(opts, config);
|
|
7152
7481
|
const toolToChecks = new Map;
|
|
7153
|
-
for (const
|
|
7154
|
-
if (!
|
|
7482
|
+
for (const check37 of checks2) {
|
|
7483
|
+
if (!check37.requiredTools?.length)
|
|
7155
7484
|
continue;
|
|
7156
|
-
for (const tool of
|
|
7485
|
+
for (const tool of check37.requiredTools) {
|
|
7157
7486
|
const existing = toolToChecks.get(tool);
|
|
7158
7487
|
if (existing) {
|
|
7159
|
-
existing.push(
|
|
7488
|
+
existing.push(check37.id);
|
|
7160
7489
|
} else {
|
|
7161
|
-
toolToChecks.set(tool, [
|
|
7490
|
+
toolToChecks.set(tool, [check37.id]);
|
|
7162
7491
|
}
|
|
7163
7492
|
}
|
|
7164
7493
|
}
|
|
@@ -7174,11 +7503,11 @@ async function checkToolAvailability(config, opts = {}) {
|
|
|
7174
7503
|
const missing = results.filter((r) => !r.found).map((r) => r.tool);
|
|
7175
7504
|
const missingSet = new Set(missing);
|
|
7176
7505
|
const skippedChecks = [];
|
|
7177
|
-
for (const
|
|
7178
|
-
if (!
|
|
7506
|
+
for (const check37 of checks2) {
|
|
7507
|
+
if (!check37.requiredTools?.length)
|
|
7179
7508
|
continue;
|
|
7180
|
-
if (
|
|
7181
|
-
skippedChecks.push(
|
|
7509
|
+
if (check37.requiredTools.some((t) => missingSet.has(t))) {
|
|
7510
|
+
skippedChecks.push(check37.id);
|
|
7182
7511
|
}
|
|
7183
7512
|
}
|
|
7184
7513
|
return { available, missing, skippedChecks, toolToChecks };
|
|
@@ -7200,7 +7529,7 @@ import { existsSync as existsSync10 } from "node:fs";
|
|
|
7200
7529
|
import { dirname as dirname4, join as join3, resolve as resolve8 } from "node:path";
|
|
7201
7530
|
|
|
7202
7531
|
// src/lib/output-paths.ts
|
|
7203
|
-
import
|
|
7532
|
+
import path7 from "node:path";
|
|
7204
7533
|
|
|
7205
7534
|
// src/lib/slug.ts
|
|
7206
7535
|
function slugify(name, fallback = "") {
|
|
@@ -7218,7 +7547,7 @@ function bookSlug(title) {
|
|
|
7218
7547
|
return slugify(title ?? "", "book");
|
|
7219
7548
|
}
|
|
7220
7549
|
function resolveOutputDir(manifestDir, title) {
|
|
7221
|
-
return
|
|
7550
|
+
return path7.resolve(manifestDir, DIST_DIRNAME, bookSlug(title));
|
|
7222
7551
|
}
|
|
7223
7552
|
function artifactName(title, format) {
|
|
7224
7553
|
return `${bookSlug(title)}-${format}.pdf`;
|
|
@@ -7473,25 +7802,25 @@ async function runChecks(ctx, opts = {}) {
|
|
|
7473
7802
|
}
|
|
7474
7803
|
const release = retainPdfCache();
|
|
7475
7804
|
try {
|
|
7476
|
-
for (const
|
|
7805
|
+
for (const check37 of checks2) {
|
|
7477
7806
|
try {
|
|
7478
|
-
const results = await
|
|
7479
|
-
const severityOverride = getCheckSeverityOverride(
|
|
7807
|
+
const results = await check37.run(ctx);
|
|
7808
|
+
const severityOverride = getCheckSeverityOverride(check37.id, ctx.config);
|
|
7480
7809
|
if (severityOverride) {
|
|
7481
7810
|
for (const r of results) {
|
|
7482
7811
|
r.severity = severityOverride;
|
|
7483
7812
|
}
|
|
7484
7813
|
}
|
|
7485
7814
|
if (results.length === 0) {
|
|
7486
|
-
passed.push(
|
|
7815
|
+
passed.push(check37.id);
|
|
7487
7816
|
} else {
|
|
7488
7817
|
allResults.push(...results);
|
|
7489
7818
|
}
|
|
7490
7819
|
} catch (err) {
|
|
7491
7820
|
allResults.push({
|
|
7492
|
-
checkId:
|
|
7821
|
+
checkId: check37.id,
|
|
7493
7822
|
severity: "error",
|
|
7494
|
-
message: `Check "${
|
|
7823
|
+
message: `Check "${check37.id}" threw: ${err instanceof Error ? err.message : String(err)}`
|
|
7495
7824
|
});
|
|
7496
7825
|
}
|
|
7497
7826
|
}
|
|
@@ -7862,8 +8191,8 @@ import { randomBytes } from "node:crypto";
|
|
|
7862
8191
|
// src/lib/build-fingerprint.ts
|
|
7863
8192
|
import * as fs from "node:fs";
|
|
7864
8193
|
import { mkdir as mkdir3, readFile as readFile21, writeFile as writeFile3 } from "node:fs/promises";
|
|
7865
|
-
import { createHash as
|
|
7866
|
-
import
|
|
8194
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
8195
|
+
import path8 from "node:path";
|
|
7867
8196
|
import git from "isomorphic-git";
|
|
7868
8197
|
var FINGERPRINT_FILENAME = "build-fingerprint.json";
|
|
7869
8198
|
var VERSION_TIMEOUT_MS = 4000;
|
|
@@ -7912,7 +8241,7 @@ async function getGitRevision(sourceDir) {
|
|
|
7912
8241
|
const candidateDirs = [sourceDir, process.cwd()].filter((v) => Boolean(v));
|
|
7913
8242
|
const seen = new Set;
|
|
7914
8243
|
for (const dir of candidateDirs) {
|
|
7915
|
-
const abs =
|
|
8244
|
+
const abs = path8.resolve(dir);
|
|
7916
8245
|
if (seen.has(abs))
|
|
7917
8246
|
continue;
|
|
7918
8247
|
seen.add(abs);
|
|
@@ -7951,7 +8280,7 @@ async function getEngineBundleHash() {
|
|
|
7951
8280
|
readFile21(viewerPath),
|
|
7952
8281
|
readFile21(agentPath)
|
|
7953
8282
|
]);
|
|
7954
|
-
const hash =
|
|
8283
|
+
const hash = createHash5("sha256");
|
|
7955
8284
|
hash.update(viewerBytes);
|
|
7956
8285
|
hash.update(agentBytes);
|
|
7957
8286
|
return hash.digest("hex").slice(0, 12);
|
|
@@ -7997,8 +8326,8 @@ function sanitizeArgs(args) {
|
|
|
7997
8326
|
return out;
|
|
7998
8327
|
}
|
|
7999
8328
|
async function writeBuildFingerprint(input) {
|
|
8000
|
-
const outputDir =
|
|
8001
|
-
const outPath =
|
|
8329
|
+
const outputDir = path8.resolve(input.outputDir);
|
|
8330
|
+
const outPath = path8.join(outputDir, FINGERPRINT_FILENAME);
|
|
8002
8331
|
const [tools, sourceRevision] = await Promise.all([
|
|
8003
8332
|
getToolVersions(),
|
|
8004
8333
|
getGitRevision(input.sourceDir)
|
|
@@ -8014,7 +8343,7 @@ async function writeBuildFingerprint(input) {
|
|
|
8014
8343
|
iccPath: input.pdfx.iccPath,
|
|
8015
8344
|
stripAnnotations: input.pdfx.stripAnnotations
|
|
8016
8345
|
},
|
|
8017
|
-
outputDir: input.recordedOutputDir ?
|
|
8346
|
+
outputDir: input.recordedOutputDir ? path8.resolve(input.recordedOutputDir) : outputDir
|
|
8018
8347
|
},
|
|
8019
8348
|
sourceRevision,
|
|
8020
8349
|
tools
|
|
@@ -8089,245 +8418,6 @@ function computeGates(format, opts, config) {
|
|
|
8089
8418
|
return { lint: lint2, preValidate, postValidate };
|
|
8090
8419
|
}
|
|
8091
8420
|
|
|
8092
|
-
// src/lib/build-staging.ts
|
|
8093
|
-
import path8 from "node:path";
|
|
8094
|
-
import os from "node:os";
|
|
8095
|
-
import fsp from "node:fs/promises";
|
|
8096
|
-
|
|
8097
|
-
// src/lib/missing-asset-placeholder.ts
|
|
8098
|
-
import { createHash as createHash5 } from "node:crypto";
|
|
8099
|
-
import { deflateSync } from "node:zlib";
|
|
8100
|
-
function placeholderOutputPath(missingOutputPath) {
|
|
8101
|
-
const hash = createHash5("sha256").update(missingOutputPath).digest("hex").slice(0, 16);
|
|
8102
|
-
return `assets/gutterpress-missing/${hash}.png`;
|
|
8103
|
-
}
|
|
8104
|
-
function decodeHtmlAttribute(value) {
|
|
8105
|
-
return value.replace(/"/g, '"').replace(/'|'/g, "'").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
8106
|
-
}
|
|
8107
|
-
function rewriteMissingImageReferences(html, replacements) {
|
|
8108
|
-
if (replacements.size === 0)
|
|
8109
|
-
return html;
|
|
8110
|
-
const replacementFor = (raw) => replacements.get(raw) ?? replacements.get(decodeHtmlAttribute(raw));
|
|
8111
|
-
const cssUrl = /url\(\s*(?:"([^"]*)"|'([^']*)'|"((?:(?!").)*)"|([^'"()\s]*))\s*\)/giy;
|
|
8112
|
-
const rewriteCssUrls = (css) => {
|
|
8113
|
-
let out2 = "";
|
|
8114
|
-
let copiedThrough = 0;
|
|
8115
|
-
let index = 0;
|
|
8116
|
-
while (index < css.length) {
|
|
8117
|
-
if (css.startsWith("/*", index)) {
|
|
8118
|
-
const end = css.indexOf("*/", index + 2);
|
|
8119
|
-
index = end < 0 ? css.length : end + 2;
|
|
8120
|
-
continue;
|
|
8121
|
-
}
|
|
8122
|
-
const char = css[index];
|
|
8123
|
-
if (char === '"' || char === "'") {
|
|
8124
|
-
const quote = char;
|
|
8125
|
-
index++;
|
|
8126
|
-
while (index < css.length) {
|
|
8127
|
-
if (css[index] === "\\") {
|
|
8128
|
-
index += 2;
|
|
8129
|
-
continue;
|
|
8130
|
-
}
|
|
8131
|
-
const current = css[index++];
|
|
8132
|
-
if (current === quote)
|
|
8133
|
-
break;
|
|
8134
|
-
}
|
|
8135
|
-
continue;
|
|
8136
|
-
}
|
|
8137
|
-
const previous = index > 0 ? css[index - 1] : "";
|
|
8138
|
-
if (!/[a-z0-9_-]/i.test(previous) && css.slice(index, index + 3).toLowerCase() === "url") {
|
|
8139
|
-
cssUrl.lastIndex = index;
|
|
8140
|
-
const match = cssUrl.exec(css);
|
|
8141
|
-
if (match) {
|
|
8142
|
-
const raw = match[1] ?? match[2] ?? match[3] ?? match[4] ?? "";
|
|
8143
|
-
const replacement = replacementFor(raw);
|
|
8144
|
-
if (replacement) {
|
|
8145
|
-
out2 += css.slice(copiedThrough, index);
|
|
8146
|
-
out2 += match[3] !== undefined ? `url("${replacement}")` : `url("${replacement}")`;
|
|
8147
|
-
index = cssUrl.lastIndex;
|
|
8148
|
-
copiedThrough = index;
|
|
8149
|
-
continue;
|
|
8150
|
-
}
|
|
8151
|
-
}
|
|
8152
|
-
}
|
|
8153
|
-
index++;
|
|
8154
|
-
}
|
|
8155
|
-
return copiedThrough === 0 ? css : out2 + css.slice(copiedThrough);
|
|
8156
|
-
};
|
|
8157
|
-
const rewriteTag = (tag) => {
|
|
8158
|
-
let out2 = tag;
|
|
8159
|
-
if (/^<img\b/i.test(tag)) {
|
|
8160
|
-
out2 = out2.replace(/(\s+src\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi, (whole, prefix, double, single, bare) => {
|
|
8161
|
-
const raw = double ?? single ?? bare ?? "";
|
|
8162
|
-
const replacement = replacementFor(raw);
|
|
8163
|
-
if (!replacement)
|
|
8164
|
-
return whole;
|
|
8165
|
-
if (double !== undefined)
|
|
8166
|
-
return `${prefix}"${replacement}"`;
|
|
8167
|
-
if (single !== undefined)
|
|
8168
|
-
return `${prefix}'${replacement}'`;
|
|
8169
|
-
return `${prefix}${replacement}`;
|
|
8170
|
-
});
|
|
8171
|
-
}
|
|
8172
|
-
if (/^<(?:img|source)\b/i.test(tag)) {
|
|
8173
|
-
out2 = out2.replace(/(\s+srcset\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi, (whole, prefix, double, single, bare) => {
|
|
8174
|
-
const value = double ?? single ?? bare ?? "";
|
|
8175
|
-
let rewritten = "";
|
|
8176
|
-
let copiedThrough = 0;
|
|
8177
|
-
let changed = false;
|
|
8178
|
-
for (const candidate of parseSrcsetUrlCandidates(value)) {
|
|
8179
|
-
const replacement = replacementFor(candidate.url);
|
|
8180
|
-
if (!replacement)
|
|
8181
|
-
continue;
|
|
8182
|
-
rewritten += value.slice(copiedThrough, candidate.start) + replacement;
|
|
8183
|
-
copiedThrough = candidate.end;
|
|
8184
|
-
changed = true;
|
|
8185
|
-
}
|
|
8186
|
-
if (!changed)
|
|
8187
|
-
return whole;
|
|
8188
|
-
rewritten += value.slice(copiedThrough);
|
|
8189
|
-
if (double !== undefined)
|
|
8190
|
-
return `${prefix}"${rewritten}"`;
|
|
8191
|
-
if (single !== undefined)
|
|
8192
|
-
return `${prefix}'${rewritten}'`;
|
|
8193
|
-
return `${prefix}${rewritten}`;
|
|
8194
|
-
});
|
|
8195
|
-
}
|
|
8196
|
-
out2 = out2.replace(/(\s+style\s*=\s*)(["'])(.*?)\2/gi, (whole, prefix, quote, value) => {
|
|
8197
|
-
const rewritten = rewriteCssUrls(value);
|
|
8198
|
-
return rewritten === value ? whole : `${prefix}${quote}${rewritten}${quote}`;
|
|
8199
|
-
});
|
|
8200
|
-
return out2;
|
|
8201
|
-
};
|
|
8202
|
-
const rewriteActiveHtml = (active) => active.replace(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g, (tag) => rewriteTag(tag));
|
|
8203
|
-
const protectedRegion = /<!--[\s\S]*?-->|<(script|style|pre|code|textarea)\b[^>]*>[\s\S]*?<\/\1\s*>/gi;
|
|
8204
|
-
let out = "";
|
|
8205
|
-
let last = 0;
|
|
8206
|
-
for (const match of html.matchAll(protectedRegion)) {
|
|
8207
|
-
out += rewriteActiveHtml(html.slice(last, match.index));
|
|
8208
|
-
if (match[1]?.toLowerCase() === "style") {
|
|
8209
|
-
const style = /^(<style\b[^>]*>)([\s\S]*)(<\/style\s*>)$/i.exec(match[0]);
|
|
8210
|
-
out += style ? `${rewriteTag(style[1])}${rewriteCssUrls(style[2])}${style[3]}` : match[0];
|
|
8211
|
-
} else {
|
|
8212
|
-
out += match[0];
|
|
8213
|
-
}
|
|
8214
|
-
last = (match.index ?? 0) + match[0].length;
|
|
8215
|
-
}
|
|
8216
|
-
out += rewriteActiveHtml(html.slice(last));
|
|
8217
|
-
return out;
|
|
8218
|
-
}
|
|
8219
|
-
function crc32(buf) {
|
|
8220
|
-
let c = ~0;
|
|
8221
|
-
for (let i = 0;i < buf.length; i++) {
|
|
8222
|
-
c ^= buf[i];
|
|
8223
|
-
for (let k = 0;k < 8; k++)
|
|
8224
|
-
c = c >>> 1 ^ 3988292384 & -(c & 1);
|
|
8225
|
-
}
|
|
8226
|
-
return ~c >>> 0;
|
|
8227
|
-
}
|
|
8228
|
-
function chunk(type, data) {
|
|
8229
|
-
const out = new Uint8Array(data.length + 12);
|
|
8230
|
-
const view = new DataView(out.buffer);
|
|
8231
|
-
view.setUint32(0, data.length);
|
|
8232
|
-
for (let i = 0;i < 4; i++)
|
|
8233
|
-
out[4 + i] = type.charCodeAt(i);
|
|
8234
|
-
out.set(data, 8);
|
|
8235
|
-
view.setUint32(data.length + 8, crc32(out.subarray(4, data.length + 8)));
|
|
8236
|
-
return out;
|
|
8237
|
-
}
|
|
8238
|
-
function placeholderPng(width = 640, height = 480, cell = 32) {
|
|
8239
|
-
const A = [217, 70, 239];
|
|
8240
|
-
const B = [26, 26, 26];
|
|
8241
|
-
const raw = new Uint8Array(height * (1 + width * 3));
|
|
8242
|
-
let p = 0;
|
|
8243
|
-
for (let y = 0;y < height; y++) {
|
|
8244
|
-
raw[p++] = 0;
|
|
8245
|
-
for (let x = 0;x < width; x++) {
|
|
8246
|
-
const c = ((x / cell | 0) + (y / cell | 0)) % 2 === 0 ? A : B;
|
|
8247
|
-
raw[p++] = c[0];
|
|
8248
|
-
raw[p++] = c[1];
|
|
8249
|
-
raw[p++] = c[2];
|
|
8250
|
-
}
|
|
8251
|
-
}
|
|
8252
|
-
const ihdr = new Uint8Array(13);
|
|
8253
|
-
const hv = new DataView(ihdr.buffer);
|
|
8254
|
-
hv.setUint32(0, width);
|
|
8255
|
-
hv.setUint32(4, height);
|
|
8256
|
-
ihdr[8] = 8;
|
|
8257
|
-
ihdr[9] = 2;
|
|
8258
|
-
const parts = [
|
|
8259
|
-
new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
|
|
8260
|
-
chunk("IHDR", ihdr),
|
|
8261
|
-
chunk("IDAT", new Uint8Array(deflateSync(raw))),
|
|
8262
|
-
chunk("IEND", new Uint8Array(0))
|
|
8263
|
-
];
|
|
8264
|
-
const total = parts.reduce((n, x) => n + x.length, 0);
|
|
8265
|
-
const png = new Uint8Array(total);
|
|
8266
|
-
let o = 0;
|
|
8267
|
-
for (const part of parts) {
|
|
8268
|
-
png.set(part, o);
|
|
8269
|
-
o += part.length;
|
|
8270
|
-
}
|
|
8271
|
-
return png;
|
|
8272
|
-
}
|
|
8273
|
-
|
|
8274
|
-
// src/lib/build-staging.ts
|
|
8275
|
-
async function shipViewerHtml(htmlFile, outDir) {
|
|
8276
|
-
await fsp.mkdir(path8.join(outDir, "engine"), { recursive: true });
|
|
8277
|
-
await fsp.copyFile(await getAssetPath("engine/gutterpress-viewer.js"), path8.join(outDir, "engine/gutterpress-viewer.js"));
|
|
8278
|
-
const tag = ` <script src="engine/gutterpress-viewer.js"></script>
|
|
8279
|
-
`;
|
|
8280
|
-
const html = await fsp.readFile(htmlFile, "utf-8");
|
|
8281
|
-
await fsp.writeFile(htmlFile, /<\/head>/i.test(html) ? html.replace(/<\/head>/i, tag + "</head>") : tag + html, "utf-8");
|
|
8282
|
-
}
|
|
8283
|
-
async function stageBookAssets(options) {
|
|
8284
|
-
const { renderDir, outDir, htmlFile, imageRefs, cssAssets, onPlan } = options;
|
|
8285
|
-
const { copies: imageCopies, errors, destinations } = await planImageCopies(renderDir, imageRefs);
|
|
8286
|
-
const copies = [...cssAssets, ...imageCopies];
|
|
8287
|
-
onPlan?.({ unresolved: errors, copyCount: copies.length });
|
|
8288
|
-
const missingPlaceholders = copies.length ? await copyReferencedAssets(copies, outDir) : new Map;
|
|
8289
|
-
let staged = await fsp.readFile(htmlFile, "utf8");
|
|
8290
|
-
if (missingPlaceholders.size > 0) {
|
|
8291
|
-
const rewrites = new Map(missingPlaceholders);
|
|
8292
|
-
for (const [ref, dest] of destinations) {
|
|
8293
|
-
const placeholder = missingPlaceholders.get(dest);
|
|
8294
|
-
if (placeholder)
|
|
8295
|
-
rewrites.set(ref, placeholder);
|
|
8296
|
-
}
|
|
8297
|
-
staged = rewriteMissingImageReferences(staged, rewrites);
|
|
8298
|
-
await fsp.writeFile(htmlFile, staged, "utf8");
|
|
8299
|
-
}
|
|
8300
|
-
if (staged.includes("--gp-shape:")) {
|
|
8301
|
-
await fsp.writeFile(htmlFile, await inlineShapeUrls(staged, outDir), "utf8");
|
|
8302
|
-
}
|
|
8303
|
-
return { missing: [...missingPlaceholders.keys()].sort() };
|
|
8304
|
-
}
|
|
8305
|
-
async function copyReferencedAssets(copies, outDir) {
|
|
8306
|
-
const dirs = new Set(copies.map((c) => path8.dirname(path8.resolve(outDir, c.to))));
|
|
8307
|
-
await Promise.all([...dirs].map((d) => fsp.mkdir(d, { recursive: true })));
|
|
8308
|
-
const missing = new Map;
|
|
8309
|
-
await Promise.all(copies.map(async (c) => {
|
|
8310
|
-
const dest = path8.resolve(outDir, c.to);
|
|
8311
|
-
try {
|
|
8312
|
-
await fsp.copyFile(c.from, dest);
|
|
8313
|
-
} catch (err) {
|
|
8314
|
-
if (err?.code === "ENOENT") {
|
|
8315
|
-
const placeholder = placeholderOutputPath(c.to);
|
|
8316
|
-
const placeholderDest = path8.resolve(outDir, placeholder);
|
|
8317
|
-
await fsp.mkdir(path8.dirname(placeholderDest), { recursive: true });
|
|
8318
|
-
await fsp.writeFile(placeholderDest, placeholderPng());
|
|
8319
|
-
missing.set(c.to, placeholder);
|
|
8320
|
-
return;
|
|
8321
|
-
}
|
|
8322
|
-
throw new BuildError(`Could not copy asset ${c.from} → ${c.to}: ` + (err instanceof Error ? err.message : String(err)), 1);
|
|
8323
|
-
}
|
|
8324
|
-
}));
|
|
8325
|
-
return missing;
|
|
8326
|
-
}
|
|
8327
|
-
async function createStageRoot() {
|
|
8328
|
-
return fsp.mkdtemp(path8.join(os.tmpdir(), "gutterpress-stage-"));
|
|
8329
|
-
}
|
|
8330
|
-
|
|
8331
8421
|
// src/lib/build-runner.ts
|
|
8332
8422
|
function splitOutPath(outArg, format) {
|
|
8333
8423
|
if (typeof outArg !== "string" || outArg.length === 0) {
|
|
@@ -8423,7 +8513,7 @@ async function runQualityGates(ctx) {
|
|
|
8423
8513
|
}
|
|
8424
8514
|
}
|
|
8425
8515
|
async function renderBook(ctx) {
|
|
8426
|
-
const { config, gates, renderDir, workDir, opts } = ctx;
|
|
8516
|
+
const { config, gates, renderDir, workDir, opts, format } = ctx;
|
|
8427
8517
|
if (config.source.files && config.source.files.length > 0) {
|
|
8428
8518
|
log.info(`Using specified files (${config.source.files.length} total)`);
|
|
8429
8519
|
} else {
|
|
@@ -8461,6 +8551,7 @@ async function renderBook(ctx) {
|
|
|
8461
8551
|
htmlFile,
|
|
8462
8552
|
imageRefs,
|
|
8463
8553
|
cssAssets,
|
|
8554
|
+
dropRelativeLinks: format !== "html",
|
|
8464
8555
|
onPlan: ({ unresolved, copyCount }) => {
|
|
8465
8556
|
if (unresolved.length > 0) {
|
|
8466
8557
|
throw new BuildError(`Cannot resolve ${unresolved.length} image reference(s):
|