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
|
@@ -4782,7 +4782,7 @@ import git from "isomorphic-git";
|
|
|
4782
4782
|
// package.json
|
|
4783
4783
|
var package_default = {
|
|
4784
4784
|
name: "gutterpress",
|
|
4785
|
-
version: "0.10.9
|
|
4785
|
+
version: "0.10.9",
|
|
4786
4786
|
description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
|
|
4787
4787
|
author: "itlackey",
|
|
4788
4788
|
license: "MPL-2.0",
|
|
@@ -5145,26 +5145,54 @@ function targetsPageWrapper(selector) {
|
|
|
5145
5145
|
return false;
|
|
5146
5146
|
return !/\bgp-(bleed|pin)\b/.test(selector);
|
|
5147
5147
|
}
|
|
5148
|
+
var cssWideResetKeywords = new Set(["initial", "unset", "revert", "revert-layer"]);
|
|
5149
|
+
var inertValues = {
|
|
5150
|
+
"z-index": new Set(["auto"]),
|
|
5151
|
+
opacity: new Set(["1", "100%"]),
|
|
5152
|
+
"mix-blend-mode": new Set(["normal"]),
|
|
5153
|
+
"background-blend-mode": new Set(["normal"]),
|
|
5154
|
+
filter: new Set(["none"]),
|
|
5155
|
+
"backdrop-filter": new Set(["none"]),
|
|
5156
|
+
transform: new Set(["none"]),
|
|
5157
|
+
rotate: new Set(["none"]),
|
|
5158
|
+
translate: new Set(["none"]),
|
|
5159
|
+
scale: new Set(["none"]),
|
|
5160
|
+
perspective: new Set(["none"]),
|
|
5161
|
+
"box-shadow": new Set(["none"]),
|
|
5162
|
+
outline: new Set(["none"]),
|
|
5163
|
+
"outline-style": new Set(["none"]),
|
|
5164
|
+
"will-change": new Set(["auto", "none"]),
|
|
5165
|
+
"clip-path": new Set(["none"]),
|
|
5166
|
+
transition: new Set(["none"]),
|
|
5167
|
+
animation: new Set(["none"]),
|
|
5168
|
+
"animation-name": new Set(["none"]),
|
|
5169
|
+
overflow: new Set(["visible"]),
|
|
5170
|
+
"overflow-x": new Set(["visible"]),
|
|
5171
|
+
"overflow-y": new Set(["visible"])
|
|
5172
|
+
};
|
|
5173
|
+
function isInertValue(prop, value) {
|
|
5174
|
+
const v = value.trim().toLowerCase();
|
|
5175
|
+
return cssWideResetKeywords.has(v) || (inertValues[prop]?.has(v) ?? false);
|
|
5176
|
+
}
|
|
5148
5177
|
function createsStackingContext(decl) {
|
|
5149
5178
|
const p = decl.prop.toLowerCase();
|
|
5150
5179
|
const v = decl.value.trim().toLowerCase();
|
|
5180
|
+
if (isInertValue(p, v))
|
|
5181
|
+
return false;
|
|
5151
5182
|
switch (p) {
|
|
5152
|
-
case "z-index":
|
|
5153
|
-
return v !== "auto" && v !== "initial" && v !== "unset";
|
|
5154
5183
|
case "isolation":
|
|
5155
5184
|
return v === "isolate";
|
|
5185
|
+
case "contain":
|
|
5186
|
+
return /\b(paint|layout|strict|content)\b/.test(v);
|
|
5187
|
+
case "z-index":
|
|
5156
5188
|
case "opacity":
|
|
5157
|
-
return v !== "1" && v !== "100%" && v !== "initial" && v !== "unset";
|
|
5158
5189
|
case "mix-blend-mode":
|
|
5159
|
-
return v !== "normal" && v !== "initial" && v !== "unset";
|
|
5160
5190
|
case "filter":
|
|
5161
5191
|
case "backdrop-filter":
|
|
5162
5192
|
case "transform":
|
|
5163
5193
|
case "perspective":
|
|
5164
5194
|
case "will-change":
|
|
5165
|
-
return
|
|
5166
|
-
case "contain":
|
|
5167
|
-
return /\b(paint|layout|strict|content)\b/.test(v);
|
|
5195
|
+
return true;
|
|
5168
5196
|
default:
|
|
5169
5197
|
return false;
|
|
5170
5198
|
}
|
|
@@ -5174,7 +5202,7 @@ function clipsDescendants(decl) {
|
|
|
5174
5202
|
if (p !== "overflow" && p !== "overflow-x" && p !== "overflow-y")
|
|
5175
5203
|
return false;
|
|
5176
5204
|
const v = decl.value.trim().toLowerCase();
|
|
5177
|
-
return v !== "" &&
|
|
5205
|
+
return v !== "" && !isInertValue(p, v);
|
|
5178
5206
|
}
|
|
5179
5207
|
function nodeLoc(node) {
|
|
5180
5208
|
return {
|
|
@@ -5215,6 +5243,8 @@ function checkCss(css, from) {
|
|
|
5215
5243
|
root.walkAtRules((at) => reportRemoteUrls(at.params || "", at));
|
|
5216
5244
|
root.walkDecls((decl) => {
|
|
5217
5245
|
const prop = decl.prop.toLowerCase();
|
|
5246
|
+
if (isInertValue(prop, decl.value))
|
|
5247
|
+
return;
|
|
5218
5248
|
if (marginBoxIgnoredProperties.has(prop) && isInPageMarginBox(decl)) {
|
|
5219
5249
|
warnings.push({
|
|
5220
5250
|
rule: ruleRiskyProps,
|
|
@@ -5321,7 +5351,8 @@ async function runLint(opts = {}) {
|
|
|
5321
5351
|
linted++;
|
|
5322
5352
|
const warnings = checkCss(css, file);
|
|
5323
5353
|
const errors = warnings.filter((w) => w.severity === "error");
|
|
5324
|
-
|
|
5354
|
+
const risky = warnings.filter((w) => w.rule === ruleRiskyProps);
|
|
5355
|
+
riskyCount += risky.length;
|
|
5325
5356
|
if (errors.length > 0) {
|
|
5326
5357
|
log.error(` ${file}`);
|
|
5327
5358
|
for (const w of errors) {
|
|
@@ -5329,6 +5360,12 @@ async function runLint(opts = {}) {
|
|
|
5329
5360
|
}
|
|
5330
5361
|
errorCount += errors.length;
|
|
5331
5362
|
}
|
|
5363
|
+
if (risky.length > 0) {
|
|
5364
|
+
log.warn(` ${file}`);
|
|
5365
|
+
for (const w of risky) {
|
|
5366
|
+
log.warn(` ${w.line}:${w.column} ${w.message} (${w.rule})`);
|
|
5367
|
+
}
|
|
5368
|
+
}
|
|
5332
5369
|
}
|
|
5333
5370
|
if (errorCount > 0) {
|
|
5334
5371
|
log.error("CSS lint errors found");
|
|
@@ -5336,7 +5373,6 @@ async function runLint(opts = {}) {
|
|
|
5336
5373
|
}
|
|
5337
5374
|
if (riskyCount > 0) {
|
|
5338
5375
|
log.warn(`${riskyCount} risky print properties found (may cause rasterization)`);
|
|
5339
|
-
log.warn("The validator will check for actual rasterized pages after PDF generation.");
|
|
5340
5376
|
} else {
|
|
5341
5377
|
log.success("CSS lint passed");
|
|
5342
5378
|
}
|
|
@@ -6969,10 +7005,10 @@ registerCheck(check19);
|
|
|
6969
7005
|
|
|
6970
7006
|
// src/checks/source/local-refs.ts
|
|
6971
7007
|
import { existsSync as existsSync9 } from "node:fs";
|
|
6972
|
-
import { readFile as readFile16 } from "node:fs/promises";
|
|
6973
7008
|
import { dirname as dirname3, resolve as resolve7 } from "node:path";
|
|
6974
7009
|
|
|
6975
7010
|
// src/checks/source/local-ref-parser.ts
|
|
7011
|
+
import { readFile as readFile16 } from "node:fs/promises";
|
|
6976
7012
|
function newlineOffsets(content) {
|
|
6977
7013
|
const offsets = [];
|
|
6978
7014
|
for (let i = 0;i < content.length; i++)
|
|
@@ -7017,7 +7053,7 @@ function createRenderedLocalRefCollector(customPlugins) {
|
|
|
7017
7053
|
if (!kind)
|
|
7018
7054
|
continue;
|
|
7019
7055
|
const ref = child.attrGet(kind === "image" ? "src" : "href");
|
|
7020
|
-
if (
|
|
7056
|
+
if (ref === null)
|
|
7021
7057
|
continue;
|
|
7022
7058
|
const offset = tokenOffsets.get(child);
|
|
7023
7059
|
refs.push({
|
|
@@ -7030,6 +7066,26 @@ function createRenderedLocalRefCollector(customPlugins) {
|
|
|
7030
7066
|
return refs;
|
|
7031
7067
|
};
|
|
7032
7068
|
}
|
|
7069
|
+
async function* renderedLocalRefs(ctx, checkId, results) {
|
|
7070
|
+
const files = (ctx.markdownFiles ?? []).slice().sort();
|
|
7071
|
+
if (files.length === 0)
|
|
7072
|
+
return;
|
|
7073
|
+
const plugins = await loadPlugins(ctx.config.extensions, ctx.inputDir, (ref, error2) => {
|
|
7074
|
+
results.push(inspectionFailed(checkId, `Plugin "${ref}" could not be loaded, so local references it defines were not checked: ${error2.message}`));
|
|
7075
|
+
});
|
|
7076
|
+
const collectRenderedLocalRefs = createRenderedLocalRefCollector(plugins);
|
|
7077
|
+
for (const file of files) {
|
|
7078
|
+
let refs;
|
|
7079
|
+
try {
|
|
7080
|
+
refs = collectRenderedLocalRefs(await readFile16(file, "utf8"));
|
|
7081
|
+
} catch {
|
|
7082
|
+
results.push(inspectionFailed(checkId, `Could not read source file: ${file}`, { file }));
|
|
7083
|
+
continue;
|
|
7084
|
+
}
|
|
7085
|
+
for (const ref of refs)
|
|
7086
|
+
yield { ...ref, file };
|
|
7087
|
+
}
|
|
7088
|
+
}
|
|
7033
7089
|
|
|
7034
7090
|
// src/checks/source/local-refs.ts
|
|
7035
7091
|
var check20 = {
|
|
@@ -7039,45 +7095,29 @@ var check20 = {
|
|
|
7039
7095
|
category: "source",
|
|
7040
7096
|
phase: "pre-build",
|
|
7041
7097
|
async run(ctx) {
|
|
7042
|
-
const files = (ctx.markdownFiles ?? []).slice().sort();
|
|
7043
|
-
if (files.length === 0)
|
|
7044
|
-
return [];
|
|
7045
7098
|
const results = [];
|
|
7046
|
-
const
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
const escape = kind === "image" ? proseImageRefError(ref, ctx.inputDir) : null;
|
|
7057
|
-
if (escape) {
|
|
7058
|
-
results.push(finding(check20.id, {
|
|
7059
|
-
severity: "error",
|
|
7060
|
-
message: escape,
|
|
7061
|
-
file,
|
|
7062
|
-
line
|
|
7063
|
-
}));
|
|
7064
|
-
continue;
|
|
7065
|
-
}
|
|
7066
|
-
if (localRefExists(ref, kind, file, ctx.inputDir))
|
|
7067
|
-
continue;
|
|
7068
|
-
results.push(finding(check20.id, {
|
|
7069
|
-
severity: kind === "image" ? "warning" : "error",
|
|
7070
|
-
code: kind === "image" ? "missing-image-placeholder" : undefined,
|
|
7071
|
-
message: kind === "image" ? `Local image not found; build will substitute a magenta placeholder: ${ref}` : `Local reference not found: ${ref}`,
|
|
7072
|
-
file,
|
|
7073
|
-
line
|
|
7074
|
-
}));
|
|
7075
|
-
}
|
|
7076
|
-
} catch {
|
|
7077
|
-
results.push(inspectionFailed(check20.id, `Could not read source file: ${file}`, {
|
|
7078
|
-
file
|
|
7099
|
+
for await (const { ref, kind, line, file } of renderedLocalRefs(ctx, check20.id, results)) {
|
|
7100
|
+
if (isNonFilesystemRef(ref))
|
|
7101
|
+
continue;
|
|
7102
|
+
const escape = kind === "image" ? proseImageRefError(ref, ctx.inputDir) : null;
|
|
7103
|
+
if (escape) {
|
|
7104
|
+
results.push(finding(check20.id, {
|
|
7105
|
+
severity: "error",
|
|
7106
|
+
message: escape,
|
|
7107
|
+
file,
|
|
7108
|
+
line
|
|
7079
7109
|
}));
|
|
7110
|
+
continue;
|
|
7080
7111
|
}
|
|
7112
|
+
if (localRefExists(ref, kind, file, ctx.inputDir))
|
|
7113
|
+
continue;
|
|
7114
|
+
results.push(finding(check20.id, {
|
|
7115
|
+
severity: kind === "image" ? "warning" : "error",
|
|
7116
|
+
code: kind === "image" ? "missing-image-placeholder" : undefined,
|
|
7117
|
+
message: kind === "image" ? `Local image not found; build will substitute a magenta placeholder: ${ref}` : `Local reference not found: ${ref}`,
|
|
7118
|
+
file,
|
|
7119
|
+
line
|
|
7120
|
+
}));
|
|
7081
7121
|
}
|
|
7082
7122
|
return results;
|
|
7083
7123
|
}
|
|
@@ -7098,70 +7138,359 @@ function localRefExists(ref, kind, sourceFile, projectRoot) {
|
|
|
7098
7138
|
}
|
|
7099
7139
|
registerCheck(check20);
|
|
7100
7140
|
|
|
7101
|
-
// src/
|
|
7102
|
-
import
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7141
|
+
// src/lib/build-staging.ts
|
|
7142
|
+
import path7 from "node:path";
|
|
7143
|
+
import os from "node:os";
|
|
7144
|
+
import fsp from "node:fs/promises";
|
|
7145
|
+
|
|
7146
|
+
// src/lib/missing-asset-placeholder.ts
|
|
7147
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
7148
|
+
import { deflateSync } from "node:zlib";
|
|
7149
|
+
function placeholderOutputPath(missingOutputPath) {
|
|
7150
|
+
const hash = createHash5("sha256").update(missingOutputPath).digest("hex").slice(0, 16);
|
|
7151
|
+
return `assets/gutterpress-missing/${hash}.png`;
|
|
7152
|
+
}
|
|
7153
|
+
function decodeHtmlAttribute(value) {
|
|
7154
|
+
return value.replace(/"/g, '"').replace(/'|'/g, "'").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
7155
|
+
}
|
|
7156
|
+
function rewriteMissingImageReferences(html, replacements) {
|
|
7157
|
+
if (replacements.size === 0)
|
|
7158
|
+
return html;
|
|
7159
|
+
const replacementFor = (raw) => replacements.get(raw) ?? replacements.get(decodeHtmlAttribute(raw));
|
|
7160
|
+
const cssUrl = /url\(\s*(?:"([^"]*)"|'([^']*)'|"((?:(?!").)*)"|([^'"()\s]*))\s*\)/giy;
|
|
7161
|
+
const rewriteCssUrls = (css) => {
|
|
7162
|
+
let out = "";
|
|
7163
|
+
let copiedThrough = 0;
|
|
7164
|
+
let index = 0;
|
|
7165
|
+
while (index < css.length) {
|
|
7166
|
+
if (css.startsWith("/*", index)) {
|
|
7167
|
+
const end = css.indexOf("*/", index + 2);
|
|
7168
|
+
index = end < 0 ? css.length : end + 2;
|
|
7169
|
+
continue;
|
|
7170
|
+
}
|
|
7171
|
+
const char = css[index];
|
|
7172
|
+
if (char === '"' || char === "'") {
|
|
7173
|
+
const quote = char;
|
|
7174
|
+
index++;
|
|
7175
|
+
while (index < css.length) {
|
|
7176
|
+
if (css[index] === "\\") {
|
|
7177
|
+
index += 2;
|
|
7124
7178
|
continue;
|
|
7125
7179
|
}
|
|
7126
|
-
|
|
7180
|
+
const current = css[index++];
|
|
7181
|
+
if (current === quote)
|
|
7182
|
+
break;
|
|
7183
|
+
}
|
|
7184
|
+
continue;
|
|
7185
|
+
}
|
|
7186
|
+
const previous = index > 0 ? css[index - 1] : "";
|
|
7187
|
+
if (!/[a-z0-9_-]/i.test(previous) && css.slice(index, index + 3).toLowerCase() === "url") {
|
|
7188
|
+
cssUrl.lastIndex = index;
|
|
7189
|
+
const match = cssUrl.exec(css);
|
|
7190
|
+
if (match) {
|
|
7191
|
+
const raw = match[1] ?? match[2] ?? match[3] ?? match[4] ?? "";
|
|
7192
|
+
const replacement = replacementFor(raw);
|
|
7193
|
+
if (replacement) {
|
|
7194
|
+
out += css.slice(copiedThrough, index);
|
|
7195
|
+
out += match[3] !== undefined ? `url("${replacement}")` : `url("${replacement}")`;
|
|
7196
|
+
index = cssUrl.lastIndex;
|
|
7197
|
+
copiedThrough = index;
|
|
7127
7198
|
continue;
|
|
7128
|
-
for (const alt of extractImageAlts(line)) {
|
|
7129
|
-
if (alt.trim())
|
|
7130
|
-
continue;
|
|
7131
|
-
results.push({
|
|
7132
|
-
checkId: check21.id,
|
|
7133
|
-
severity: "warning",
|
|
7134
|
-
message: "Image is missing alt text",
|
|
7135
|
-
file,
|
|
7136
|
-
line: i + 1
|
|
7137
|
-
});
|
|
7138
7199
|
}
|
|
7139
7200
|
}
|
|
7140
|
-
}
|
|
7201
|
+
}
|
|
7202
|
+
index++;
|
|
7141
7203
|
}
|
|
7142
|
-
return
|
|
7204
|
+
return copiedThrough === 0 ? css : out + css.slice(copiedThrough);
|
|
7205
|
+
};
|
|
7206
|
+
const rewriteTag = (tag) => {
|
|
7207
|
+
let out = tag;
|
|
7208
|
+
if (/^<img\b/i.test(tag)) {
|
|
7209
|
+
out = out.replace(/(\s+src\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi, (whole, prefix, double, single, bare) => {
|
|
7210
|
+
const raw = double ?? single ?? bare ?? "";
|
|
7211
|
+
const replacement = replacementFor(raw);
|
|
7212
|
+
if (!replacement)
|
|
7213
|
+
return whole;
|
|
7214
|
+
if (double !== undefined)
|
|
7215
|
+
return `${prefix}"${replacement}"`;
|
|
7216
|
+
if (single !== undefined)
|
|
7217
|
+
return `${prefix}'${replacement}'`;
|
|
7218
|
+
return `${prefix}${replacement}`;
|
|
7219
|
+
});
|
|
7220
|
+
}
|
|
7221
|
+
if (/^<(?:img|source)\b/i.test(tag)) {
|
|
7222
|
+
out = out.replace(/(\s+srcset\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi, (whole, prefix, double, single, bare) => {
|
|
7223
|
+
const value = double ?? single ?? bare ?? "";
|
|
7224
|
+
let rewritten = "";
|
|
7225
|
+
let copiedThrough = 0;
|
|
7226
|
+
let changed = false;
|
|
7227
|
+
for (const candidate of parseSrcsetUrlCandidates(value)) {
|
|
7228
|
+
const replacement = replacementFor(candidate.url);
|
|
7229
|
+
if (!replacement)
|
|
7230
|
+
continue;
|
|
7231
|
+
rewritten += value.slice(copiedThrough, candidate.start) + replacement;
|
|
7232
|
+
copiedThrough = candidate.end;
|
|
7233
|
+
changed = true;
|
|
7234
|
+
}
|
|
7235
|
+
if (!changed)
|
|
7236
|
+
return whole;
|
|
7237
|
+
rewritten += value.slice(copiedThrough);
|
|
7238
|
+
if (double !== undefined)
|
|
7239
|
+
return `${prefix}"${rewritten}"`;
|
|
7240
|
+
if (single !== undefined)
|
|
7241
|
+
return `${prefix}'${rewritten}'`;
|
|
7242
|
+
return `${prefix}${rewritten}`;
|
|
7243
|
+
});
|
|
7244
|
+
}
|
|
7245
|
+
out = out.replace(/(\s+style\s*=\s*)(["'])(.*?)\2/gi, (whole, prefix, quote, value) => {
|
|
7246
|
+
const rewritten = rewriteCssUrls(value);
|
|
7247
|
+
return rewritten === value ? whole : `${prefix}${quote}${rewritten}${quote}`;
|
|
7248
|
+
});
|
|
7249
|
+
return out;
|
|
7250
|
+
};
|
|
7251
|
+
return rewriteActiveHtml(html, rewriteTag, (region, element) => {
|
|
7252
|
+
if (element !== "style")
|
|
7253
|
+
return region;
|
|
7254
|
+
const style = /^(<style\b[^>]*>)([\s\S]*)(<\/style\s*>)$/i.exec(region);
|
|
7255
|
+
return style ? `${rewriteTag(style[1])}${rewriteCssUrls(style[2])}${style[3]}` : region;
|
|
7256
|
+
});
|
|
7257
|
+
}
|
|
7258
|
+
function rewriteActiveHtml(html, rewriteTag, rewriteProtected = (region) => region) {
|
|
7259
|
+
const rewriteTags = (active) => active.replace(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g, rewriteTag);
|
|
7260
|
+
const protectedRegion = /<!--[\s\S]*?-->|<(script|style|pre|code|textarea)\b[^>]*>[\s\S]*?<\/\1\s*>/gi;
|
|
7261
|
+
let out = "";
|
|
7262
|
+
let last = 0;
|
|
7263
|
+
for (const match of html.matchAll(protectedRegion)) {
|
|
7264
|
+
out += rewriteTags(html.slice(last, match.index));
|
|
7265
|
+
out += rewriteProtected(match[0], match[1]?.toLowerCase());
|
|
7266
|
+
last = (match.index ?? 0) + match[0].length;
|
|
7143
7267
|
}
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7268
|
+
return out + rewriteTags(html.slice(last));
|
|
7269
|
+
}
|
|
7270
|
+
function crc32(buf) {
|
|
7271
|
+
let c = ~0;
|
|
7272
|
+
for (let i = 0;i < buf.length; i++) {
|
|
7273
|
+
c ^= buf[i];
|
|
7274
|
+
for (let k = 0;k < 8; k++)
|
|
7275
|
+
c = c >>> 1 ^ 3988292384 & -(c & 1);
|
|
7151
7276
|
}
|
|
7152
|
-
|
|
7153
|
-
|
|
7277
|
+
return ~c >>> 0;
|
|
7278
|
+
}
|
|
7279
|
+
function chunk(type, data) {
|
|
7280
|
+
const out = new Uint8Array(data.length + 12);
|
|
7281
|
+
const view = new DataView(out.buffer);
|
|
7282
|
+
view.setUint32(0, data.length);
|
|
7283
|
+
for (let i = 0;i < 4; i++)
|
|
7284
|
+
out[4 + i] = type.charCodeAt(i);
|
|
7285
|
+
out.set(data, 8);
|
|
7286
|
+
view.setUint32(data.length + 8, crc32(out.subarray(4, data.length + 8)));
|
|
7287
|
+
return out;
|
|
7288
|
+
}
|
|
7289
|
+
function placeholderPng(width = 640, height = 480, cell = 32) {
|
|
7290
|
+
const A = [217, 70, 239];
|
|
7291
|
+
const B = [26, 26, 26];
|
|
7292
|
+
const raw = new Uint8Array(height * (1 + width * 3));
|
|
7293
|
+
let p = 0;
|
|
7294
|
+
for (let y = 0;y < height; y++) {
|
|
7295
|
+
raw[p++] = 0;
|
|
7296
|
+
for (let x = 0;x < width; x++) {
|
|
7297
|
+
const c = ((x / cell | 0) + (y / cell | 0)) % 2 === 0 ? A : B;
|
|
7298
|
+
raw[p++] = c[0];
|
|
7299
|
+
raw[p++] = c[1];
|
|
7300
|
+
raw[p++] = c[2];
|
|
7301
|
+
}
|
|
7154
7302
|
}
|
|
7155
|
-
|
|
7303
|
+
const ihdr = new Uint8Array(13);
|
|
7304
|
+
const hv = new DataView(ihdr.buffer);
|
|
7305
|
+
hv.setUint32(0, width);
|
|
7306
|
+
hv.setUint32(4, height);
|
|
7307
|
+
ihdr[8] = 8;
|
|
7308
|
+
ihdr[9] = 2;
|
|
7309
|
+
const parts = [
|
|
7310
|
+
new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
|
|
7311
|
+
chunk("IHDR", ihdr),
|
|
7312
|
+
chunk("IDAT", new Uint8Array(deflateSync(raw))),
|
|
7313
|
+
chunk("IEND", new Uint8Array(0))
|
|
7314
|
+
];
|
|
7315
|
+
const total = parts.reduce((n, x) => n + x.length, 0);
|
|
7316
|
+
const png = new Uint8Array(total);
|
|
7317
|
+
let o = 0;
|
|
7318
|
+
for (const part of parts) {
|
|
7319
|
+
png.set(part, o);
|
|
7320
|
+
o += part.length;
|
|
7321
|
+
}
|
|
7322
|
+
return png;
|
|
7156
7323
|
}
|
|
7157
|
-
registerCheck(check21);
|
|
7158
7324
|
|
|
7159
|
-
// src/
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7325
|
+
// src/lib/build-staging.ts
|
|
7326
|
+
async function shipViewerHtml(htmlFile, outDir) {
|
|
7327
|
+
await fsp.mkdir(path7.join(outDir, "engine"), { recursive: true });
|
|
7328
|
+
await fsp.copyFile(await getAssetPath("engine/gutterpress-viewer.js"), path7.join(outDir, "engine/gutterpress-viewer.js"));
|
|
7329
|
+
const tag = ` <script src="engine/gutterpress-viewer.js"></script>
|
|
7330
|
+
`;
|
|
7331
|
+
const html = await fsp.readFile(htmlFile, "utf-8");
|
|
7332
|
+
await fsp.writeFile(htmlFile, /<\/head>/i.test(html) ? html.replace(/<\/head>/i, tag + "</head>") : tag + html, "utf-8");
|
|
7333
|
+
}
|
|
7334
|
+
async function stageBookAssets(options) {
|
|
7335
|
+
const { renderDir, outDir, htmlFile, imageRefs, cssAssets, onPlan, dropRelativeLinks } = options;
|
|
7336
|
+
const { copies: imageCopies, errors, destinations } = await planImageCopies(renderDir, imageRefs);
|
|
7337
|
+
const copies = [...cssAssets, ...imageCopies];
|
|
7338
|
+
onPlan?.({ unresolved: errors, copyCount: copies.length });
|
|
7339
|
+
const missingPlaceholders = copies.length ? await copyReferencedAssets(copies, outDir) : new Map;
|
|
7340
|
+
const rendered = await fsp.readFile(htmlFile, "utf8");
|
|
7341
|
+
let staged = rendered;
|
|
7342
|
+
if (missingPlaceholders.size > 0) {
|
|
7343
|
+
const rewrites = new Map(missingPlaceholders);
|
|
7344
|
+
for (const [ref, dest] of destinations) {
|
|
7345
|
+
const placeholder = missingPlaceholders.get(dest);
|
|
7346
|
+
if (placeholder)
|
|
7347
|
+
rewrites.set(ref, placeholder);
|
|
7348
|
+
}
|
|
7349
|
+
staged = rewriteMissingImageReferences(staged, rewrites);
|
|
7350
|
+
}
|
|
7351
|
+
if (dropRelativeLinks)
|
|
7352
|
+
staged = dropRelativeLinkHrefs(staged);
|
|
7353
|
+
if (staged.includes("--gp-shape:"))
|
|
7354
|
+
staged = await inlineShapeUrls(staged, outDir);
|
|
7355
|
+
if (staged !== rendered)
|
|
7356
|
+
await fsp.writeFile(htmlFile, staged, "utf8");
|
|
7357
|
+
return { missing: [...missingPlaceholders.keys()].sort() };
|
|
7358
|
+
}
|
|
7359
|
+
async function copyReferencedAssets(copies, outDir) {
|
|
7360
|
+
const dirs = new Set(copies.map((c) => path7.dirname(path7.resolve(outDir, c.to))));
|
|
7361
|
+
await Promise.all([...dirs].map((d) => fsp.mkdir(d, { recursive: true })));
|
|
7362
|
+
const missing = new Map;
|
|
7363
|
+
await Promise.all(copies.map(async (c) => {
|
|
7364
|
+
const dest = path7.resolve(outDir, c.to);
|
|
7365
|
+
try {
|
|
7366
|
+
await fsp.copyFile(c.from, dest);
|
|
7367
|
+
} catch (err) {
|
|
7368
|
+
if (err?.code === "ENOENT") {
|
|
7369
|
+
const placeholder = placeholderOutputPath(c.to);
|
|
7370
|
+
const placeholderDest = path7.resolve(outDir, placeholder);
|
|
7371
|
+
await fsp.mkdir(path7.dirname(placeholderDest), { recursive: true });
|
|
7372
|
+
await fsp.writeFile(placeholderDest, placeholderPng());
|
|
7373
|
+
missing.set(c.to, placeholder);
|
|
7374
|
+
return;
|
|
7375
|
+
}
|
|
7376
|
+
throw new BuildError(`Could not copy asset ${c.from} → ${c.to}: ` + (err instanceof Error ? err.message : String(err)), 1);
|
|
7377
|
+
}
|
|
7378
|
+
}));
|
|
7379
|
+
return missing;
|
|
7380
|
+
}
|
|
7381
|
+
function isPrintResolvableHref(href) {
|
|
7382
|
+
const value = href.trim();
|
|
7383
|
+
if (value.startsWith("#"))
|
|
7384
|
+
return true;
|
|
7385
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(value)?.[1]?.toLowerCase();
|
|
7386
|
+
return scheme !== undefined && scheme !== "file";
|
|
7387
|
+
}
|
|
7388
|
+
function dropRelativeLinkHrefs(html) {
|
|
7389
|
+
const hrefAttr = /\s+href\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i;
|
|
7390
|
+
return rewriteActiveHtml(html, (tag) => {
|
|
7391
|
+
if (!/^<a(?=[\s/>])/i.test(tag))
|
|
7392
|
+
return tag;
|
|
7393
|
+
const m = hrefAttr.exec(tag);
|
|
7394
|
+
if (!m)
|
|
7395
|
+
return tag;
|
|
7396
|
+
const href = decodeHtmlAttribute(m[1] ?? m[2] ?? m[3] ?? "");
|
|
7397
|
+
return isPrintResolvableHref(href) ? tag : tag.replace(hrefAttr, "");
|
|
7398
|
+
});
|
|
7399
|
+
}
|
|
7400
|
+
async function createStageRoot() {
|
|
7401
|
+
return fsp.mkdtemp(path7.join(os.tmpdir(), "gutterpress-stage-"));
|
|
7402
|
+
}
|
|
7403
|
+
|
|
7404
|
+
// src/checks/source/dangling-links.ts
|
|
7405
|
+
var check21 = {
|
|
7406
|
+
id: "source.links.dangling",
|
|
7407
|
+
name: "Dangling Links",
|
|
7408
|
+
description: "Flags relative links the printed book cannot open (their href is dropped at print time)",
|
|
7409
|
+
category: "source",
|
|
7410
|
+
phase: "pre-build",
|
|
7411
|
+
async run(ctx) {
|
|
7412
|
+
const results = [];
|
|
7413
|
+
for await (const { ref, kind, line, file } of renderedLocalRefs(ctx, check21.id, results)) {
|
|
7414
|
+
if (kind !== "link" || isPrintResolvableHref(ref))
|
|
7415
|
+
continue;
|
|
7416
|
+
results.push(finding(check21.id, {
|
|
7417
|
+
severity: "warning",
|
|
7418
|
+
code: "dangling-link",
|
|
7419
|
+
data: { ref },
|
|
7420
|
+
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.`,
|
|
7421
|
+
file,
|
|
7422
|
+
line
|
|
7423
|
+
}));
|
|
7424
|
+
}
|
|
7425
|
+
return results;
|
|
7426
|
+
}
|
|
7427
|
+
};
|
|
7428
|
+
registerCheck(check21);
|
|
7429
|
+
|
|
7430
|
+
// src/checks/source/accessibility-alt-text.ts
|
|
7431
|
+
import { readFile as readFile17 } from "node:fs/promises";
|
|
7432
|
+
var check22 = {
|
|
7433
|
+
id: "source.accessibility.alt-text",
|
|
7434
|
+
name: "Image Alt Text",
|
|
7435
|
+
description: "Checks markdown images include non-empty alt text",
|
|
7436
|
+
category: "source",
|
|
7437
|
+
phase: "pre-build",
|
|
7438
|
+
async run(ctx) {
|
|
7439
|
+
const files = (ctx.markdownFiles ?? []).slice().sort();
|
|
7440
|
+
if (files.length === 0)
|
|
7441
|
+
return [];
|
|
7442
|
+
const results = [];
|
|
7443
|
+
for (const file of files) {
|
|
7444
|
+
try {
|
|
7445
|
+
const content = await readFile17(file, "utf8");
|
|
7446
|
+
const lines = content.split(`
|
|
7447
|
+
`);
|
|
7448
|
+
let inFence = false;
|
|
7449
|
+
for (let i = 0;i < lines.length; i++) {
|
|
7450
|
+
const line = lines[i] ?? "";
|
|
7451
|
+
if (/^\s*(```|~~~)/.test(line)) {
|
|
7452
|
+
inFence = !inFence;
|
|
7453
|
+
continue;
|
|
7454
|
+
}
|
|
7455
|
+
if (inFence)
|
|
7456
|
+
continue;
|
|
7457
|
+
for (const alt of extractImageAlts(line)) {
|
|
7458
|
+
if (alt.trim())
|
|
7459
|
+
continue;
|
|
7460
|
+
results.push({
|
|
7461
|
+
checkId: check22.id,
|
|
7462
|
+
severity: "warning",
|
|
7463
|
+
message: "Image is missing alt text",
|
|
7464
|
+
file,
|
|
7465
|
+
line: i + 1
|
|
7466
|
+
});
|
|
7467
|
+
}
|
|
7468
|
+
}
|
|
7469
|
+
} catch {}
|
|
7470
|
+
}
|
|
7471
|
+
return results;
|
|
7472
|
+
}
|
|
7473
|
+
};
|
|
7474
|
+
function extractImageAlts(line) {
|
|
7475
|
+
const alts = [];
|
|
7476
|
+
const inlinePattern = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
|
7477
|
+
const referencePattern = /!\[([^\]]*)\]\[[^\]]*\]/g;
|
|
7478
|
+
for (const match of line.matchAll(inlinePattern)) {
|
|
7479
|
+
alts.push(match[1] ?? "");
|
|
7480
|
+
}
|
|
7481
|
+
for (const match of line.matchAll(referencePattern)) {
|
|
7482
|
+
alts.push(match[1] ?? "");
|
|
7483
|
+
}
|
|
7484
|
+
return alts;
|
|
7485
|
+
}
|
|
7486
|
+
registerCheck(check22);
|
|
7487
|
+
|
|
7488
|
+
// src/checks/source/accessibility-heading-order.ts
|
|
7489
|
+
import { readFile as readFile18 } from "node:fs/promises";
|
|
7490
|
+
var check23 = {
|
|
7491
|
+
id: "source.accessibility.heading-order",
|
|
7492
|
+
name: "Heading Order",
|
|
7493
|
+
description: "Checks markdown heading levels do not jump by more than one",
|
|
7165
7494
|
category: "source",
|
|
7166
7495
|
phase: "pre-build",
|
|
7167
7496
|
async run(ctx) {
|
|
@@ -7190,7 +7519,7 @@ var check22 = {
|
|
|
7190
7519
|
const level = match[1].length;
|
|
7191
7520
|
if (prevLevel != null && level > prevLevel + 1) {
|
|
7192
7521
|
results.push({
|
|
7193
|
-
checkId:
|
|
7522
|
+
checkId: check23.id,
|
|
7194
7523
|
severity: "warning",
|
|
7195
7524
|
message: `Heading level jump from h${prevLevel} to h${level}`,
|
|
7196
7525
|
file,
|
|
@@ -7204,11 +7533,11 @@ var check22 = {
|
|
|
7204
7533
|
return results;
|
|
7205
7534
|
}
|
|
7206
7535
|
};
|
|
7207
|
-
registerCheck(
|
|
7536
|
+
registerCheck(check23);
|
|
7208
7537
|
|
|
7209
7538
|
// src/checks/source/layout-markers.ts
|
|
7210
7539
|
import { readFile as readFile19 } from "node:fs/promises";
|
|
7211
|
-
var
|
|
7540
|
+
var check24 = {
|
|
7212
7541
|
id: "source.markdown.layout-markers",
|
|
7213
7542
|
name: "Layout Markers",
|
|
7214
7543
|
description: "Reports @page/@section/@chapter marker arguments Gutterpress could not understand, plus any unknown gp-* class",
|
|
@@ -7220,7 +7549,7 @@ var check23 = {
|
|
|
7220
7549
|
return [];
|
|
7221
7550
|
const results = [];
|
|
7222
7551
|
const plugins = await loadPlugins(ctx.config.extensions, ctx.inputDir, (ref, error2) => {
|
|
7223
|
-
results.push(inspectionFailed(
|
|
7552
|
+
results.push(inspectionFailed(check24.id, `Plugin "${ref}" could not be loaded, so markers it defines were not checked: ${error2.message}`));
|
|
7224
7553
|
});
|
|
7225
7554
|
const md = createMarkdownRenderer(plugins);
|
|
7226
7555
|
for (const file of files) {
|
|
@@ -7229,7 +7558,7 @@ var check23 = {
|
|
|
7229
7558
|
const env = {};
|
|
7230
7559
|
md.render(content, env);
|
|
7231
7560
|
for (const w of env.layoutWarnings ?? []) {
|
|
7232
|
-
results.push(finding(
|
|
7561
|
+
results.push(finding(check24.id, {
|
|
7233
7562
|
severity: "warning",
|
|
7234
7563
|
message: w.message,
|
|
7235
7564
|
file,
|
|
@@ -7238,17 +7567,17 @@ var check23 = {
|
|
|
7238
7567
|
}));
|
|
7239
7568
|
}
|
|
7240
7569
|
} catch (error2) {
|
|
7241
|
-
results.push(inspectionFailed(
|
|
7570
|
+
results.push(inspectionFailed(check24.id, `Could not check layout markers in ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`, { file }));
|
|
7242
7571
|
}
|
|
7243
7572
|
}
|
|
7244
7573
|
return results;
|
|
7245
7574
|
}
|
|
7246
7575
|
};
|
|
7247
|
-
registerCheck(
|
|
7576
|
+
registerCheck(check24);
|
|
7248
7577
|
|
|
7249
7578
|
// src/checks/source/merge-markers.ts
|
|
7250
7579
|
import { readFile as readFile20 } from "node:fs/promises";
|
|
7251
|
-
import
|
|
7580
|
+
import path8 from "node:path";
|
|
7252
7581
|
|
|
7253
7582
|
// src/checks/asset/extensions.ts
|
|
7254
7583
|
var RASTER_INSPECTABLE_EXTS = [
|
|
@@ -7277,7 +7606,7 @@ var CLOSE_SENTINEL = ">>>>>>> online version";
|
|
|
7277
7606
|
function withoutOnlineTag(relPath) {
|
|
7278
7607
|
return relPath.replace(/\.online(?=\.[^./]*$|$)/, "");
|
|
7279
7608
|
}
|
|
7280
|
-
var
|
|
7609
|
+
var check25 = {
|
|
7281
7610
|
id: "source.sync.merge-markers",
|
|
7282
7611
|
name: "Combined Versions",
|
|
7283
7612
|
description: "Finds passages and files still holding two versions (yours and the online copy) after a sync",
|
|
@@ -7291,7 +7620,7 @@ var check24 = {
|
|
|
7291
7620
|
try {
|
|
7292
7621
|
content = await readFile20(file, "utf8");
|
|
7293
7622
|
} catch {
|
|
7294
|
-
results.push(inspectionFailed(
|
|
7623
|
+
results.push(inspectionFailed(check25.id, `Could not read source file: ${file}`, { file }));
|
|
7295
7624
|
continue;
|
|
7296
7625
|
}
|
|
7297
7626
|
if (!content.includes(OPEN_SENTINEL) && !content.includes(CLOSE_SENTINEL))
|
|
@@ -7303,7 +7632,7 @@ var check24 = {
|
|
|
7303
7632
|
const line = lines[i].endsWith("\r") ? lines[i].slice(0, -1) : lines[i];
|
|
7304
7633
|
if (line === OPEN_SENTINEL) {
|
|
7305
7634
|
inBlock = true;
|
|
7306
|
-
results.push(finding(
|
|
7635
|
+
results.push(finding(check25.id, {
|
|
7307
7636
|
severity: "error",
|
|
7308
7637
|
code: "two-versions-passage",
|
|
7309
7638
|
message: "This passage has two versions (yours and the online copy) — keep what you want, then delete the marker lines.",
|
|
@@ -7312,7 +7641,7 @@ var check24 = {
|
|
|
7312
7641
|
}));
|
|
7313
7642
|
} else if (line === CLOSE_SENTINEL) {
|
|
7314
7643
|
if (!inBlock) {
|
|
7315
|
-
results.push(finding(
|
|
7644
|
+
results.push(finding(check25.id, {
|
|
7316
7645
|
severity: "error",
|
|
7317
7646
|
code: "leftover-version-marker",
|
|
7318
7647
|
message: "This marker line is left over from combining two versions — delete it.",
|
|
@@ -7332,8 +7661,8 @@ var check24 = {
|
|
|
7332
7661
|
ignore: [...ASSET_SCAN_IGNORE_GLOBS]
|
|
7333
7662
|
});
|
|
7334
7663
|
for (const sibling of siblings.sort()) {
|
|
7335
|
-
const rel =
|
|
7336
|
-
results.push(finding(
|
|
7664
|
+
const rel = path8.relative(ctx.inputDir, sibling).split(path8.sep).join("/");
|
|
7665
|
+
results.push(finding(check25.id, {
|
|
7337
7666
|
severity: "warning",
|
|
7338
7667
|
code: "kept-both-versions",
|
|
7339
7668
|
message: `Two versions of ${withoutOnlineTag(rel)} are in your project — keep the one you want, then delete the other.`,
|
|
@@ -7341,12 +7670,12 @@ var check24 = {
|
|
|
7341
7670
|
}));
|
|
7342
7671
|
}
|
|
7343
7672
|
} catch (error2) {
|
|
7344
|
-
results.push(inspectionFailed(
|
|
7673
|
+
results.push(inspectionFailed(check25.id, `Could not scan for kept-both files: ${error2 instanceof Error ? error2.message : String(error2)}`));
|
|
7345
7674
|
}
|
|
7346
7675
|
return results;
|
|
7347
7676
|
}
|
|
7348
7677
|
};
|
|
7349
|
-
registerCheck(
|
|
7678
|
+
registerCheck(check25);
|
|
7350
7679
|
|
|
7351
7680
|
// src/checks/asset/image-file-size.ts
|
|
7352
7681
|
import { stat as stat3 } from "node:fs/promises";
|
|
@@ -7355,24 +7684,24 @@ import { stat as stat3 } from "node:fs/promises";
|
|
|
7355
7684
|
import { stat as stat2, readFile as readFile21 } from "node:fs/promises";
|
|
7356
7685
|
var DEFAULT_DPI = 72;
|
|
7357
7686
|
var cache = new Map;
|
|
7358
|
-
async function inspectImage(
|
|
7687
|
+
async function inspectImage(path9) {
|
|
7359
7688
|
let st;
|
|
7360
7689
|
try {
|
|
7361
|
-
st = await stat2(
|
|
7690
|
+
st = await stat2(path9);
|
|
7362
7691
|
} catch {
|
|
7363
7692
|
return null;
|
|
7364
7693
|
}
|
|
7365
|
-
const hit = cache.get(
|
|
7694
|
+
const hit = cache.get(path9);
|
|
7366
7695
|
if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size)
|
|
7367
7696
|
return hit.info;
|
|
7368
7697
|
let info2 = null;
|
|
7369
7698
|
try {
|
|
7370
|
-
const buf = await readFile21(
|
|
7699
|
+
const buf = await readFile21(path9);
|
|
7371
7700
|
info2 = parseImage(buf);
|
|
7372
7701
|
} catch {
|
|
7373
7702
|
info2 = null;
|
|
7374
7703
|
}
|
|
7375
|
-
cache.set(
|
|
7704
|
+
cache.set(path9, { mtimeMs: st.mtimeMs, size: st.size, info: info2 });
|
|
7376
7705
|
return info2;
|
|
7377
7706
|
}
|
|
7378
7707
|
function parseImage(b) {
|
|
@@ -7572,7 +7901,7 @@ async function collectImageFiles(dirs, exts, ignore = ASSET_SCAN_IGNORE_GLOBS) {
|
|
|
7572
7901
|
}
|
|
7573
7902
|
|
|
7574
7903
|
// src/checks/asset/image-file-size.ts
|
|
7575
|
-
var
|
|
7904
|
+
var check26 = {
|
|
7576
7905
|
id: "asset.image.file-size",
|
|
7577
7906
|
name: "Image File Size",
|
|
7578
7907
|
description: "Checks that image files do not exceed the maximum size limit",
|
|
@@ -7593,14 +7922,14 @@ var check25 = {
|
|
|
7593
7922
|
if (info2.size > maxSize) {
|
|
7594
7923
|
const sizeMb = (info2.size / 1e6).toFixed(1);
|
|
7595
7924
|
const maxMb = (maxSize / 1e6).toFixed(1);
|
|
7596
|
-
results.push(finding(
|
|
7925
|
+
results.push(finding(check26.id, {
|
|
7597
7926
|
severity: "warning",
|
|
7598
7927
|
message: `Image file too large: ${sizeMb}MB (max ${maxMb}MB)`,
|
|
7599
7928
|
file
|
|
7600
7929
|
}));
|
|
7601
7930
|
}
|
|
7602
7931
|
} catch {
|
|
7603
|
-
results.push(inspectionFailed(
|
|
7932
|
+
results.push(inspectionFailed(check26.id, `Could not stat image file: ${file}`, {
|
|
7604
7933
|
file
|
|
7605
7934
|
}));
|
|
7606
7935
|
}
|
|
@@ -7608,10 +7937,10 @@ var check25 = {
|
|
|
7608
7937
|
return results;
|
|
7609
7938
|
}
|
|
7610
7939
|
};
|
|
7611
|
-
registerCheck(
|
|
7940
|
+
registerCheck(check26);
|
|
7612
7941
|
|
|
7613
7942
|
// src/checks/asset/image-resolution.ts
|
|
7614
|
-
var
|
|
7943
|
+
var check27 = {
|
|
7615
7944
|
id: "asset.image.resolution",
|
|
7616
7945
|
name: "Image Resolution",
|
|
7617
7946
|
description: "Checks source image DPI from embedded density metadata",
|
|
@@ -7633,7 +7962,7 @@ var check26 = {
|
|
|
7633
7962
|
const { xDpi, yDpi } = info2;
|
|
7634
7963
|
if (xDpi > 0 && yDpi > 0 && (xDpi < minDpi || yDpi < minDpi)) {
|
|
7635
7964
|
results.push({
|
|
7636
|
-
checkId:
|
|
7965
|
+
checkId: check27.id,
|
|
7637
7966
|
severity: "warning",
|
|
7638
7967
|
message: `Image resolution too low: ${xDpi}x${yDpi} DPI (minimum ${minDpi} DPI)`,
|
|
7639
7968
|
file
|
|
@@ -7643,7 +7972,7 @@ var check26 = {
|
|
|
7643
7972
|
return results;
|
|
7644
7973
|
}
|
|
7645
7974
|
};
|
|
7646
|
-
registerCheck(
|
|
7975
|
+
registerCheck(check27);
|
|
7647
7976
|
|
|
7648
7977
|
// src/checks/asset/image-color-space.ts
|
|
7649
7978
|
var LABEL = {
|
|
@@ -7651,7 +7980,7 @@ var LABEL = {
|
|
|
7651
7980
|
gray: "Gray",
|
|
7652
7981
|
cmyk: "CMYK"
|
|
7653
7982
|
};
|
|
7654
|
-
var
|
|
7983
|
+
var check28 = {
|
|
7655
7984
|
id: "asset.image.color-space",
|
|
7656
7985
|
name: "Image Color Space",
|
|
7657
7986
|
description: "Validates image color spaces against allowed list",
|
|
@@ -7672,7 +8001,7 @@ var check27 = {
|
|
|
7672
8001
|
const cs = info2?.colorSpace;
|
|
7673
8002
|
if (cs && !allowedLower.has(cs)) {
|
|
7674
8003
|
results.push({
|
|
7675
|
-
checkId:
|
|
8004
|
+
checkId: check28.id,
|
|
7676
8005
|
severity: "warning",
|
|
7677
8006
|
message: `Image uses ${LABEL[cs] ?? cs} color space (allowed: ${allowed.join(", ")})`,
|
|
7678
8007
|
file
|
|
@@ -7682,10 +8011,10 @@ var check27 = {
|
|
|
7682
8011
|
return results;
|
|
7683
8012
|
}
|
|
7684
8013
|
};
|
|
7685
|
-
registerCheck(
|
|
8014
|
+
registerCheck(check28);
|
|
7686
8015
|
|
|
7687
8016
|
// src/checks/asset/image-alpha.ts
|
|
7688
|
-
var
|
|
8017
|
+
var check29 = {
|
|
7689
8018
|
id: "asset.image.alpha-channel",
|
|
7690
8019
|
name: "Image Alpha Channel",
|
|
7691
8020
|
description: "Checks for alpha channels in PNG/TIFF images",
|
|
@@ -7703,7 +8032,7 @@ var check28 = {
|
|
|
7703
8032
|
const info2 = await inspectImage(file);
|
|
7704
8033
|
if (info2?.hasAlpha) {
|
|
7705
8034
|
results.push({
|
|
7706
|
-
checkId:
|
|
8035
|
+
checkId: check29.id,
|
|
7707
8036
|
severity: "warning",
|
|
7708
8037
|
message: "Image contains alpha channel, which may cause print issues",
|
|
7709
8038
|
file
|
|
@@ -7713,10 +8042,10 @@ var check28 = {
|
|
|
7713
8042
|
return results;
|
|
7714
8043
|
}
|
|
7715
8044
|
};
|
|
7716
|
-
registerCheck(
|
|
8045
|
+
registerCheck(check29);
|
|
7717
8046
|
|
|
7718
8047
|
// src/checks/asset/image-tac.ts
|
|
7719
|
-
var
|
|
8048
|
+
var check30 = {
|
|
7720
8049
|
id: "asset.image.tac-raster",
|
|
7721
8050
|
name: "Image TAC (Raster)",
|
|
7722
8051
|
description: "Rasterizes and checks TAC per image using Ghostscript",
|
|
@@ -7735,7 +8064,7 @@ var check29 = {
|
|
|
7735
8064
|
const ghostscript = await resolveGhostscript();
|
|
7736
8065
|
if (!ghostscript) {
|
|
7737
8066
|
return [
|
|
7738
|
-
inspectionFailed(
|
|
8067
|
+
inspectionFailed(check30.id, "Could not inspect image ink coverage: Ghostscript executable not found")
|
|
7739
8068
|
];
|
|
7740
8069
|
}
|
|
7741
8070
|
for (const file of files) {
|
|
@@ -7753,7 +8082,7 @@ var check29 = {
|
|
|
7753
8082
|
if (nums.length === 4 && nums.every((n) => Number.isFinite(n))) {
|
|
7754
8083
|
const tac = (nums[0] + nums[1] + nums[2] + nums[3]) * 100;
|
|
7755
8084
|
if (tac > maxTac) {
|
|
7756
|
-
results.push(finding(
|
|
8085
|
+
results.push(finding(check30.id, {
|
|
7757
8086
|
severity: "warning",
|
|
7758
8087
|
message: `Image TAC exceeds limit: ${tac.toFixed(1)}% (max ${maxTac}%)`,
|
|
7759
8088
|
file,
|
|
@@ -7765,16 +8094,16 @@ var check29 = {
|
|
|
7765
8094
|
}
|
|
7766
8095
|
}
|
|
7767
8096
|
} catch {
|
|
7768
|
-
results.push(inspectionFailed(
|
|
8097
|
+
results.push(inspectionFailed(check30.id, `Could not inspect image ink coverage: ${file}`, { file }));
|
|
7769
8098
|
}
|
|
7770
8099
|
}
|
|
7771
8100
|
return results;
|
|
7772
8101
|
}
|
|
7773
8102
|
};
|
|
7774
|
-
registerCheck(
|
|
8103
|
+
registerCheck(check30);
|
|
7775
8104
|
|
|
7776
8105
|
// src/checks/asset/approved-fonts.ts
|
|
7777
|
-
var
|
|
8106
|
+
var check31 = {
|
|
7778
8107
|
id: "asset.font.approved-files",
|
|
7779
8108
|
name: "Approved Font Files",
|
|
7780
8109
|
description: "Checks font files against the approved file patterns",
|
|
@@ -7813,7 +8142,7 @@ var check30 = {
|
|
|
7813
8142
|
for (const font of allFonts) {
|
|
7814
8143
|
if (!approvedFonts.has(font)) {
|
|
7815
8144
|
results.push({
|
|
7816
|
-
checkId:
|
|
8145
|
+
checkId: check31.id,
|
|
7817
8146
|
severity: "warning",
|
|
7818
8147
|
message: "Font file not in approved list",
|
|
7819
8148
|
file: font
|
|
@@ -7823,7 +8152,7 @@ var check30 = {
|
|
|
7823
8152
|
return results;
|
|
7824
8153
|
}
|
|
7825
8154
|
};
|
|
7826
|
-
registerCheck(
|
|
8155
|
+
registerCheck(check31);
|
|
7827
8156
|
|
|
7828
8157
|
// src/checks/asset/font-license.ts
|
|
7829
8158
|
import { existsSync as existsSync10 } from "node:fs";
|
|
@@ -7838,7 +8167,7 @@ var LICENSE_NAMES = [
|
|
|
7838
8167
|
"OFL-1.1.txt",
|
|
7839
8168
|
"COPYING"
|
|
7840
8169
|
];
|
|
7841
|
-
var
|
|
8170
|
+
var check32 = {
|
|
7842
8171
|
id: "asset.font.license",
|
|
7843
8172
|
name: "Font License",
|
|
7844
8173
|
description: "Checks for license files in font directories",
|
|
@@ -7865,7 +8194,7 @@ var check31 = {
|
|
|
7865
8194
|
const hasLicense = LICENSE_NAMES.some((name) => existsSync10(resolve8(fontDir, name)));
|
|
7866
8195
|
if (!hasLicense) {
|
|
7867
8196
|
results.push({
|
|
7868
|
-
checkId:
|
|
8197
|
+
checkId: check32.id,
|
|
7869
8198
|
severity: "warning",
|
|
7870
8199
|
message: `No font license file found in directory`,
|
|
7871
8200
|
file: fontDir
|
|
@@ -7875,10 +8204,10 @@ var check31 = {
|
|
|
7875
8204
|
return results;
|
|
7876
8205
|
}
|
|
7877
8206
|
};
|
|
7878
|
-
registerCheck(
|
|
8207
|
+
registerCheck(check32);
|
|
7879
8208
|
|
|
7880
8209
|
// src/checks/heuristic/text-density.ts
|
|
7881
|
-
var
|
|
8210
|
+
var check33 = {
|
|
7882
8211
|
id: "heuristic.whitespace.text-density",
|
|
7883
8212
|
name: "Text Density",
|
|
7884
8213
|
description: "Checks characters-per-page ratio",
|
|
@@ -7907,7 +8236,7 @@ var check32 = {
|
|
|
7907
8236
|
});
|
|
7908
8237
|
if (lowPages.length > 0) {
|
|
7909
8238
|
results.push({
|
|
7910
|
-
checkId:
|
|
8239
|
+
checkId: check33.id,
|
|
7911
8240
|
severity: "info",
|
|
7912
8241
|
message: `Low text density on pages: ${lowPages.join(", ")} (below ${range.min} chars)`,
|
|
7913
8242
|
file: ctx.pdfPath
|
|
@@ -7915,7 +8244,7 @@ var check32 = {
|
|
|
7915
8244
|
}
|
|
7916
8245
|
if (highPages.length > 0) {
|
|
7917
8246
|
results.push({
|
|
7918
|
-
checkId:
|
|
8247
|
+
checkId: check33.id,
|
|
7919
8248
|
severity: "info",
|
|
7920
8249
|
message: `High text density on pages: ${highPages.join(", ")} (above ${range.max} chars)`,
|
|
7921
8250
|
file: ctx.pdfPath
|
|
@@ -7924,11 +8253,11 @@ var check32 = {
|
|
|
7924
8253
|
return results;
|
|
7925
8254
|
}
|
|
7926
8255
|
};
|
|
7927
|
-
registerCheck(
|
|
8256
|
+
registerCheck(check33);
|
|
7928
8257
|
|
|
7929
8258
|
// src/checks/heuristic/section-density.ts
|
|
7930
8259
|
import { readFile as readFile22 } from "node:fs/promises";
|
|
7931
|
-
var
|
|
8260
|
+
var check34 = {
|
|
7932
8261
|
id: "heuristic.chunking.section-density",
|
|
7933
8262
|
name: "Section Density",
|
|
7934
8263
|
description: "Checks heading/paragraph/callout density from source Markdown",
|
|
@@ -7954,7 +8283,7 @@ var check33 = {
|
|
|
7954
8283
|
const line = lines[i];
|
|
7955
8284
|
if (/^#{1,6}\s/.test(line)) {
|
|
7956
8285
|
if (paragraphCount > maxParas) {
|
|
7957
|
-
results.push(finding(
|
|
8286
|
+
results.push(finding(check34.id, {
|
|
7958
8287
|
severity: "info",
|
|
7959
8288
|
message: `Section has ${paragraphCount} paragraphs (max recommended: ${maxParas})`,
|
|
7960
8289
|
file,
|
|
@@ -7974,7 +8303,7 @@ var check33 = {
|
|
|
7974
8303
|
}
|
|
7975
8304
|
}
|
|
7976
8305
|
if (paragraphCount > maxParas) {
|
|
7977
|
-
results.push(finding(
|
|
8306
|
+
results.push(finding(check34.id, {
|
|
7978
8307
|
severity: "info",
|
|
7979
8308
|
message: `Section has ${paragraphCount} paragraphs (max recommended: ${maxParas})`,
|
|
7980
8309
|
file,
|
|
@@ -7982,7 +8311,7 @@ var check33 = {
|
|
|
7982
8311
|
}));
|
|
7983
8312
|
}
|
|
7984
8313
|
} catch {
|
|
7985
|
-
results.push(inspectionFailed(
|
|
8314
|
+
results.push(inspectionFailed(check34.id, `Could not read source file: ${file}`, {
|
|
7986
8315
|
file
|
|
7987
8316
|
}));
|
|
7988
8317
|
}
|
|
@@ -7990,10 +8319,10 @@ var check33 = {
|
|
|
7990
8319
|
return results;
|
|
7991
8320
|
}
|
|
7992
8321
|
};
|
|
7993
|
-
registerCheck(
|
|
8322
|
+
registerCheck(check34);
|
|
7994
8323
|
|
|
7995
8324
|
// src/checks/heuristic/layer-count.ts
|
|
7996
|
-
var
|
|
8325
|
+
var check35 = {
|
|
7997
8326
|
id: "heuristic.decoration.layer-count",
|
|
7998
8327
|
name: "Layer Count",
|
|
7999
8328
|
description: "Counts image objects per page",
|
|
@@ -8018,7 +8347,7 @@ var check34 = {
|
|
|
8018
8347
|
heavyPages.sort((a, b) => a - b);
|
|
8019
8348
|
return [
|
|
8020
8349
|
{
|
|
8021
|
-
checkId:
|
|
8350
|
+
checkId: check35.id,
|
|
8022
8351
|
severity: "info",
|
|
8023
8352
|
message: `Pages with many image layers (>${maxLayers}): ${heavyPages.join(", ")}`,
|
|
8024
8353
|
file: ctx.pdfPath
|
|
@@ -8028,10 +8357,10 @@ var check34 = {
|
|
|
8028
8357
|
return [];
|
|
8029
8358
|
}
|
|
8030
8359
|
};
|
|
8031
|
-
registerCheck(
|
|
8360
|
+
registerCheck(check35);
|
|
8032
8361
|
|
|
8033
8362
|
// src/checks/heuristic/placement-variance.ts
|
|
8034
|
-
var
|
|
8363
|
+
var check36 = {
|
|
8035
8364
|
id: "heuristic.layout.placement-variance",
|
|
8036
8365
|
name: "Placement Variance",
|
|
8037
8366
|
description: "Analyzes text baseline coordinates for layout consistency",
|
|
@@ -8049,7 +8378,7 @@ var check35 = {
|
|
|
8049
8378
|
const uniqueX = new Set(positions.map((p) => Math.round(p.x)));
|
|
8050
8379
|
return [
|
|
8051
8380
|
{
|
|
8052
|
-
checkId:
|
|
8381
|
+
checkId: check36.id,
|
|
8053
8382
|
severity: "info",
|
|
8054
8383
|
message: `Layout analysis: ${uniqueX.size} unique horizontal text positions across ${positions.length} text blocks.`,
|
|
8055
8384
|
file: ctx.pdfPath
|
|
@@ -8057,7 +8386,7 @@ var check35 = {
|
|
|
8057
8386
|
];
|
|
8058
8387
|
}
|
|
8059
8388
|
};
|
|
8060
|
-
registerCheck(
|
|
8389
|
+
registerCheck(check36);
|
|
8061
8390
|
|
|
8062
8391
|
// src/checks/runner.ts
|
|
8063
8392
|
function getCheckSeverityOverride(checkId, config) {
|
|
@@ -8087,25 +8416,25 @@ async function runChecks(ctx, opts = {}) {
|
|
|
8087
8416
|
}
|
|
8088
8417
|
const release = retainPdfCache();
|
|
8089
8418
|
try {
|
|
8090
|
-
for (const
|
|
8419
|
+
for (const check37 of checks2) {
|
|
8091
8420
|
try {
|
|
8092
|
-
const results = await
|
|
8093
|
-
const severityOverride = getCheckSeverityOverride(
|
|
8421
|
+
const results = await check37.run(ctx);
|
|
8422
|
+
const severityOverride = getCheckSeverityOverride(check37.id, ctx.config);
|
|
8094
8423
|
if (severityOverride) {
|
|
8095
8424
|
for (const r of results) {
|
|
8096
8425
|
r.severity = severityOverride;
|
|
8097
8426
|
}
|
|
8098
8427
|
}
|
|
8099
8428
|
if (results.length === 0) {
|
|
8100
|
-
passed.push(
|
|
8429
|
+
passed.push(check37.id);
|
|
8101
8430
|
} else {
|
|
8102
8431
|
allResults.push(...results);
|
|
8103
8432
|
}
|
|
8104
8433
|
} catch (err) {
|
|
8105
8434
|
allResults.push({
|
|
8106
|
-
checkId:
|
|
8435
|
+
checkId: check37.id,
|
|
8107
8436
|
severity: "error",
|
|
8108
|
-
message: `Check "${
|
|
8437
|
+
message: `Check "${check37.id}" threw: ${err instanceof Error ? err.message : String(err)}`
|
|
8109
8438
|
});
|
|
8110
8439
|
}
|
|
8111
8440
|
}
|
|
@@ -8145,15 +8474,15 @@ function emptyReport() {
|
|
|
8145
8474
|
async function checkToolAvailability(config, opts = {}) {
|
|
8146
8475
|
const { checks: checks2 } = selectChecks(opts, config);
|
|
8147
8476
|
const toolToChecks = new Map;
|
|
8148
|
-
for (const
|
|
8149
|
-
if (!
|
|
8477
|
+
for (const check37 of checks2) {
|
|
8478
|
+
if (!check37.requiredTools?.length)
|
|
8150
8479
|
continue;
|
|
8151
|
-
for (const tool of
|
|
8480
|
+
for (const tool of check37.requiredTools) {
|
|
8152
8481
|
const existing = toolToChecks.get(tool);
|
|
8153
8482
|
if (existing) {
|
|
8154
|
-
existing.push(
|
|
8483
|
+
existing.push(check37.id);
|
|
8155
8484
|
} else {
|
|
8156
|
-
toolToChecks.set(tool, [
|
|
8485
|
+
toolToChecks.set(tool, [check37.id]);
|
|
8157
8486
|
}
|
|
8158
8487
|
}
|
|
8159
8488
|
}
|
|
@@ -8169,11 +8498,11 @@ async function checkToolAvailability(config, opts = {}) {
|
|
|
8169
8498
|
const missing = results.filter((r) => !r.found).map((r) => r.tool);
|
|
8170
8499
|
const missingSet = new Set(missing);
|
|
8171
8500
|
const skippedChecks = [];
|
|
8172
|
-
for (const
|
|
8173
|
-
if (!
|
|
8501
|
+
for (const check37 of checks2) {
|
|
8502
|
+
if (!check37.requiredTools?.length)
|
|
8174
8503
|
continue;
|
|
8175
|
-
if (
|
|
8176
|
-
skippedChecks.push(
|
|
8504
|
+
if (check37.requiredTools.some((t) => missingSet.has(t))) {
|
|
8505
|
+
skippedChecks.push(check37.id);
|
|
8177
8506
|
}
|
|
8178
8507
|
}
|
|
8179
8508
|
return { available, missing, skippedChecks, toolToChecks };
|
|
@@ -8580,245 +8909,6 @@ function computeGates(format, opts, config) {
|
|
|
8580
8909
|
return { lint: lint2, preValidate, postValidate };
|
|
8581
8910
|
}
|
|
8582
8911
|
|
|
8583
|
-
// src/lib/build-staging.ts
|
|
8584
|
-
import path8 from "node:path";
|
|
8585
|
-
import os from "node:os";
|
|
8586
|
-
import fsp from "node:fs/promises";
|
|
8587
|
-
|
|
8588
|
-
// src/lib/missing-asset-placeholder.ts
|
|
8589
|
-
import { createHash as createHash5 } from "node:crypto";
|
|
8590
|
-
import { deflateSync } from "node:zlib";
|
|
8591
|
-
function placeholderOutputPath(missingOutputPath) {
|
|
8592
|
-
const hash = createHash5("sha256").update(missingOutputPath).digest("hex").slice(0, 16);
|
|
8593
|
-
return `assets/gutterpress-missing/${hash}.png`;
|
|
8594
|
-
}
|
|
8595
|
-
function decodeHtmlAttribute(value) {
|
|
8596
|
-
return value.replace(/"/g, '"').replace(/'|'/g, "'").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
8597
|
-
}
|
|
8598
|
-
function rewriteMissingImageReferences(html, replacements) {
|
|
8599
|
-
if (replacements.size === 0)
|
|
8600
|
-
return html;
|
|
8601
|
-
const replacementFor = (raw) => replacements.get(raw) ?? replacements.get(decodeHtmlAttribute(raw));
|
|
8602
|
-
const cssUrl = /url\(\s*(?:"([^"]*)"|'([^']*)'|"((?:(?!").)*)"|([^'"()\s]*))\s*\)/giy;
|
|
8603
|
-
const rewriteCssUrls = (css) => {
|
|
8604
|
-
let out2 = "";
|
|
8605
|
-
let copiedThrough = 0;
|
|
8606
|
-
let index = 0;
|
|
8607
|
-
while (index < css.length) {
|
|
8608
|
-
if (css.startsWith("/*", index)) {
|
|
8609
|
-
const end = css.indexOf("*/", index + 2);
|
|
8610
|
-
index = end < 0 ? css.length : end + 2;
|
|
8611
|
-
continue;
|
|
8612
|
-
}
|
|
8613
|
-
const char = css[index];
|
|
8614
|
-
if (char === '"' || char === "'") {
|
|
8615
|
-
const quote = char;
|
|
8616
|
-
index++;
|
|
8617
|
-
while (index < css.length) {
|
|
8618
|
-
if (css[index] === "\\") {
|
|
8619
|
-
index += 2;
|
|
8620
|
-
continue;
|
|
8621
|
-
}
|
|
8622
|
-
const current = css[index++];
|
|
8623
|
-
if (current === quote)
|
|
8624
|
-
break;
|
|
8625
|
-
}
|
|
8626
|
-
continue;
|
|
8627
|
-
}
|
|
8628
|
-
const previous = index > 0 ? css[index - 1] : "";
|
|
8629
|
-
if (!/[a-z0-9_-]/i.test(previous) && css.slice(index, index + 3).toLowerCase() === "url") {
|
|
8630
|
-
cssUrl.lastIndex = index;
|
|
8631
|
-
const match = cssUrl.exec(css);
|
|
8632
|
-
if (match) {
|
|
8633
|
-
const raw = match[1] ?? match[2] ?? match[3] ?? match[4] ?? "";
|
|
8634
|
-
const replacement = replacementFor(raw);
|
|
8635
|
-
if (replacement) {
|
|
8636
|
-
out2 += css.slice(copiedThrough, index);
|
|
8637
|
-
out2 += match[3] !== undefined ? `url("${replacement}")` : `url("${replacement}")`;
|
|
8638
|
-
index = cssUrl.lastIndex;
|
|
8639
|
-
copiedThrough = index;
|
|
8640
|
-
continue;
|
|
8641
|
-
}
|
|
8642
|
-
}
|
|
8643
|
-
}
|
|
8644
|
-
index++;
|
|
8645
|
-
}
|
|
8646
|
-
return copiedThrough === 0 ? css : out2 + css.slice(copiedThrough);
|
|
8647
|
-
};
|
|
8648
|
-
const rewriteTag = (tag) => {
|
|
8649
|
-
let out2 = tag;
|
|
8650
|
-
if (/^<img\b/i.test(tag)) {
|
|
8651
|
-
out2 = out2.replace(/(\s+src\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi, (whole, prefix, double, single, bare) => {
|
|
8652
|
-
const raw = double ?? single ?? bare ?? "";
|
|
8653
|
-
const replacement = replacementFor(raw);
|
|
8654
|
-
if (!replacement)
|
|
8655
|
-
return whole;
|
|
8656
|
-
if (double !== undefined)
|
|
8657
|
-
return `${prefix}"${replacement}"`;
|
|
8658
|
-
if (single !== undefined)
|
|
8659
|
-
return `${prefix}'${replacement}'`;
|
|
8660
|
-
return `${prefix}${replacement}`;
|
|
8661
|
-
});
|
|
8662
|
-
}
|
|
8663
|
-
if (/^<(?:img|source)\b/i.test(tag)) {
|
|
8664
|
-
out2 = out2.replace(/(\s+srcset\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi, (whole, prefix, double, single, bare) => {
|
|
8665
|
-
const value = double ?? single ?? bare ?? "";
|
|
8666
|
-
let rewritten = "";
|
|
8667
|
-
let copiedThrough = 0;
|
|
8668
|
-
let changed = false;
|
|
8669
|
-
for (const candidate of parseSrcsetUrlCandidates(value)) {
|
|
8670
|
-
const replacement = replacementFor(candidate.url);
|
|
8671
|
-
if (!replacement)
|
|
8672
|
-
continue;
|
|
8673
|
-
rewritten += value.slice(copiedThrough, candidate.start) + replacement;
|
|
8674
|
-
copiedThrough = candidate.end;
|
|
8675
|
-
changed = true;
|
|
8676
|
-
}
|
|
8677
|
-
if (!changed)
|
|
8678
|
-
return whole;
|
|
8679
|
-
rewritten += value.slice(copiedThrough);
|
|
8680
|
-
if (double !== undefined)
|
|
8681
|
-
return `${prefix}"${rewritten}"`;
|
|
8682
|
-
if (single !== undefined)
|
|
8683
|
-
return `${prefix}'${rewritten}'`;
|
|
8684
|
-
return `${prefix}${rewritten}`;
|
|
8685
|
-
});
|
|
8686
|
-
}
|
|
8687
|
-
out2 = out2.replace(/(\s+style\s*=\s*)(["'])(.*?)\2/gi, (whole, prefix, quote, value) => {
|
|
8688
|
-
const rewritten = rewriteCssUrls(value);
|
|
8689
|
-
return rewritten === value ? whole : `${prefix}${quote}${rewritten}${quote}`;
|
|
8690
|
-
});
|
|
8691
|
-
return out2;
|
|
8692
|
-
};
|
|
8693
|
-
const rewriteActiveHtml = (active) => active.replace(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g, (tag) => rewriteTag(tag));
|
|
8694
|
-
const protectedRegion = /<!--[\s\S]*?-->|<(script|style|pre|code|textarea)\b[^>]*>[\s\S]*?<\/\1\s*>/gi;
|
|
8695
|
-
let out = "";
|
|
8696
|
-
let last = 0;
|
|
8697
|
-
for (const match of html.matchAll(protectedRegion)) {
|
|
8698
|
-
out += rewriteActiveHtml(html.slice(last, match.index));
|
|
8699
|
-
if (match[1]?.toLowerCase() === "style") {
|
|
8700
|
-
const style = /^(<style\b[^>]*>)([\s\S]*)(<\/style\s*>)$/i.exec(match[0]);
|
|
8701
|
-
out += style ? `${rewriteTag(style[1])}${rewriteCssUrls(style[2])}${style[3]}` : match[0];
|
|
8702
|
-
} else {
|
|
8703
|
-
out += match[0];
|
|
8704
|
-
}
|
|
8705
|
-
last = (match.index ?? 0) + match[0].length;
|
|
8706
|
-
}
|
|
8707
|
-
out += rewriteActiveHtml(html.slice(last));
|
|
8708
|
-
return out;
|
|
8709
|
-
}
|
|
8710
|
-
function crc32(buf) {
|
|
8711
|
-
let c = ~0;
|
|
8712
|
-
for (let i = 0;i < buf.length; i++) {
|
|
8713
|
-
c ^= buf[i];
|
|
8714
|
-
for (let k = 0;k < 8; k++)
|
|
8715
|
-
c = c >>> 1 ^ 3988292384 & -(c & 1);
|
|
8716
|
-
}
|
|
8717
|
-
return ~c >>> 0;
|
|
8718
|
-
}
|
|
8719
|
-
function chunk(type, data) {
|
|
8720
|
-
const out = new Uint8Array(data.length + 12);
|
|
8721
|
-
const view = new DataView(out.buffer);
|
|
8722
|
-
view.setUint32(0, data.length);
|
|
8723
|
-
for (let i = 0;i < 4; i++)
|
|
8724
|
-
out[4 + i] = type.charCodeAt(i);
|
|
8725
|
-
out.set(data, 8);
|
|
8726
|
-
view.setUint32(data.length + 8, crc32(out.subarray(4, data.length + 8)));
|
|
8727
|
-
return out;
|
|
8728
|
-
}
|
|
8729
|
-
function placeholderPng(width = 640, height = 480, cell = 32) {
|
|
8730
|
-
const A = [217, 70, 239];
|
|
8731
|
-
const B = [26, 26, 26];
|
|
8732
|
-
const raw = new Uint8Array(height * (1 + width * 3));
|
|
8733
|
-
let p = 0;
|
|
8734
|
-
for (let y = 0;y < height; y++) {
|
|
8735
|
-
raw[p++] = 0;
|
|
8736
|
-
for (let x = 0;x < width; x++) {
|
|
8737
|
-
const c = ((x / cell | 0) + (y / cell | 0)) % 2 === 0 ? A : B;
|
|
8738
|
-
raw[p++] = c[0];
|
|
8739
|
-
raw[p++] = c[1];
|
|
8740
|
-
raw[p++] = c[2];
|
|
8741
|
-
}
|
|
8742
|
-
}
|
|
8743
|
-
const ihdr = new Uint8Array(13);
|
|
8744
|
-
const hv = new DataView(ihdr.buffer);
|
|
8745
|
-
hv.setUint32(0, width);
|
|
8746
|
-
hv.setUint32(4, height);
|
|
8747
|
-
ihdr[8] = 8;
|
|
8748
|
-
ihdr[9] = 2;
|
|
8749
|
-
const parts = [
|
|
8750
|
-
new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
|
|
8751
|
-
chunk("IHDR", ihdr),
|
|
8752
|
-
chunk("IDAT", new Uint8Array(deflateSync(raw))),
|
|
8753
|
-
chunk("IEND", new Uint8Array(0))
|
|
8754
|
-
];
|
|
8755
|
-
const total = parts.reduce((n, x) => n + x.length, 0);
|
|
8756
|
-
const png = new Uint8Array(total);
|
|
8757
|
-
let o = 0;
|
|
8758
|
-
for (const part of parts) {
|
|
8759
|
-
png.set(part, o);
|
|
8760
|
-
o += part.length;
|
|
8761
|
-
}
|
|
8762
|
-
return png;
|
|
8763
|
-
}
|
|
8764
|
-
|
|
8765
|
-
// src/lib/build-staging.ts
|
|
8766
|
-
async function shipViewerHtml(htmlFile, outDir) {
|
|
8767
|
-
await fsp.mkdir(path8.join(outDir, "engine"), { recursive: true });
|
|
8768
|
-
await fsp.copyFile(await getAssetPath("engine/gutterpress-viewer.js"), path8.join(outDir, "engine/gutterpress-viewer.js"));
|
|
8769
|
-
const tag = ` <script src="engine/gutterpress-viewer.js"></script>
|
|
8770
|
-
`;
|
|
8771
|
-
const html = await fsp.readFile(htmlFile, "utf-8");
|
|
8772
|
-
await fsp.writeFile(htmlFile, /<\/head>/i.test(html) ? html.replace(/<\/head>/i, tag + "</head>") : tag + html, "utf-8");
|
|
8773
|
-
}
|
|
8774
|
-
async function stageBookAssets(options) {
|
|
8775
|
-
const { renderDir, outDir, htmlFile, imageRefs, cssAssets, onPlan } = options;
|
|
8776
|
-
const { copies: imageCopies, errors, destinations } = await planImageCopies(renderDir, imageRefs);
|
|
8777
|
-
const copies = [...cssAssets, ...imageCopies];
|
|
8778
|
-
onPlan?.({ unresolved: errors, copyCount: copies.length });
|
|
8779
|
-
const missingPlaceholders = copies.length ? await copyReferencedAssets(copies, outDir) : new Map;
|
|
8780
|
-
let staged = await fsp.readFile(htmlFile, "utf8");
|
|
8781
|
-
if (missingPlaceholders.size > 0) {
|
|
8782
|
-
const rewrites = new Map(missingPlaceholders);
|
|
8783
|
-
for (const [ref, dest] of destinations) {
|
|
8784
|
-
const placeholder = missingPlaceholders.get(dest);
|
|
8785
|
-
if (placeholder)
|
|
8786
|
-
rewrites.set(ref, placeholder);
|
|
8787
|
-
}
|
|
8788
|
-
staged = rewriteMissingImageReferences(staged, rewrites);
|
|
8789
|
-
await fsp.writeFile(htmlFile, staged, "utf8");
|
|
8790
|
-
}
|
|
8791
|
-
if (staged.includes("--gp-shape:")) {
|
|
8792
|
-
await fsp.writeFile(htmlFile, await inlineShapeUrls(staged, outDir), "utf8");
|
|
8793
|
-
}
|
|
8794
|
-
return { missing: [...missingPlaceholders.keys()].sort() };
|
|
8795
|
-
}
|
|
8796
|
-
async function copyReferencedAssets(copies, outDir) {
|
|
8797
|
-
const dirs = new Set(copies.map((c) => path8.dirname(path8.resolve(outDir, c.to))));
|
|
8798
|
-
await Promise.all([...dirs].map((d) => fsp.mkdir(d, { recursive: true })));
|
|
8799
|
-
const missing = new Map;
|
|
8800
|
-
await Promise.all(copies.map(async (c) => {
|
|
8801
|
-
const dest = path8.resolve(outDir, c.to);
|
|
8802
|
-
try {
|
|
8803
|
-
await fsp.copyFile(c.from, dest);
|
|
8804
|
-
} catch (err) {
|
|
8805
|
-
if (err?.code === "ENOENT") {
|
|
8806
|
-
const placeholder = placeholderOutputPath(c.to);
|
|
8807
|
-
const placeholderDest = path8.resolve(outDir, placeholder);
|
|
8808
|
-
await fsp.mkdir(path8.dirname(placeholderDest), { recursive: true });
|
|
8809
|
-
await fsp.writeFile(placeholderDest, placeholderPng());
|
|
8810
|
-
missing.set(c.to, placeholder);
|
|
8811
|
-
return;
|
|
8812
|
-
}
|
|
8813
|
-
throw new BuildError(`Could not copy asset ${c.from} → ${c.to}: ` + (err instanceof Error ? err.message : String(err)), 1);
|
|
8814
|
-
}
|
|
8815
|
-
}));
|
|
8816
|
-
return missing;
|
|
8817
|
-
}
|
|
8818
|
-
async function createStageRoot() {
|
|
8819
|
-
return fsp.mkdtemp(path8.join(os.tmpdir(), "gutterpress-stage-"));
|
|
8820
|
-
}
|
|
8821
|
-
|
|
8822
8912
|
// src/lib/build-runner.ts
|
|
8823
8913
|
function splitOutPath(outArg, format) {
|
|
8824
8914
|
if (typeof outArg !== "string" || outArg.length === 0) {
|
|
@@ -8914,7 +9004,7 @@ async function runQualityGates(ctx) {
|
|
|
8914
9004
|
}
|
|
8915
9005
|
}
|
|
8916
9006
|
async function renderBook(ctx) {
|
|
8917
|
-
const { config, gates, renderDir, workDir, opts } = ctx;
|
|
9007
|
+
const { config, gates, renderDir, workDir, opts, format } = ctx;
|
|
8918
9008
|
if (config.source.files && config.source.files.length > 0) {
|
|
8919
9009
|
log.info(`Using specified files (${config.source.files.length} total)`);
|
|
8920
9010
|
} else {
|
|
@@ -8952,6 +9042,7 @@ async function renderBook(ctx) {
|
|
|
8952
9042
|
htmlFile,
|
|
8953
9043
|
imageRefs,
|
|
8954
9044
|
cssAssets,
|
|
9045
|
+
dropRelativeLinks: format !== "html",
|
|
8955
9046
|
onPlan: ({ unresolved, copyCount }) => {
|
|
8956
9047
|
if (unresolved.length > 0) {
|
|
8957
9048
|
throw new BuildError(`Cannot resolve ${unresolved.length} image reference(s):
|