@gmickel/gno 1.45.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/THIRD_PARTY_NOTICES.md +46 -0
- package/assets/skill/SKILL.md +7 -6
- package/assets/skill/cli-reference.md +14 -6
- package/assets/skill/mcp-reference.md +4 -1
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.0.0.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.0.0.zip.sha256 +1 -0
- package/browser-extension/dist/chunk-4tc9v0ja.js +74 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/browser-extension/dist/preview.html +1 -1
- package/browser-extension/dist/service-worker.js +32 -33
- package/bunfig.toml +2 -0
- package/package.json +40 -26
- package/spec/cli.md +30 -11
- package/spec/db/schema.sql +146 -1
- package/spec/mcp.md +26 -0
- package/src/app/context-runtime-types.ts +3 -0
- package/src/app/context-runtime.ts +2 -0
- package/src/cli/commands/ask.ts +6 -1
- package/src/cli/commands/daemon.ts +21 -8
- package/src/cli/commands/embed.ts +77 -41
- package/src/cli/commands/mcp/install.ts +20 -0
- package/src/cli/commands/mcp/paths.ts +25 -0
- package/src/cli/commands/mcp/status.ts +6 -0
- package/src/cli/detach.ts +3 -2
- package/src/cli/program.ts +6 -0
- package/src/config/types.ts +3 -3
- package/src/converters/adapters/markitdownTs/adapter.ts +1 -2
- package/src/converters/adapters/officeparser/adapter.ts +1 -2
- package/src/converters/versions.ts +6 -8
- package/src/core/context-evidence.ts +8 -4
- package/src/core/job-manager.ts +95 -13
- package/src/core/network-boundary-inventory.ts +10 -0
- package/src/core/shutdown-budget.ts +45 -0
- package/src/embed/backlog.ts +107 -4
- package/src/embed/batch.ts +42 -2
- package/src/embed/fingerprint.ts +16 -0
- package/src/embed/retry.ts +113 -5
- package/src/embed/variant-backlog.ts +105 -0
- package/src/embed/variant-plan.ts +62 -0
- package/src/embed/variant-retry.ts +113 -0
- package/src/ingestion/graph-reconciliation.ts +327 -0
- package/src/ingestion/sync.ts +9 -272
- package/src/llm/http-inference.ts +6 -0
- package/src/llm/httpEmbedding.ts +37 -6
- package/src/llm/httpGeneration.ts +18 -3
- package/src/llm/httpRerank.ts +23 -5
- package/src/llm/inference-cancellation.ts +168 -0
- package/src/llm/inference-scope.ts +202 -0
- package/src/llm/lazy-ports.ts +115 -0
- package/src/llm/native-worker/client.ts +541 -0
- package/src/llm/native-worker/dispatcher.ts +228 -0
- package/src/llm/native-worker/embedding-identity.ts +33 -0
- package/src/llm/native-worker/entry.ts +173 -0
- package/src/llm/native-worker/errors.ts +32 -0
- package/src/llm/native-worker/evaluation.ts +16 -0
- package/src/llm/native-worker/owned-exit.ts +108 -0
- package/src/llm/native-worker/owner.ts +141 -0
- package/src/llm/native-worker/ports.ts +317 -0
- package/src/llm/native-worker/protocol.ts +442 -0
- package/src/llm/native-worker/runtime-config.ts +92 -0
- package/src/llm/nodeLlamaCpp/adapter.ts +77 -20
- package/src/llm/nodeLlamaCpp/embedding.ts +130 -46
- package/src/llm/nodeLlamaCpp/generation.ts +34 -5
- package/src/llm/nodeLlamaCpp/lifecycle-options.ts +99 -0
- package/src/llm/nodeLlamaCpp/lifecycle.ts +209 -204
- package/src/llm/nodeLlamaCpp/rerank-capacity.ts +111 -0
- package/src/llm/nodeLlamaCpp/rerank.ts +118 -27
- package/src/llm/nodeLlamaCpp/simulator-handle.ts +73 -0
- package/src/llm/nodeLlamaCpp/simulator-install.ts +124 -0
- package/src/llm/nodeLlamaCpp/simulator-session.ts +240 -0
- package/src/llm/nodeLlamaCpp/simulator-types.ts +80 -0
- package/src/llm/types.ts +35 -5
- package/src/mcp/context.ts +27 -0
- package/src/mcp/http-transport.ts +12 -10
- package/src/mcp/server.ts +3 -0
- package/src/mcp/tool-profile.ts +30 -8
- package/src/mcp/tools/context.ts +8 -11
- package/src/mcp/tools/embed.ts +1 -1
- package/src/mcp/tools/index-cmd.ts +1 -1
- package/src/mcp/tools/index.ts +10 -8
- package/src/mcp/tools/query.ts +14 -30
- package/src/mcp/tools/vsearch.ts +1 -1
- package/src/pipeline/answer.ts +23 -3
- package/src/pipeline/claim-verifier.ts +6 -0
- package/src/pipeline/expansion.ts +43 -40
- package/src/pipeline/explain.ts +6 -2
- package/src/pipeline/filters.ts +63 -0
- package/src/pipeline/fusion.ts +29 -9
- package/src/pipeline/graph-retrieval.ts +29 -9
- package/src/pipeline/hybrid.ts +198 -55
- package/src/pipeline/hydration.ts +161 -0
- package/src/pipeline/owner-fusion.ts +87 -0
- package/src/pipeline/rerank.ts +35 -11
- package/src/pipeline/search.ts +13 -2
- package/src/pipeline/types.ts +5 -3
- package/src/pipeline/vsearch.ts +87 -7
- package/src/sdk/client.ts +47 -3
- package/src/sdk/embed.ts +63 -39
- package/src/serve/background-runtime.ts +1 -1
- package/src/serve/context.ts +41 -56
- package/src/serve/embed-scheduler.ts +58 -35
- package/src/serve/public/components/IndexingProgress.tsx +46 -60
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/lib/shiki-language-ids.ts +14 -0
- package/src/serve/resident-admission.ts +36 -36
- package/src/serve/resident-background-work.ts +20 -2
- package/src/serve/resident-request.ts +11 -5
- package/src/serve/resident-runtime.ts +97 -61
- package/src/serve/resident-shutdown.ts +153 -0
- package/src/serve/routes/api.ts +3 -1
- package/src/serve/server.ts +47 -26
- package/src/store/migrations/028-vector-variants.ts +54 -0
- package/src/store/migrations/029-graph-reference-state.ts +77 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +251 -183
- package/src/store/sqlite/eligibility.ts +174 -0
- package/src/store/sqlite/graph-edge-application.ts +66 -0
- package/src/store/sqlite/graph-reference-state.ts +194 -0
- package/src/store/sqlite/legacy-vector-ownership.ts +79 -0
- package/src/store/types.ts +80 -12
- package/src/store/vector/eligibility.ts +36 -0
- package/src/store/vector/freshness.ts +33 -6
- package/src/store/vector/lazy.ts +81 -0
- package/src/store/vector/sqlite-vec.ts +106 -54
- package/src/store/vector/stats.ts +14 -3
- package/src/store/vector/types.ts +35 -2
- package/src/store/vector/variant-search.ts +192 -0
- package/src/store/vector/variants.ts +451 -0
- package/vendor/converters/markitdown-ts/LICENSE +21 -0
- package/vendor/converters/markitdown-ts/dist/index.cjs +1180 -0
- package/vendor/converters/markitdown-ts/dist/index.d.cts +46 -0
- package/vendor/converters/markitdown-ts/dist/index.d.mts +46 -0
- package/vendor/converters/markitdown-ts/dist/index.d.ts +46 -0
- package/vendor/converters/markitdown-ts/dist/index.mjs +1152 -0
- package/vendor/converters/markitdown-ts/package.json +77 -0
- package/vendor/converters/officeparser/LICENSE +21 -0
- package/vendor/converters/officeparser/dist/OfficeConverter.d.ts +47 -0
- package/vendor/converters/officeparser/dist/OfficeConverter.js +76 -0
- package/vendor/converters/officeparser/dist/OfficeGenerator.d.ts +23 -0
- package/vendor/converters/officeparser/dist/OfficeGenerator.js +73 -0
- package/vendor/converters/officeparser/dist/OfficeParser.d.ts +106 -0
- package/vendor/converters/officeparser/dist/OfficeParser.js +332 -0
- package/vendor/converters/officeparser/dist/cli.d.ts +28 -0
- package/vendor/converters/officeparser/dist/cli.js +381 -0
- package/vendor/converters/officeparser/dist/defaults.d.ts +41 -0
- package/vendor/converters/officeparser/dist/defaults.js +218 -0
- package/vendor/converters/officeparser/dist/generators/BaseGenerator.d.ts +107 -0
- package/vendor/converters/officeparser/dist/generators/BaseGenerator.js +248 -0
- package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.d.ts +82 -0
- package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.js +797 -0
- package/vendor/converters/officeparser/dist/generators/CsvGenerator.d.ts +38 -0
- package/vendor/converters/officeparser/dist/generators/CsvGenerator.js +245 -0
- package/vendor/converters/officeparser/dist/generators/EpubGenerator.d.ts +43 -0
- package/vendor/converters/officeparser/dist/generators/EpubGenerator.js +315 -0
- package/vendor/converters/officeparser/dist/generators/HtmlGenerator.d.ts +59 -0
- package/vendor/converters/officeparser/dist/generators/HtmlGenerator.js +1942 -0
- package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.d.ts +96 -0
- package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.js +1175 -0
- package/vendor/converters/officeparser/dist/generators/PdfGenerator.d.ts +22 -0
- package/vendor/converters/officeparser/dist/generators/PdfGenerator.js +194 -0
- package/vendor/converters/officeparser/dist/generators/RtfGenerator.d.ts +29 -0
- package/vendor/converters/officeparser/dist/generators/RtfGenerator.js +316 -0
- package/vendor/converters/officeparser/dist/generators/TextGenerator.d.ts +13 -0
- package/vendor/converters/officeparser/dist/generators/TextGenerator.js +201 -0
- package/vendor/converters/officeparser/dist/index.d.ts +60 -0
- package/vendor/converters/officeparser/dist/index.js +72 -0
- package/vendor/converters/officeparser/dist/index.mjs +18 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.d.ts +2621 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.iife.js +1336 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.mjs +1335 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.slim.d.ts +2621 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.slim.iife.js +1336 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.slim.mjs +1335 -0
- package/vendor/converters/officeparser/dist/parsers/CsvParser.d.ts +9 -0
- package/vendor/converters/officeparser/dist/parsers/CsvParser.js +115 -0
- package/vendor/converters/officeparser/dist/parsers/EpubParser.d.ts +8 -0
- package/vendor/converters/officeparser/dist/parsers/EpubParser.js +217 -0
- package/vendor/converters/officeparser/dist/parsers/ExcelParser.d.ts +32 -0
- package/vendor/converters/officeparser/dist/parsers/ExcelParser.js +736 -0
- package/vendor/converters/officeparser/dist/parsers/HtmlParser.d.ts +2 -0
- package/vendor/converters/officeparser/dist/parsers/HtmlParser.js +1287 -0
- package/vendor/converters/officeparser/dist/parsers/MarkdownParser.d.ts +2 -0
- package/vendor/converters/officeparser/dist/parsers/MarkdownParser.js +1272 -0
- package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.d.ts +31 -0
- package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.js +1819 -0
- package/vendor/converters/officeparser/dist/parsers/PdfParser.d.ts +67 -0
- package/vendor/converters/officeparser/dist/parsers/PdfParser.js +848 -0
- package/vendor/converters/officeparser/dist/parsers/PowerPointParser.d.ts +32 -0
- package/vendor/converters/officeparser/dist/parsers/PowerPointParser.js +950 -0
- package/vendor/converters/officeparser/dist/parsers/RtfParser.d.ts +187 -0
- package/vendor/converters/officeparser/dist/parsers/RtfParser.js +1801 -0
- package/vendor/converters/officeparser/dist/parsers/WordParser.d.ts +79 -0
- package/vendor/converters/officeparser/dist/parsers/WordParser.js +1177 -0
- package/vendor/converters/officeparser/dist/sbom.cdx.json +1763 -0
- package/vendor/converters/officeparser/dist/types.d.ts +2507 -0
- package/vendor/converters/officeparser/dist/types.js +107 -0
- package/vendor/converters/officeparser/dist/utils/astUtils.d.ts +16 -0
- package/vendor/converters/officeparser/dist/utils/astUtils.js +33 -0
- package/vendor/converters/officeparser/dist/utils/chartUtils.d.ts +6 -0
- package/vendor/converters/officeparser/dist/utils/chartUtils.js +257 -0
- package/vendor/converters/officeparser/dist/utils/configUtils.d.ts +44 -0
- package/vendor/converters/officeparser/dist/utils/configUtils.js +315 -0
- package/vendor/converters/officeparser/dist/utils/dateUtils.d.ts +17 -0
- package/vendor/converters/officeparser/dist/utils/dateUtils.js +69 -0
- package/vendor/converters/officeparser/dist/utils/envUtils.d.ts +29 -0
- package/vendor/converters/officeparser/dist/utils/envUtils.js +152 -0
- package/vendor/converters/officeparser/dist/utils/errorUtils.d.ts +72 -0
- package/vendor/converters/officeparser/dist/utils/errorUtils.js +245 -0
- package/vendor/converters/officeparser/dist/utils/imageUtils.d.ts +66 -0
- package/vendor/converters/officeparser/dist/utils/imageUtils.js +133 -0
- package/vendor/converters/officeparser/dist/utils/mathUtils.d.ts +42 -0
- package/vendor/converters/officeparser/dist/utils/mathUtils.js +385 -0
- package/vendor/converters/officeparser/dist/utils/moduleLoader.d.ts +18 -0
- package/vendor/converters/officeparser/dist/utils/moduleLoader.js +106 -0
- package/vendor/converters/officeparser/dist/utils/ocrUtils.d.ts +42 -0
- package/vendor/converters/officeparser/dist/utils/ocrUtils.js +428 -0
- package/vendor/converters/officeparser/dist/utils/sanitize.d.ts +148 -0
- package/vendor/converters/officeparser/dist/utils/sanitize.js +344 -0
- package/vendor/converters/officeparser/dist/utils/sheetUtils.d.ts +7 -0
- package/vendor/converters/officeparser/dist/utils/sheetUtils.js +35 -0
- package/vendor/converters/officeparser/dist/utils/styleMapper.d.ts +36 -0
- package/vendor/converters/officeparser/dist/utils/styleMapper.js +224 -0
- package/vendor/converters/officeparser/dist/utils/xmlUtils.d.ts +163 -0
- package/vendor/converters/officeparser/dist/utils/xmlUtils.js +461 -0
- package/vendor/converters/officeparser/dist/utils/zipUtils.d.ts +134 -0
- package/vendor/converters/officeparser/dist/utils/zipUtils.js +337 -0
- package/vendor/converters/officeparser/package.json +147 -0
- package/vendor/converters/upstream-manifest.json +124 -0
- package/vendor/dependency-fixes/README.md +77 -0
- package/vendor/dependency-fixes/vendor-converters.py +83 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.45.1.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.45.1.zip.sha256 +0 -1
- package/browser-extension/dist/chunk-627emwpj.js +0 -75
- /package/browser-extension/dist/{chunk-ydfx5d7p.css → chunk-z74y8n8c.css} +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ConversionResult, GeneratorConfig, OfficeParserAST } from '../types.js';
|
|
2
|
+
import { BaseGenerator } from './BaseGenerator.js';
|
|
3
|
+
/**
|
|
4
|
+
* Generates CSV files from an AST.
|
|
5
|
+
*/
|
|
6
|
+
export declare class CsvGenerator extends BaseGenerator<'csv'> {
|
|
7
|
+
constructor(ast: OfficeParserAST, config?: GeneratorConfig<'csv'>);
|
|
8
|
+
/**
|
|
9
|
+
* Generates CSV content from the provided AST.
|
|
10
|
+
*
|
|
11
|
+
* @returns A CSV string or a ZIP archive containing multiple CSVs
|
|
12
|
+
*/
|
|
13
|
+
generate(): Promise<ConversionResult<'csv'>>;
|
|
14
|
+
/**
|
|
15
|
+
* Recursively finds all nodes that can be treated as sheets (sheet or table).
|
|
16
|
+
*/
|
|
17
|
+
private collectSheetLikeNodes;
|
|
18
|
+
/**
|
|
19
|
+
* Renders a sheet or table node to raw row data.
|
|
20
|
+
*/
|
|
21
|
+
private renderNodeToRows;
|
|
22
|
+
/**
|
|
23
|
+
* Escapes a value for CSV formatting: RFC 4180 quoting plus a spreadsheet
|
|
24
|
+
* formula-injection guard (see csvSafeCell in ../utils/sanitize.js).
|
|
25
|
+
*/
|
|
26
|
+
private escapeCsvValue;
|
|
27
|
+
/** Sanitizes a value for a `#` comment line (document metadata or sheet name).
|
|
28
|
+
* Comment lines are free text prefixed with `#`, not RFC-4180 cells, so they
|
|
29
|
+
* can't be quoted; instead we (a) collapse newlines so the value can't break out
|
|
30
|
+
* and inject a new row, and (b) replace the column delimiter with a space so a
|
|
31
|
+
* value like `x,=1+1` can't split into a second cell that a spreadsheet would
|
|
32
|
+
* evaluate as a formula (CSV formula/DDE injection). */
|
|
33
|
+
private sanitizeComment;
|
|
34
|
+
/**
|
|
35
|
+
* Renders metadata as comments.
|
|
36
|
+
*/
|
|
37
|
+
private renderMetadata;
|
|
38
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CsvGenerator = void 0;
|
|
4
|
+
const fflate_1 = require("fflate");
|
|
5
|
+
const types_js_1 = require("../types.js");
|
|
6
|
+
const sheetUtils_js_1 = require("../utils/sheetUtils.js");
|
|
7
|
+
const sanitize_js_1 = require("../utils/sanitize.js");
|
|
8
|
+
const BaseGenerator_js_1 = require("./BaseGenerator.js");
|
|
9
|
+
/**
|
|
10
|
+
* Generates CSV files from an AST.
|
|
11
|
+
*/
|
|
12
|
+
class CsvGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
13
|
+
constructor(ast, config) {
|
|
14
|
+
super('csv', ast, config);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Generates CSV content from the provided AST.
|
|
18
|
+
*
|
|
19
|
+
* @returns A CSV string or a ZIP archive containing multiple CSVs
|
|
20
|
+
*/
|
|
21
|
+
async generate() {
|
|
22
|
+
const csvConfig = this.config.csvConfig;
|
|
23
|
+
const delimiter = csvConfig.columnDelimiter;
|
|
24
|
+
const mergeSheets = csvConfig.mergeSheets;
|
|
25
|
+
// 1. Collect all "sheet-like" nodes (sheets and tables)
|
|
26
|
+
const sheetNodes = await this.collectSheetLikeNodes(this.ast.content);
|
|
27
|
+
if (sheetNodes.length === 0) {
|
|
28
|
+
return { value: '', messages: this.messages };
|
|
29
|
+
}
|
|
30
|
+
const metadataHeader = this.config.renderMetadata ? this.renderMetadata(this.ast, delimiter) : '';
|
|
31
|
+
// 2. Filter sheets based on range
|
|
32
|
+
let selectedNodes = sheetNodes;
|
|
33
|
+
if (csvConfig.sheets) {
|
|
34
|
+
const indices = (0, sheetUtils_js_1.parseRangeString)(csvConfig.sheets);
|
|
35
|
+
selectedNodes = indices
|
|
36
|
+
.filter(i => i > 0 && i <= sheetNodes.length)
|
|
37
|
+
.map(i => sheetNodes[i - 1]);
|
|
38
|
+
}
|
|
39
|
+
if (selectedNodes.length === 0) {
|
|
40
|
+
this.warn(types_js_1.OfficeWarningType.SHEET_RANGE_NOT_FOUND, csvConfig.sheets);
|
|
41
|
+
return { value: '', messages: this.messages };
|
|
42
|
+
}
|
|
43
|
+
// 3. Generate CSV content for each selected node
|
|
44
|
+
const sheetData = [];
|
|
45
|
+
let globalMaxCols = 0;
|
|
46
|
+
for (let i = 0; i < selectedNodes.length; i++) {
|
|
47
|
+
const node = selectedNodes[i];
|
|
48
|
+
const name = node.metadata?.sheetName || `Sheet${i + 1}`;
|
|
49
|
+
const rows = await this.renderNodeToRows(node);
|
|
50
|
+
const maxCols = Math.max(...rows.map(r => r.length), 0);
|
|
51
|
+
if (mergeSheets) {
|
|
52
|
+
globalMaxCols = Math.max(globalMaxCols, maxCols);
|
|
53
|
+
}
|
|
54
|
+
sheetData.push({ name, rows });
|
|
55
|
+
}
|
|
56
|
+
// 4. Handle merging or separate files
|
|
57
|
+
if (mergeSheets) {
|
|
58
|
+
const mergedLines = [];
|
|
59
|
+
if (metadataHeader)
|
|
60
|
+
mergedLines.push(metadataHeader.trim());
|
|
61
|
+
for (const sheet of sheetData) {
|
|
62
|
+
if (sheetData.length > 1)
|
|
63
|
+
mergedLines.push(`# Sheet: ${this.sanitizeComment(sheet.name, delimiter)}`);
|
|
64
|
+
for (const row of sheet.rows) {
|
|
65
|
+
const paddedRow = [...row];
|
|
66
|
+
// Don't pad comments
|
|
67
|
+
if (row.length > 1 || (row.length === 1 && !row[0].startsWith('#'))) {
|
|
68
|
+
while (paddedRow.length < globalMaxCols)
|
|
69
|
+
paddedRow.push('');
|
|
70
|
+
}
|
|
71
|
+
mergedLines.push(paddedRow.map(v => this.escapeCsvValue(v, delimiter)).join(delimiter));
|
|
72
|
+
}
|
|
73
|
+
mergedLines.push(''); // Blank line between sheets
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
value: mergedLines.join('\n'),
|
|
77
|
+
messages: this.messages
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
else if (sheetData.length > 1) {
|
|
81
|
+
// Create a ZIP archive for multiple sheets
|
|
82
|
+
const zipFiles = {};
|
|
83
|
+
for (const sheet of sheetData) {
|
|
84
|
+
const sheetMaxCols = Math.max(...sheet.rows.map(r => r.length), 0);
|
|
85
|
+
const csvLines = [];
|
|
86
|
+
if (metadataHeader)
|
|
87
|
+
csvLines.push(metadataHeader.trim());
|
|
88
|
+
for (const row of sheet.rows) {
|
|
89
|
+
const paddedRow = [...row];
|
|
90
|
+
// Don't pad comments
|
|
91
|
+
if (row.length > 1 || (row.length === 1 && !row[0].startsWith('#'))) {
|
|
92
|
+
while (paddedRow.length < sheetMaxCols)
|
|
93
|
+
paddedRow.push('');
|
|
94
|
+
}
|
|
95
|
+
csvLines.push(paddedRow.map(v => this.escapeCsvValue(v, delimiter)).join(delimiter));
|
|
96
|
+
}
|
|
97
|
+
const fileName = `${sheet.name.replace(/[^\w\s-]/g, '_')}.csv`;
|
|
98
|
+
zipFiles[fileName] = new TextEncoder().encode(csvLines.join('\n'));
|
|
99
|
+
}
|
|
100
|
+
const zipBuffer = (0, fflate_1.zipSync)(zipFiles);
|
|
101
|
+
return {
|
|
102
|
+
value: zipBuffer,
|
|
103
|
+
messages: this.messages
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// Single sheet: return as plain string
|
|
108
|
+
const sheet = sheetData[0];
|
|
109
|
+
const sheetMaxCols = Math.max(...sheet.rows.map(r => r.length), 0);
|
|
110
|
+
const csvLines = [];
|
|
111
|
+
if (metadataHeader)
|
|
112
|
+
csvLines.push(metadataHeader.trim());
|
|
113
|
+
for (const row of sheet.rows) {
|
|
114
|
+
const paddedRow = [...row];
|
|
115
|
+
// Don't pad comments
|
|
116
|
+
if (row.length > 1 || (row.length === 1 && !row[0].startsWith('#'))) {
|
|
117
|
+
while (paddedRow.length < sheetMaxCols)
|
|
118
|
+
paddedRow.push('');
|
|
119
|
+
}
|
|
120
|
+
csvLines.push(paddedRow.map(v => this.escapeCsvValue(v, delimiter)).join(delimiter));
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
value: csvLines.join('\n'),
|
|
124
|
+
messages: this.messages
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Recursively finds all nodes that can be treated as sheets (sheet or table).
|
|
130
|
+
*/
|
|
131
|
+
async collectSheetLikeNodes(nodes) {
|
|
132
|
+
const result = [];
|
|
133
|
+
for (const node of nodes) {
|
|
134
|
+
const override = await this.handleOnNode(node);
|
|
135
|
+
if (override === false) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (node.type === 'sheet' || node.type === 'table') {
|
|
139
|
+
result.push(node);
|
|
140
|
+
}
|
|
141
|
+
else if (node.children) {
|
|
142
|
+
result.push(...(await this.collectSheetLikeNodes(node.children)));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Renders a sheet or table node to raw row data.
|
|
149
|
+
*/
|
|
150
|
+
async renderNodeToRows(node) {
|
|
151
|
+
if (!node.children)
|
|
152
|
+
return [];
|
|
153
|
+
const rows = [];
|
|
154
|
+
const rowNodes = node.children.filter(c => c.type === 'row' || c.type === 'comment');
|
|
155
|
+
// Text processor for cell content
|
|
156
|
+
const cellProcessor = (n, co) => {
|
|
157
|
+
if (n.type === 'text' || n.type === 'code')
|
|
158
|
+
return n.text || '';
|
|
159
|
+
if (n.type === 'break')
|
|
160
|
+
return '\n';
|
|
161
|
+
return co;
|
|
162
|
+
};
|
|
163
|
+
for (const rowNode of rowNodes) {
|
|
164
|
+
const override = await this.handleOnNode(rowNode);
|
|
165
|
+
if (override === false) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (typeof override === 'string') {
|
|
169
|
+
rows.push([override]);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (rowNode.type === 'comment') {
|
|
173
|
+
rows.push([rowNode.text || '']);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (!rowNode.children) {
|
|
177
|
+
rows.push([]);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const cellNodes = rowNode.children.filter(c => c.type === 'cell');
|
|
181
|
+
const rowValues = [];
|
|
182
|
+
let lastCol = -1;
|
|
183
|
+
for (const cell of cellNodes) {
|
|
184
|
+
const currentCol = cell.metadata?.col ?? (lastCol + 1);
|
|
185
|
+
// Fill gaps with empty strings
|
|
186
|
+
while (lastCol < currentCol - 1) {
|
|
187
|
+
rowValues.push('');
|
|
188
|
+
lastCol++;
|
|
189
|
+
}
|
|
190
|
+
const cellText = await this.processNodeRecursive(cell, cellProcessor);
|
|
191
|
+
rowValues.push(cellText);
|
|
192
|
+
// Handle colSpan: move lastCol forward
|
|
193
|
+
const colSpan = cell.metadata?.colSpan || 1;
|
|
194
|
+
lastCol = currentCol + colSpan - 1;
|
|
195
|
+
}
|
|
196
|
+
rows.push(rowValues);
|
|
197
|
+
}
|
|
198
|
+
return rows;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Escapes a value for CSV formatting: RFC 4180 quoting plus a spreadsheet
|
|
202
|
+
* formula-injection guard (see csvSafeCell in ../utils/sanitize.js).
|
|
203
|
+
*/
|
|
204
|
+
escapeCsvValue(val, delimiter) {
|
|
205
|
+
return (0, sanitize_js_1.csvSafeCell)(val, delimiter);
|
|
206
|
+
}
|
|
207
|
+
/** Sanitizes a value for a `#` comment line (document metadata or sheet name).
|
|
208
|
+
* Comment lines are free text prefixed with `#`, not RFC-4180 cells, so they
|
|
209
|
+
* can't be quoted; instead we (a) collapse newlines so the value can't break out
|
|
210
|
+
* and inject a new row, and (b) replace the column delimiter with a space so a
|
|
211
|
+
* value like `x,=1+1` can't split into a second cell that a spreadsheet would
|
|
212
|
+
* evaluate as a formula (CSV formula/DDE injection). */
|
|
213
|
+
sanitizeComment(val, delimiter) {
|
|
214
|
+
let s = String(val ?? '').replace(/[\r\n]+/g, ' ');
|
|
215
|
+
if (delimiter)
|
|
216
|
+
s = s.split(delimiter).join(' ');
|
|
217
|
+
return s;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Renders metadata as comments.
|
|
221
|
+
*/
|
|
222
|
+
renderMetadata(_ast, delimiter) {
|
|
223
|
+
// Via effectiveMetadata rather than the passed AST so `metadataOverrides` applies here
|
|
224
|
+
// as it does in every other generator.
|
|
225
|
+
const m = this.effectiveMetadata;
|
|
226
|
+
if (!m || Object.keys(m).length === 0)
|
|
227
|
+
return '';
|
|
228
|
+
let output = '';
|
|
229
|
+
if (m.title)
|
|
230
|
+
output += `# Title: ${this.sanitizeComment(m.title, delimiter)}\n`;
|
|
231
|
+
if (m.author)
|
|
232
|
+
output += `# Author: ${this.sanitizeComment(m.author, delimiter)}\n`;
|
|
233
|
+
if (m.created)
|
|
234
|
+
output += `# Created: ${this.sanitizeComment(new Date(m.created).toLocaleString(), delimiter)}\n`;
|
|
235
|
+
if (m.modified)
|
|
236
|
+
output += `# Modified: ${this.sanitizeComment(new Date(m.modified).toLocaleString(), delimiter)}\n`;
|
|
237
|
+
if (m.customProperties) {
|
|
238
|
+
for (const [k, v] of Object.entries(m.customProperties)) {
|
|
239
|
+
output += `# ${this.sanitizeComment(k, delimiter)}: ${this.sanitizeComment(String(v), delimiter)}\n`;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return output ? output + '\n' : '';
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
exports.CsvGenerator = CsvGenerator;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ConversionResult, GeneratorConfig, OfficeParserAST } from '../types.js';
|
|
2
|
+
import { BaseGenerator } from './BaseGenerator.js';
|
|
3
|
+
/**
|
|
4
|
+
* Generates a minimal, valid EPUB 3 file from an AST.
|
|
5
|
+
*
|
|
6
|
+
* Every AST node is rendered as a single XHTML content document (reusing `HtmlGenerator`
|
|
7
|
+
* for the actual markup, since EPUB content documents are XHTML) and packaged with the
|
|
8
|
+
* required `mimetype`, `META-INF/container.xml`, OPF manifest, and navigation document.
|
|
9
|
+
*
|
|
10
|
+
* `HtmlGenerator` embeds images as base64 `data:` URIs, but EPUB reading systems do not
|
|
11
|
+
* render `data:` URIs - images must be packaged as separate resources referenced by a
|
|
12
|
+
* relative path. So each data-URI image is extracted into `OEBPS/images/`, declared in
|
|
13
|
+
* the manifest, and its `<img src>` rewritten to point at the packaged file.
|
|
14
|
+
*/
|
|
15
|
+
export declare class EpubGenerator extends BaseGenerator<'epub'> {
|
|
16
|
+
constructor(ast: OfficeParserAST, config?: GeneratorConfig<'epub'>);
|
|
17
|
+
/**
|
|
18
|
+
* Resolves the modification instant used for both the EPUB 3 `dcterms:modified` property
|
|
19
|
+
* (which the specification requires) and every zip entry's mtime.
|
|
20
|
+
*
|
|
21
|
+
* Takes the value from `effectiveMetadata`, i.e. `metadataOverrides.modified` if the caller
|
|
22
|
+
* set one, otherwise the source document's own `metadata.modified`, and only falls back to
|
|
23
|
+
* the current time when neither exists. That last fallback is the sole non-reproducible
|
|
24
|
+
* option, so it is the last resort rather than the default.
|
|
25
|
+
*
|
|
26
|
+
* **Both outputs matter for reproducibility.** `dcterms:modified` is the visible one, but
|
|
27
|
+
* `zipSync` defaults each entry's mtime to `Date.now()`, so pinning only the OPF still
|
|
28
|
+
* yields archives that differ byte-for-byte on every run. That second source is easy to
|
|
29
|
+
* miss because DOS zip timestamps have two-second granularity - back-to-back generation
|
|
30
|
+
* looks stable and only a gap longer than that reveals it.
|
|
31
|
+
*
|
|
32
|
+
* `iso` is `YYYY-MM-DDThh:mm:ssZ` (UTC, whole seconds) as EPUB requires; `toISOString()`
|
|
33
|
+
* emits milliseconds, so they are stripped.
|
|
34
|
+
*/
|
|
35
|
+
private resolveModified;
|
|
36
|
+
/**
|
|
37
|
+
* Zip's DOS timestamp field cannot represent dates outside 1980-2099, and fflate throws
|
|
38
|
+
* rather than clamping. A document legitimately carrying a date outside that window (an
|
|
39
|
+
* unset/epoch-zero mtime is the common case) must not take EPUB generation down with it.
|
|
40
|
+
*/
|
|
41
|
+
private clampToZipRange;
|
|
42
|
+
generate(): Promise<ConversionResult<'epub'>>;
|
|
43
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EpubGenerator = void 0;
|
|
4
|
+
const fflate_1 = require("fflate");
|
|
5
|
+
const BaseGenerator_js_1 = require("./BaseGenerator.js");
|
|
6
|
+
const HtmlGenerator_js_1 = require("./HtmlGenerator.js");
|
|
7
|
+
const sanitize_js_1 = require("../utils/sanitize.js");
|
|
8
|
+
const VOID_TAGS = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
|
|
9
|
+
/** Block-level tags whose HTML5 content model does not permit them inside a <p>. */
|
|
10
|
+
const BLOCK_TAGS_INVALID_IN_P = ['div', 'table', 'ul', 'ol', 'dl', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'section', 'figure'];
|
|
11
|
+
/**
|
|
12
|
+
* HTML5's content model forbids block elements inside <p> (a <p> can only hold "phrasing
|
|
13
|
+
* content"), but browsers silently fix this via the HTML5 parsing algorithm's
|
|
14
|
+
* auto-closing rule: seeing a block start tag implicitly closes the open <p> first.
|
|
15
|
+
* XML parsers have no such rule - they just build the tree exactly as written. Since
|
|
16
|
+
* HtmlGenerator always wraps images in `<div class="image-container">`, a paragraph
|
|
17
|
+
* whose only child is an image becomes `<p><div>...</div></p>`: well-formed XML, but
|
|
18
|
+
* many EPUB rendering engines refuse to lay out a block box found inside a paragraph and
|
|
19
|
+
* simply drop it - silently, with no parse error, which is why the image vanishes.
|
|
20
|
+
*
|
|
21
|
+
* Fixes this by promoting any `<p ...>` that contains a nested block tag to a `<div ...>`
|
|
22
|
+
* instead, matching what a browser's auto-correction effectively produces. Paragraphs
|
|
23
|
+
* don't nest, so the first `</p>` after each `<p>` is always its match.
|
|
24
|
+
*/
|
|
25
|
+
const promoteParagraphsWithBlockContent = (html) => {
|
|
26
|
+
const blockTagPattern = new RegExp(`<(?:${BLOCK_TAGS_INVALID_IN_P.join('|')})\\b`, 'i');
|
|
27
|
+
let result = '';
|
|
28
|
+
let cursor = 0;
|
|
29
|
+
const pOpenRegex = /<p(\s[^>]*)?>/gi;
|
|
30
|
+
let match;
|
|
31
|
+
while ((match = pOpenRegex.exec(html)) !== null) {
|
|
32
|
+
if (match.index < cursor)
|
|
33
|
+
continue; // inside content already emitted by a prior promotion
|
|
34
|
+
result += html.slice(cursor, match.index);
|
|
35
|
+
const contentStart = match.index + match[0].length;
|
|
36
|
+
const closeMatch = /<\/p>/i.exec(html.slice(contentStart));
|
|
37
|
+
if (!closeMatch) {
|
|
38
|
+
// No closing tag found (shouldn't happen with well-formed generator output) -
|
|
39
|
+
// leave as-is rather than risk corrupting the rest of the document.
|
|
40
|
+
result += match[0];
|
|
41
|
+
cursor = contentStart;
|
|
42
|
+
pOpenRegex.lastIndex = cursor;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const inner = html.slice(contentStart, contentStart + closeMatch.index);
|
|
46
|
+
const attrs = match[1] || '';
|
|
47
|
+
result += blockTagPattern.test(inner) ? `<div${attrs}>${inner}</div>` : `<p${attrs}>${inner}</p>`;
|
|
48
|
+
cursor = contentStart + closeMatch.index + closeMatch[0].length;
|
|
49
|
+
pOpenRegex.lastIndex = cursor;
|
|
50
|
+
}
|
|
51
|
+
result += html.slice(cursor);
|
|
52
|
+
return result;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Converts HtmlGenerator's HTML output into well-formed XHTML, which EPUB reading
|
|
56
|
+
* systems parse as strict XML (unlike browsers, which tolerate HTML's looseness).
|
|
57
|
+
*
|
|
58
|
+
* This is more than cosmetic: a single raw `&` or unclosed tag makes the whole content
|
|
59
|
+
* document fail to open. The conversion:
|
|
60
|
+
* - strips `<script>` blocks — EpubGenerator renders through HtmlGenerator with
|
|
61
|
+
* `standalone: false`, which already omits the envelope-level stylesheet and Chart.js/
|
|
62
|
+
* spreadsheet scripts entirely, but a chart *node* still emits its own inline
|
|
63
|
+
* `<script>` (chart-init JS) regardless of that flag, since it's content, not envelope.
|
|
64
|
+
* A reading system can't execute it anyway, and its JS operators can contain raw `&`/`<`
|
|
65
|
+
* that are illegal as XML character data, so it's stripped here;
|
|
66
|
+
* - promotes `<p>` tags that contain nested block content (see
|
|
67
|
+
* promoteParagraphsWithBlockContent above) to `<div>`, since XML readers don't apply
|
|
68
|
+
* HTML5's auto-closing correction that hides this in a browser;
|
|
69
|
+
* - normalises HTML named entities (` `) to numeric references, since XML predefines
|
|
70
|
+
* only `&`/`<`/`>`/`"`/`'`;
|
|
71
|
+
* - escapes stray ampersands (e.g. in `href` query strings) not already part of a valid
|
|
72
|
+
* reference;
|
|
73
|
+
* - gives HTML boolean attributes an explicit value (`checked` -> `checked="checked"`);
|
|
74
|
+
* - self-closes void elements (`<br>` -> `<br/>`).
|
|
75
|
+
*/
|
|
76
|
+
const toXhtml = (html) => {
|
|
77
|
+
let out = html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '');
|
|
78
|
+
out = promoteParagraphsWithBlockContent(out);
|
|
79
|
+
// Named -> numeric entities (nbsp is the only named entity HtmlGenerator emits).
|
|
80
|
+
out = out.replace(/ /g, ' ');
|
|
81
|
+
// Escape ampersands that don't already open a valid XML entity reference.
|
|
82
|
+
out = out.replace(/&(?!(?:amp|lt|gt|quot|apos|#\d+|#x[0-9a-fA-F]+);)/g, '&');
|
|
83
|
+
// Give bare boolean attributes an explicit value. Scoped to the specific tags that
|
|
84
|
+
// emit them (checkbox task-list items, media iframes) so body text like "the selected
|
|
85
|
+
// option" is never rewritten.
|
|
86
|
+
out = out.replace(/<input\b([^>]*?)\schecked(\s*\/?>)/gi, '<input$1 checked="checked"$2');
|
|
87
|
+
out = out.replace(/<(iframe|video|audio)\b([^>]*?)\s(allowfullscreen|autoplay|controls|loop|muted)(\s*\/?>|\s)/gi, '<$1$2 $3="$3"$4');
|
|
88
|
+
// Self-close void elements (non-greedy attr capture so an already-present trailing `/`
|
|
89
|
+
// isn't duplicated, e.g. `<meta .../>` must not become `<meta ...//>`).
|
|
90
|
+
const voidTagPattern = new RegExp(`<(${VOID_TAGS.join('|')})((?:\\s[^>]*?)?)\\s*/?>`, 'gi');
|
|
91
|
+
out = out.replace(voidTagPattern, (_m, tag, attrs) => `<${tag}${attrs}/>`);
|
|
92
|
+
return out;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Minimal, book-friendly CSS injected into every EPUB content document. Kept static and
|
|
96
|
+
* free of `&`/`<` so it is XML-safe inline; the reading system supplies typography, so
|
|
97
|
+
* this only covers structural essentials the stripped page-chrome would otherwise lose.
|
|
98
|
+
*/
|
|
99
|
+
const EPUB_STYLESHEET = `img { max-width: 100%; height: auto; }
|
|
100
|
+
table { border-collapse: collapse; margin: 1em 0; }
|
|
101
|
+
td, th { border: 1px solid #ccc; padding: 4px 8px; }`;
|
|
102
|
+
/** Maps an image MIME type to a file extension for the packaged resource. */
|
|
103
|
+
const MIME_EXT = {
|
|
104
|
+
'image/jpeg': 'jpg', 'image/jpg': 'jpg', 'image/png': 'png', 'image/gif': 'gif',
|
|
105
|
+
'image/svg+xml': 'svg', 'image/webp': 'webp', 'image/bmp': 'bmp', 'image/tiff': 'tiff'
|
|
106
|
+
};
|
|
107
|
+
/** Decodes a base64 string to raw bytes, cross-env (atob exists in Node 16+ and browsers). */
|
|
108
|
+
const decodeBase64 = (b64) => {
|
|
109
|
+
const bin = atob(b64);
|
|
110
|
+
const bytes = new Uint8Array(bin.length);
|
|
111
|
+
for (let i = 0; i < bin.length; i++)
|
|
112
|
+
bytes[i] = bin.charCodeAt(i);
|
|
113
|
+
return bytes;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Generates a minimal, valid EPUB 3 file from an AST.
|
|
117
|
+
*
|
|
118
|
+
* Every AST node is rendered as a single XHTML content document (reusing `HtmlGenerator`
|
|
119
|
+
* for the actual markup, since EPUB content documents are XHTML) and packaged with the
|
|
120
|
+
* required `mimetype`, `META-INF/container.xml`, OPF manifest, and navigation document.
|
|
121
|
+
*
|
|
122
|
+
* `HtmlGenerator` embeds images as base64 `data:` URIs, but EPUB reading systems do not
|
|
123
|
+
* render `data:` URIs - images must be packaged as separate resources referenced by a
|
|
124
|
+
* relative path. So each data-URI image is extracted into `OEBPS/images/`, declared in
|
|
125
|
+
* the manifest, and its `<img src>` rewritten to point at the packaged file.
|
|
126
|
+
*/
|
|
127
|
+
class EpubGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
128
|
+
constructor(ast, config) {
|
|
129
|
+
super('epub', ast, config);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Resolves the modification instant used for both the EPUB 3 `dcterms:modified` property
|
|
133
|
+
* (which the specification requires) and every zip entry's mtime.
|
|
134
|
+
*
|
|
135
|
+
* Takes the value from `effectiveMetadata`, i.e. `metadataOverrides.modified` if the caller
|
|
136
|
+
* set one, otherwise the source document's own `metadata.modified`, and only falls back to
|
|
137
|
+
* the current time when neither exists. That last fallback is the sole non-reproducible
|
|
138
|
+
* option, so it is the last resort rather than the default.
|
|
139
|
+
*
|
|
140
|
+
* **Both outputs matter for reproducibility.** `dcterms:modified` is the visible one, but
|
|
141
|
+
* `zipSync` defaults each entry's mtime to `Date.now()`, so pinning only the OPF still
|
|
142
|
+
* yields archives that differ byte-for-byte on every run. That second source is easy to
|
|
143
|
+
* miss because DOS zip timestamps have two-second granularity - back-to-back generation
|
|
144
|
+
* looks stable and only a gap longer than that reveals it.
|
|
145
|
+
*
|
|
146
|
+
* `iso` is `YYYY-MM-DDThh:mm:ssZ` (UTC, whole seconds) as EPUB requires; `toISOString()`
|
|
147
|
+
* emits milliseconds, so they are stripped.
|
|
148
|
+
*/
|
|
149
|
+
resolveModified() {
|
|
150
|
+
const raw = this.effectiveMetadata.modified;
|
|
151
|
+
let resolved = null;
|
|
152
|
+
if (raw instanceof Date && !isNaN(raw.getTime())) {
|
|
153
|
+
resolved = raw;
|
|
154
|
+
}
|
|
155
|
+
else if (typeof raw === 'string' && raw !== '') {
|
|
156
|
+
// A parser may hand back a date-like string rather than a Date.
|
|
157
|
+
const parsed = new Date(raw);
|
|
158
|
+
if (!isNaN(parsed.getTime()))
|
|
159
|
+
resolved = parsed;
|
|
160
|
+
}
|
|
161
|
+
resolved ??= new Date();
|
|
162
|
+
return {
|
|
163
|
+
iso: resolved.toISOString().replace(/\.\d+Z$/, 'Z'),
|
|
164
|
+
mtime: this.clampToZipRange(resolved),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Zip's DOS timestamp field cannot represent dates outside 1980-2099, and fflate throws
|
|
169
|
+
* rather than clamping. A document legitimately carrying a date outside that window (an
|
|
170
|
+
* unset/epoch-zero mtime is the common case) must not take EPUB generation down with it.
|
|
171
|
+
*/
|
|
172
|
+
clampToZipRange(date) {
|
|
173
|
+
const MIN = Date.UTC(1980, 0, 1);
|
|
174
|
+
const MAX = Date.UTC(2099, 11, 31, 23, 59, 59);
|
|
175
|
+
const t = date.getTime();
|
|
176
|
+
if (t < MIN)
|
|
177
|
+
return new Date(MIN);
|
|
178
|
+
if (t > MAX)
|
|
179
|
+
return new Date(MAX);
|
|
180
|
+
return date;
|
|
181
|
+
}
|
|
182
|
+
async generate() {
|
|
183
|
+
const htmlGenerator = new HtmlGenerator_js_1.HtmlGenerator(this.ast, {
|
|
184
|
+
...this.config,
|
|
185
|
+
// Force sourceAttributes off: those data-* attributes are wire-format plumbing for
|
|
186
|
+
// structured consumers and change the mermaid shape's rendered appearance, neither of
|
|
187
|
+
// which belongs in a packaged EPUB.
|
|
188
|
+
htmlConfig: { ...this.config.htmlConfig, standalone: false, sourceAttributes: false },
|
|
189
|
+
});
|
|
190
|
+
const htmlResult = await htmlGenerator.generate();
|
|
191
|
+
let bodyHtml = typeof htmlResult.value === 'string' ? htmlResult.value : '';
|
|
192
|
+
// Extract base64 data-URI images into packaged files (EPUB readers don't render
|
|
193
|
+
// `data:` URIs). Each distinct image becomes one OEBPS/images/imageN.ext resource,
|
|
194
|
+
// a manifest <item>, and a rewritten relative `src`. Deduped so a repeated image
|
|
195
|
+
// is packaged once.
|
|
196
|
+
const imageResources = {};
|
|
197
|
+
const imageManifestItems = [];
|
|
198
|
+
const dataUriToHref = new Map();
|
|
199
|
+
let imageCounter = 0;
|
|
200
|
+
bodyHtml = bodyHtml.replace(/(<img\b[^>]*\bsrc=")(data:(image\/[a-zA-Z0-9.+-]+);base64,([^"]+))(")/gi, (_full, pre, dataUri, mime, b64, post) => {
|
|
201
|
+
let href = dataUriToHref.get(dataUri);
|
|
202
|
+
if (!href) {
|
|
203
|
+
imageCounter++;
|
|
204
|
+
const ext = MIME_EXT[mime.toLowerCase()] || 'img';
|
|
205
|
+
href = `images/image${imageCounter}.${ext}`;
|
|
206
|
+
dataUriToHref.set(dataUri, href);
|
|
207
|
+
try {
|
|
208
|
+
imageResources[`OEBPS/${href}`] = decodeBase64(b64);
|
|
209
|
+
imageManifestItems.push(`<item id="img${imageCounter}" href="${href}" media-type="${mime}"/>`);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// Undecodable data - leave the original src untouched rather than
|
|
213
|
+
// emit a manifest entry for a resource we couldn't write.
|
|
214
|
+
dataUriToHref.delete(dataUri);
|
|
215
|
+
return `${pre}${dataUri}${post}`;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return `${pre}${href}${post}`;
|
|
219
|
+
});
|
|
220
|
+
const xhtmlBody = toXhtml(bodyHtml);
|
|
221
|
+
// Via effectiveMetadata so `metadataOverrides` applies here as it does everywhere else.
|
|
222
|
+
const meta = this.effectiveMetadata;
|
|
223
|
+
const title = meta.title || 'Untitled';
|
|
224
|
+
const author = meta.author;
|
|
225
|
+
const description = meta.description;
|
|
226
|
+
// dc:subject is the OPF's only vocabulary slot for either of these; both are repeatable.
|
|
227
|
+
const subject = meta.subject;
|
|
228
|
+
const keywords = meta.keywords;
|
|
229
|
+
const nativeProps = (meta.nativeProperties || {});
|
|
230
|
+
const language = nativeProps.language || 'en';
|
|
231
|
+
// OPF metadata is a closed Dublin Core vocabulary: a caller-defined key has no valid
|
|
232
|
+
// element to live in, and inventing one risks failing EPUB validation outright.
|
|
233
|
+
this.warnUnrepresentableCustomMetadata('EPUB');
|
|
234
|
+
const identifier = nativeProps.identifier || `urn:x-officeparser:${this.slugify(title)}-${xhtmlBody.length}`;
|
|
235
|
+
const { iso: modified, mtime } = this.resolveModified();
|
|
236
|
+
const chapterXhtml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
237
|
+
<!DOCTYPE html>
|
|
238
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="${(0, sanitize_js_1.escapeXml)(language)}">
|
|
239
|
+
<head>
|
|
240
|
+
<meta charset="utf-8"/>
|
|
241
|
+
<title>${(0, sanitize_js_1.escapeXml)(title)}</title>
|
|
242
|
+
<style type="text/css">
|
|
243
|
+
${EPUB_STYLESHEET}
|
|
244
|
+
</style>
|
|
245
|
+
</head>
|
|
246
|
+
<body>
|
|
247
|
+
${xhtmlBody}
|
|
248
|
+
</body>
|
|
249
|
+
</html>`;
|
|
250
|
+
// Built as a list rather than inline ternaries so an absent field contributes nothing at
|
|
251
|
+
// all. Inline `${x ? ... : ''}` leaves the surrounding indentation and newline behind, so
|
|
252
|
+
// every optional field the document lacks used to emit a stray blank line into the OPF.
|
|
253
|
+
const optionalDcElements = [
|
|
254
|
+
author ? `<dc:creator>${(0, sanitize_js_1.escapeXml)(author)}</dc:creator>` : '',
|
|
255
|
+
description ? `<dc:description>${(0, sanitize_js_1.escapeXml)(description)}</dc:description>` : '',
|
|
256
|
+
// dc:subject is repeatable and is the OPF's only slot for either of these.
|
|
257
|
+
subject ? `<dc:subject>${(0, sanitize_js_1.escapeXml)(subject)}</dc:subject>` : '',
|
|
258
|
+
keywords ? `<dc:subject>${(0, sanitize_js_1.escapeXml)(keywords)}</dc:subject>` : '',
|
|
259
|
+
].filter(Boolean).map(el => ` ${el}`).join('\n');
|
|
260
|
+
const opf = `<?xml version="1.0" encoding="UTF-8"?>
|
|
261
|
+
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="pub-id">
|
|
262
|
+
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
|
|
263
|
+
<dc:identifier id="pub-id">${(0, sanitize_js_1.escapeXml)(identifier)}</dc:identifier>
|
|
264
|
+
<dc:title>${(0, sanitize_js_1.escapeXml)(title)}</dc:title>
|
|
265
|
+
${optionalDcElements}
|
|
266
|
+
<dc:language>${(0, sanitize_js_1.escapeXml)(language)}</dc:language>
|
|
267
|
+
<meta property="dcterms:modified">${modified}</meta>
|
|
268
|
+
</metadata>
|
|
269
|
+
<manifest>
|
|
270
|
+
<item id="chapter1" href="chapter1.xhtml" media-type="application/xhtml+xml"/>
|
|
271
|
+
<item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>${imageManifestItems.length ? '\n ' + imageManifestItems.join('\n ') : ''}
|
|
272
|
+
</manifest>
|
|
273
|
+
<spine>
|
|
274
|
+
<itemref idref="chapter1"/>
|
|
275
|
+
</spine>
|
|
276
|
+
</package>`;
|
|
277
|
+
const navXhtml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
278
|
+
<!DOCTYPE html>
|
|
279
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
|
|
280
|
+
<head><meta charset="utf-8"/><title>Navigation</title></head>
|
|
281
|
+
<body>
|
|
282
|
+
<nav epub:type="toc" id="toc">
|
|
283
|
+
<h1>${(0, sanitize_js_1.escapeXml)(title)}</h1>
|
|
284
|
+
<ol>
|
|
285
|
+
<li><a href="chapter1.xhtml">${(0, sanitize_js_1.escapeXml)(title)}</a></li>
|
|
286
|
+
</ol>
|
|
287
|
+
</nav>
|
|
288
|
+
</body>
|
|
289
|
+
</html>`;
|
|
290
|
+
const containerXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
291
|
+
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
|
292
|
+
<rootfiles>
|
|
293
|
+
<rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
|
|
294
|
+
</rootfiles>
|
|
295
|
+
</container>`;
|
|
296
|
+
const encoder = new TextEncoder();
|
|
297
|
+
// EPUB requires the mimetype entry to be the first file in the archive, stored
|
|
298
|
+
// uncompressed (level 0) - readers use it to sniff the format before parsing any XML.
|
|
299
|
+
const zipFiles = {
|
|
300
|
+
mimetype: [encoder.encode('application/epub+zip'), { level: 0 }],
|
|
301
|
+
'META-INF/container.xml': encoder.encode(containerXml),
|
|
302
|
+
'OEBPS/content.opf': encoder.encode(opf),
|
|
303
|
+
'OEBPS/nav.xhtml': encoder.encode(navXhtml),
|
|
304
|
+
'OEBPS/chapter1.xhtml': encoder.encode(chapterXhtml),
|
|
305
|
+
...imageResources,
|
|
306
|
+
};
|
|
307
|
+
return {
|
|
308
|
+
// An explicit mtime is what makes the archive reproducible; fflate would otherwise
|
|
309
|
+
// stamp every entry with Date.now(). See resolveModified().
|
|
310
|
+
value: (0, fflate_1.zipSync)(zipFiles, { mtime }),
|
|
311
|
+
messages: this.messages,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
exports.EpubGenerator = EpubGenerator;
|