@nowline/export 0.8.6 → 0.10.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/dist/index.d.ts +86 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +201 -159
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
- package/src/index.ts +294 -201
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
|
+
import { type ResolveDiagnostic } from '@nowline/core';
|
|
1
2
|
import type { ResolvedFontPair } from '@nowline/export-core';
|
|
2
|
-
import {
|
|
3
|
+
import type { NonWorkingDisplay, ThemeName } from '@nowline/layout';
|
|
4
|
+
import { type StagedRoadmap, type StageInputs, type StageResult } from '@nowline/renderer';
|
|
3
5
|
export type { PdfOrientation, ResolvedFontPair } from '@nowline/export-core';
|
|
4
|
-
export type { ThemeName } from '@nowline/layout';
|
|
6
|
+
export type { NonWorkingDisplay, ThemeName } from '@nowline/layout';
|
|
5
7
|
export { civilDateInZone, type NormalizedZone, normalizeZone, type ResolveTodayOptions, resolveToday, TimezoneError, } from '@nowline/layout';
|
|
8
|
+
export type { StagedRoadmap, StageResult } from '@nowline/renderer';
|
|
9
|
+
/**
|
|
10
|
+
* Thrown when include resolution reports a wave-rule error (a coded or
|
|
11
|
+
* `rule: 'wave'` diagnostic, specs/waves.md §6.1) and no other include error.
|
|
12
|
+
* `message` is the same `@nowline/export: include error in …` text as the
|
|
13
|
+
* plain include error; `diagnostics` carries every resolver diagnostic, errors
|
|
14
|
+
* and warnings, so a surface can report the routed ones (see
|
|
15
|
+
* `isRoutedResolveDiagnostic`) with file and line, like validator diagnostics.
|
|
16
|
+
* Every other include error stays a plain `Error`.
|
|
17
|
+
*/
|
|
18
|
+
export declare class IncludeResolveError extends Error {
|
|
19
|
+
readonly diagnostics: ResolveDiagnostic[];
|
|
20
|
+
constructor(message: string, diagnostics: ResolveDiagnostic[]);
|
|
21
|
+
}
|
|
6
22
|
/** The eight canonical export formats. Every surface produces exactly these. */
|
|
7
23
|
export type ExportFormat = 'svg' | 'png' | 'pdf' | 'html' | 'mermaid' | 'xlsx' | 'msproj' | 'json';
|
|
8
24
|
/**
|
|
@@ -46,14 +62,26 @@ export interface RenderInputs {
|
|
|
46
62
|
* Optional for text-only formats (svg, html, json, mermaid, msproj, xlsx).
|
|
47
63
|
*/
|
|
48
64
|
fonts?: ResolvedFontPair;
|
|
49
|
-
/** Optional
|
|
65
|
+
/** Optional best-effort maximum canvas width in px; unset fits the content. */
|
|
50
66
|
width?: number;
|
|
67
|
+
/**
|
|
68
|
+
* How the chart shows a calendar's non-working days (`hide` collapses them,
|
|
69
|
+
* `show` draws shaded bands). Wins over the file's
|
|
70
|
+
* `default roadmap non-working:` key; leave it unset (undefined) to let the
|
|
71
|
+
* key apply, then `hide`. The schedule exporters (mermaid, msproj, xlsx)
|
|
72
|
+
* take no display input, so their bytes are the same in both views.
|
|
73
|
+
*/
|
|
74
|
+
nonWorking?: NonWorkingDisplay;
|
|
51
75
|
/** Drop `<a>` links from the rendered SVG. */
|
|
52
76
|
noLinks?: boolean;
|
|
53
77
|
/** Treat render warnings (e.g. missing assets) as hard errors. */
|
|
54
78
|
strict?: boolean;
|
|
55
|
-
/**
|
|
56
|
-
|
|
79
|
+
/**
|
|
80
|
+
* PDF page size: a preset name, a custom dimensions string, or a value
|
|
81
|
+
* already parsed with `parsePageSize` (so a caller can reject a bad one
|
|
82
|
+
* before rendering).
|
|
83
|
+
*/
|
|
84
|
+
pageSize?: import('@nowline/export-core').PdfPageSize | string;
|
|
57
85
|
/** PDF orientation. */
|
|
58
86
|
orientation?: import('@nowline/export-core').PdfOrientation;
|
|
59
87
|
/** PDF margin in points. */
|
|
@@ -62,19 +90,68 @@ export interface RenderInputs {
|
|
|
62
90
|
pngScale?: number;
|
|
63
91
|
/** MS Project start-date override (YYYY-MM-DD). */
|
|
64
92
|
msprojStart?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Called before layout with the routed (wave-rule) resolver diagnostics
|
|
95
|
+
* when include resolution succeeds, which leaves only warnings (possibly
|
|
96
|
+
* none). Errors throw `IncludeResolveError` instead. Never affects output.
|
|
97
|
+
*/
|
|
98
|
+
onResolveDiagnostics?: (diagnostics: ResolveDiagnostic[]) => void;
|
|
99
|
+
/**
|
|
100
|
+
* Receives non-fatal notices about the artifact, such as the MS Project
|
|
101
|
+
* lossy-feature summary. Never affects output. Omit to keep each
|
|
102
|
+
* exporter's default (stderr), as before.
|
|
103
|
+
*/
|
|
104
|
+
onNotice?: (message: string) => void;
|
|
105
|
+
/**
|
|
106
|
+
* Receives asset warnings, such as a `logo:` file that is missing or
|
|
107
|
+
* unreadable (the render then proceeds without the logo) or SVG logo
|
|
108
|
+
* content the sanitizer removed. Under `strict` they throw instead and
|
|
109
|
+
* this is not called.
|
|
110
|
+
*/
|
|
111
|
+
onWarning?: (message: string) => void;
|
|
112
|
+
}
|
|
113
|
+
/** What a caller that has already parsed `source` may hand to {@link stageForExport}. */
|
|
114
|
+
export interface StageForExportOptions {
|
|
115
|
+
/** A document the caller already parsed and validated from `source`. */
|
|
116
|
+
document?: StageInputs['document'];
|
|
117
|
+
/** The caller's Langium services, which built `document`. Omitted: the kernel's own. */
|
|
118
|
+
services?: StageInputs['services'];
|
|
65
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Stage `source` the way the kernel does (`stageRoadmap` with the host's
|
|
122
|
+
* source and asset reads, laid out with `inputs`) and return the raw result:
|
|
123
|
+
* `invalid`, `include-error` or `staged`. A surface that reports the stage's
|
|
124
|
+
* own diagnostics maps them itself, then hands a `staged` result to
|
|
125
|
+
* {@link exportStaged}; {@link acceptStagedRoadmap} is the kernel's mapping.
|
|
126
|
+
*/
|
|
127
|
+
export declare function stageForExport(source: string, inputs: RenderInputs, host: HostEnv, options?: StageForExportOptions): Promise<StageResult>;
|
|
128
|
+
/**
|
|
129
|
+
* Map a stage result to the kernel's contract: validation and include errors
|
|
130
|
+
* throw, routed warnings reach `onResolveDiagnostics`, and logo warnings
|
|
131
|
+
* reach `onWarning` (or throw under `strict`). Returns the staged roadmap.
|
|
132
|
+
*/
|
|
133
|
+
export declare function acceptStagedRoadmap(stage: StageResult, inputs: RenderInputs): StagedRoadmap;
|
|
66
134
|
/**
|
|
67
135
|
* Produce the export artifact for `source` in `format`. This is the single
|
|
68
136
|
* code path to any exported artifact — no surface re-implements a stage.
|
|
137
|
+
* It is `stageRoadmap` plus the kernel's error mapping, then `exportStaged`.
|
|
69
138
|
*
|
|
70
139
|
* Text formats (svg / json / html / mermaid / msproj) are returned as UTF-8
|
|
71
140
|
* bytes; binary formats (png / pdf / xlsx) as their native bytes. Returning
|
|
72
141
|
* Uint8Array for every format keeps the determinism gate able to `sha256` one
|
|
73
142
|
* value per (fixture, format).
|
|
74
|
-
*
|
|
75
|
-
* Note: PNG rasterizes via @nowline/export-png (native resvg-js) until s4
|
|
76
|
-
* swaps it to @resvg/resvg-wasm. The `host.loadWasm()` seam is reserved for
|
|
77
|
-
* that swap and is not called here yet.
|
|
78
143
|
*/
|
|
79
144
|
export declare function exportDocument(source: string, format: ExportFormat, inputs: RenderInputs, host: HostEnv): Promise<Uint8Array>;
|
|
145
|
+
/**
|
|
146
|
+
* Export a roadmap the caller already staged with `stageRoadmap`, so a
|
|
147
|
+
* surface that stages once (for diagnostics or layout insights) does not
|
|
148
|
+
* parse and lay out again. The stage must have been laid out with the same
|
|
149
|
+
* theme, today, width, locale and non-working display as `inputs`; a
|
|
150
|
+
* mismatch throws. The caller owns the stage's own errors and warnings
|
|
151
|
+
* (`exportDocument` maps them to the kernel's contract first); this applies
|
|
152
|
+
* `noLinks`, `strict` and `onWarning` to the render only.
|
|
153
|
+
*
|
|
154
|
+
* The SVG is rendered only for the formats that use it (svg, html, png, pdf).
|
|
155
|
+
*/
|
|
156
|
+
export declare function exportStaged(stage: StagedRoadmap, format: ExportFormat, inputs: RenderInputs, host: HostEnv): Promise<Uint8Array>;
|
|
80
157
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// @nowline/export — the shared export kernel.
|
|
2
2
|
//
|
|
3
3
|
// The single code path from `.nowline` source to any of the eight export
|
|
4
|
-
// artifacts (parse → resolve includes →
|
|
4
|
+
// artifacts (stageRoadmap: parse → resolve includes → load logo → schedule →
|
|
5
|
+
// layout; then renderSvg → format adapter).
|
|
5
6
|
// Every surface — the CLI, the `@nowline/mcp` server, the VS Code extension,
|
|
6
7
|
// and the Free/Pro web apps — runs this same code with the same inputs, so
|
|
7
8
|
// they all produce byte-for-byte identical output for the same source and the
|
|
@@ -10,84 +11,47 @@
|
|
|
10
11
|
// Spec: specs/export-determinism.md (the byte-identity precedent and the
|
|
11
12
|
// HostEnv / RenderInputs contract this surface implements).
|
|
12
13
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { renderSvg } from '@nowline/renderer';
|
|
18
|
-
import { URI } from 'langium';
|
|
14
|
+
// Staging is `@nowline/renderer`'s `stageRoadmap` (specs/surface-options.md
|
|
15
|
+
// §3.3); `exportStaged` exports a roadmap a caller already staged.
|
|
16
|
+
import { createNowlineServices, serializeToJson, } from '@nowline/core';
|
|
17
|
+
import { renderSvg, stageRoadmap, } from '@nowline/renderer';
|
|
19
18
|
export { civilDateInZone, normalizeZone, resolveToday, TimezoneError, } from '@nowline/layout';
|
|
19
|
+
/**
|
|
20
|
+
* Thrown when include resolution reports a wave-rule error (a coded or
|
|
21
|
+
* `rule: 'wave'` diagnostic, specs/waves.md §6.1) and no other include error.
|
|
22
|
+
* `message` is the same `@nowline/export: include error in …` text as the
|
|
23
|
+
* plain include error; `diagnostics` carries every resolver diagnostic, errors
|
|
24
|
+
* and warnings, so a surface can report the routed ones (see
|
|
25
|
+
* `isRoutedResolveDiagnostic`) with file and line, like validator diagnostics.
|
|
26
|
+
* Every other include error stays a plain `Error`.
|
|
27
|
+
*/
|
|
28
|
+
export class IncludeResolveError extends Error {
|
|
29
|
+
diagnostics;
|
|
30
|
+
constructor(message, diagnostics) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = 'IncludeResolveError';
|
|
33
|
+
this.diagnostics = diagnostics;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
20
36
|
let cachedServices;
|
|
21
|
-
let docCounter = 0;
|
|
22
37
|
function getExportServices() {
|
|
23
38
|
if (!cachedServices)
|
|
24
39
|
cachedServices = createNowlineServices();
|
|
25
40
|
return cachedServices;
|
|
26
41
|
}
|
|
27
|
-
|
|
28
|
-
// Always mint a fresh URI — re-using one lets Langium mutate the prior
|
|
29
|
-
// document in place, causing spurious diagnostics on repeated exports.
|
|
30
|
-
return URI.parse(`memory:///kernel-${++docCounter}.nowline`);
|
|
31
|
-
}
|
|
32
|
-
// ---- Inline JSON serializer ---------------------------------------------------
|
|
42
|
+
// ---- JSON serializer ---------------------------------------------------------
|
|
33
43
|
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
function isAstNode(v) {
|
|
40
|
-
return (v !== null && typeof v === 'object' && typeof v.$type === 'string');
|
|
41
|
-
}
|
|
42
|
-
function serializeNode(node, includePositions) {
|
|
43
|
-
const out = { $type: node.$type };
|
|
44
|
-
if (includePositions && node.$cstNode) {
|
|
45
|
-
const cst = node.$cstNode;
|
|
46
|
-
out.$position = {
|
|
47
|
-
start: {
|
|
48
|
-
line: cst.range.start.line + 1,
|
|
49
|
-
column: cst.range.start.character + 1,
|
|
50
|
-
offset: cst.offset,
|
|
51
|
-
},
|
|
52
|
-
end: {
|
|
53
|
-
line: cst.range.end.line + 1,
|
|
54
|
-
column: cst.range.end.character + 1,
|
|
55
|
-
offset: cst.end,
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
for (const [key, value] of Object.entries(node)) {
|
|
60
|
-
if (key.startsWith('$') || CONTAINER_KEYS.has(key) || RUNTIME_KEYS.has(key))
|
|
61
|
-
continue;
|
|
62
|
-
out[key] = serializeValue(value, includePositions);
|
|
63
|
-
}
|
|
64
|
-
return out;
|
|
65
|
-
}
|
|
66
|
-
function serializeValue(value, includePositions) {
|
|
67
|
-
if (value === null || value === undefined)
|
|
68
|
-
return value;
|
|
69
|
-
if (Array.isArray(value))
|
|
70
|
-
return value.map((v) => serializeValue(v, includePositions));
|
|
71
|
-
if (isAstNode(value))
|
|
72
|
-
return serializeNode(value, includePositions);
|
|
73
|
-
if (typeof value === 'object') {
|
|
74
|
-
const out = {};
|
|
75
|
-
for (const [k, v] of Object.entries(value)) {
|
|
76
|
-
if (k.startsWith('$') || CONTAINER_KEYS.has(k) || RUNTIME_KEYS.has(k))
|
|
77
|
-
continue;
|
|
78
|
-
out[k] = serializeValue(v, includePositions);
|
|
79
|
-
}
|
|
80
|
-
return out;
|
|
81
|
-
}
|
|
82
|
-
return value;
|
|
83
|
-
}
|
|
44
|
+
// Delegates to core's `serializeToJson`. The one envelope difference: the
|
|
45
|
+
// stage parses from a synthetic `memory:///stage-N.nowline` URI, so it
|
|
46
|
+
// overrides `file.uri` with the raw `file://${sourcePath}` concatenation —
|
|
47
|
+
// not `URI.file()`, which would percent-encode and change the bytes the
|
|
48
|
+
// determinism gate hashes.
|
|
84
49
|
function serializeDocumentToJson(document, source, sourcePath) {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}, null, 2);
|
|
50
|
+
const doc = serializeToJson(document, source, {
|
|
51
|
+
includePositions: true,
|
|
52
|
+
uri: `file://${sourcePath}`,
|
|
53
|
+
});
|
|
54
|
+
return JSON.stringify(doc, null, 2);
|
|
91
55
|
}
|
|
92
56
|
// ---- MIME inference ----------------------------------------------------------
|
|
93
57
|
function guessMime(ref) {
|
|
@@ -102,23 +66,46 @@ function guessMime(ref) {
|
|
|
102
66
|
return 'image/webp';
|
|
103
67
|
return 'application/octet-stream';
|
|
104
68
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
* preserving while making the SVG explicit.
|
|
111
|
-
*/
|
|
112
|
-
function pinnedFamilies(fonts) {
|
|
113
|
-
return { sans: fonts.sans.name, serif: fonts.sans.name, mono: fonts.mono.name };
|
|
69
|
+
function hostAssetResolver(host) {
|
|
70
|
+
return async (ref) => {
|
|
71
|
+
const bytes = await host.readAsset(ref);
|
|
72
|
+
return { bytes, mime: guessMime(ref) };
|
|
73
|
+
};
|
|
114
74
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
75
|
+
// ---- Argument checks ---------------------------------------------------------
|
|
76
|
+
function checkHostAndInputs(host, inputs) {
|
|
77
|
+
if (typeof host?.readSource !== 'function' ||
|
|
78
|
+
typeof host.readAsset !== 'function' ||
|
|
79
|
+
typeof host.loadWasm !== 'function') {
|
|
80
|
+
throw new TypeError('@nowline/export: host must implement the HostEnv interface.');
|
|
81
|
+
}
|
|
82
|
+
if (!(inputs?.today instanceof Date) && inputs?.today !== undefined) {
|
|
83
|
+
throw new TypeError('@nowline/export: inputs.today must be a Date (UTC midnight) or undefined.');
|
|
84
|
+
}
|
|
85
|
+
if (typeof inputs?.sourcePath !== 'string' || inputs.sourcePath.length === 0) {
|
|
86
|
+
throw new TypeError('@nowline/export: inputs.sourcePath must be a non-empty string.');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const STAGE_LAYOUT_KEYS = ['theme', 'today', 'width', 'locale', 'nonWorking'];
|
|
90
|
+
function sameLayoutValue(a, b) {
|
|
91
|
+
if (a instanceof Date || b instanceof Date) {
|
|
92
|
+
return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
|
|
93
|
+
}
|
|
94
|
+
return a === b;
|
|
95
|
+
}
|
|
96
|
+
/** A stage laid out with other options would export a different chart than `inputs` asks for. */
|
|
97
|
+
function checkStageMatches(stage, inputs) {
|
|
98
|
+
for (const key of STAGE_LAYOUT_KEYS) {
|
|
99
|
+
if (!sameLayoutValue(stage.layout[key], inputs[key])) {
|
|
100
|
+
throw new Error(`@nowline/export: the staged roadmap was laid out with a different ${key} than inputs.${key}.`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// ---- Staging -----------------------------------------------------------------
|
|
105
|
+
/** Lexer, parser and validator error messages, in document order. */
|
|
106
|
+
function errorMessages(diagnostics) {
|
|
120
107
|
const errors = [];
|
|
121
|
-
for (const raw of
|
|
108
|
+
for (const raw of diagnostics) {
|
|
122
109
|
if (raw.origin === 'lexer') {
|
|
123
110
|
errors.push(raw.error.message);
|
|
124
111
|
}
|
|
@@ -129,58 +116,92 @@ async function stageDocument(source, inputs, host) {
|
|
|
129
116
|
errors.push(raw.diagnostic.message);
|
|
130
117
|
}
|
|
131
118
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
119
|
+
return errors;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Stage `source` the way the kernel does (`stageRoadmap` with the host's
|
|
123
|
+
* source and asset reads, laid out with `inputs`) and return the raw result:
|
|
124
|
+
* `invalid`, `include-error` or `staged`. A surface that reports the stage's
|
|
125
|
+
* own diagnostics maps them itself, then hands a `staged` result to
|
|
126
|
+
* {@link exportStaged}; {@link acceptStagedRoadmap} is the kernel's mapping.
|
|
127
|
+
*/
|
|
128
|
+
export function stageForExport(source, inputs, host, options = {}) {
|
|
129
|
+
return stageRoadmap({
|
|
130
|
+
source,
|
|
131
|
+
sourcePath: inputs.sourcePath,
|
|
132
|
+
services: options.services ?? getExportServices(),
|
|
133
|
+
readSource: host.readSource.bind(host),
|
|
134
|
+
assetResolver: hostAssetResolver(host),
|
|
135
|
+
document: options.document,
|
|
136
|
+
layout: inputs,
|
|
139
137
|
});
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Map a stage result to the kernel's contract: validation and include errors
|
|
141
|
+
* throw, routed warnings reach `onResolveDiagnostics`, and logo warnings
|
|
142
|
+
* reach `onWarning` (or throw under `strict`). Returns the staged roadmap.
|
|
143
|
+
*/
|
|
144
|
+
export function acceptStagedRoadmap(stage, inputs) {
|
|
145
|
+
if (stage.kind === 'invalid') {
|
|
146
|
+
throw new Error(`@nowline/export: validation errors in ${inputs.sourcePath}: ${errorMessages(stage.diagnostics).join('; ')}`);
|
|
144
147
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
// Include errors are more fundamental than wave-rule errors: any of them
|
|
149
|
+
// keeps the plain throw. Wave-rule errors alone travel on a typed error.
|
|
150
|
+
const includeError = (diag) => `@nowline/export: include error in ${inputs.sourcePath}: ${diag.message}`;
|
|
151
|
+
if (stage.kind === 'include-error') {
|
|
152
|
+
if (stage.legacy)
|
|
153
|
+
throw new Error(includeError(stage.legacy));
|
|
154
|
+
const firstError = stage.routed.filter((d) => d.severity === 'error')[0];
|
|
155
|
+
throw new IncludeResolveError(includeError(firstError), stage.resolved.diagnostics);
|
|
156
|
+
}
|
|
157
|
+
inputs.onResolveDiagnostics?.(stage.routed);
|
|
158
|
+
// A failed logo load warns (throws under strict); the stage laid out as
|
|
159
|
+
// if no logo were declared.
|
|
160
|
+
if (inputs.strict && stage.warnings.length > 0) {
|
|
161
|
+
throw new Error(`@nowline/export: render warnings (--strict): ${stage.warnings.join('; ')}`);
|
|
162
|
+
}
|
|
163
|
+
for (const msg of stage.warnings)
|
|
164
|
+
inputs.onWarning?.(msg);
|
|
165
|
+
return stage;
|
|
166
|
+
}
|
|
167
|
+
/** Stage `source` and map the result to the kernel's contract. */
|
|
168
|
+
async function stageDocument(source, inputs, host) {
|
|
169
|
+
return acceptStagedRoadmap(await stageForExport(source, inputs, host), inputs);
|
|
170
|
+
}
|
|
171
|
+
// ---- Rendering ---------------------------------------------------------------
|
|
172
|
+
/**
|
|
173
|
+
* Pin the renderer's per-role families to the resolved fonts' real family
|
|
174
|
+
* names so raster/preview SVG names exactly the bundled face. `serif` maps to
|
|
175
|
+
* the sans family: the resolver has no serif role and raster already falls
|
|
176
|
+
* back serif->sans (resvg has no serif face loaded), so this is behavior-
|
|
177
|
+
* preserving while making the SVG explicit.
|
|
178
|
+
*/
|
|
179
|
+
function pinnedFamilies(fonts) {
|
|
180
|
+
return { sans: fonts.sans.name, serif: fonts.sans.name, mono: fonts.mono.name };
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Render the staged model. The portable render (no `fontFamilies`, the
|
|
184
|
+
* FONT_STACK) reports non-strict warnings to `onWarning`; a font-pinned
|
|
185
|
+
* raster re-render does not repeat them. Every render embeds the stage's
|
|
186
|
+
* logo bytes, so the logo is read once.
|
|
187
|
+
*/
|
|
188
|
+
async function renderStaged(stage, inputs, assetResolver, fontFamilies) {
|
|
189
|
+
const warnings = [];
|
|
190
|
+
const svg = await renderSvg(stage.model, {
|
|
191
|
+
assetResolver,
|
|
192
|
+
logo: stage.logo,
|
|
193
|
+
noLinks: inputs.noLinks ?? false,
|
|
194
|
+
strict: inputs.strict ?? false,
|
|
195
|
+
warn: (msg) => warnings.push(msg),
|
|
196
|
+
fontFamilies,
|
|
150
197
|
});
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
assetResolver,
|
|
159
|
-
noLinks: inputs.noLinks ?? false,
|
|
160
|
-
strict: inputs.strict ?? false,
|
|
161
|
-
warn: (msg) => warnings.push(msg),
|
|
162
|
-
fontFamilies,
|
|
163
|
-
});
|
|
164
|
-
if (inputs.strict && warnings.length > 0) {
|
|
165
|
-
throw new Error(`@nowline/export: render warnings (--strict): ${warnings.join('; ')}`);
|
|
166
|
-
}
|
|
167
|
-
return svg;
|
|
168
|
-
};
|
|
169
|
-
// Portable render (FONT_STACK) for the svg/html file exports.
|
|
170
|
-
const svg = await renderWith();
|
|
171
|
-
return {
|
|
172
|
-
svg,
|
|
173
|
-
renderWith: (fontFamilies) => renderWith(fontFamilies),
|
|
174
|
-
exportInputs: {
|
|
175
|
-
model,
|
|
176
|
-
ast,
|
|
177
|
-
resolved,
|
|
178
|
-
sourcePath: inputs.sourcePath,
|
|
179
|
-
today: inputs.today,
|
|
180
|
-
},
|
|
181
|
-
document: doc,
|
|
182
|
-
source,
|
|
183
|
-
};
|
|
198
|
+
if (inputs.strict && warnings.length > 0) {
|
|
199
|
+
throw new Error(`@nowline/export: render warnings (--strict): ${warnings.join('; ')}`);
|
|
200
|
+
}
|
|
201
|
+
if (!fontFamilies)
|
|
202
|
+
for (const msg of warnings)
|
|
203
|
+
inputs.onWarning?.(msg);
|
|
204
|
+
return svg;
|
|
184
205
|
}
|
|
185
206
|
// ---- Text encoding -----------------------------------------------------------
|
|
186
207
|
const encoder = new TextEncoder();
|
|
@@ -191,61 +212,81 @@ function toUtf8(text) {
|
|
|
191
212
|
/**
|
|
192
213
|
* Produce the export artifact for `source` in `format`. This is the single
|
|
193
214
|
* code path to any exported artifact — no surface re-implements a stage.
|
|
215
|
+
* It is `stageRoadmap` plus the kernel's error mapping, then `exportStaged`.
|
|
194
216
|
*
|
|
195
217
|
* Text formats (svg / json / html / mermaid / msproj) are returned as UTF-8
|
|
196
218
|
* bytes; binary formats (png / pdf / xlsx) as their native bytes. Returning
|
|
197
219
|
* Uint8Array for every format keeps the determinism gate able to `sha256` one
|
|
198
220
|
* value per (fixture, format).
|
|
199
|
-
*
|
|
200
|
-
* Note: PNG rasterizes via @nowline/export-png (native resvg-js) until s4
|
|
201
|
-
* swaps it to @resvg/resvg-wasm. The `host.loadWasm()` seam is reserved for
|
|
202
|
-
* that swap and is not called here yet.
|
|
203
221
|
*/
|
|
204
222
|
export async function exportDocument(source, format, inputs, host) {
|
|
205
223
|
if (typeof source !== 'string') {
|
|
206
224
|
throw new TypeError('@nowline/export: source must be a string.');
|
|
207
225
|
}
|
|
208
|
-
|
|
209
|
-
typeof host.readAsset !== 'function' ||
|
|
210
|
-
typeof host.loadWasm !== 'function') {
|
|
211
|
-
throw new TypeError('@nowline/export: host must implement the HostEnv interface.');
|
|
212
|
-
}
|
|
213
|
-
if (!(inputs?.today instanceof Date) && inputs?.today !== undefined) {
|
|
214
|
-
throw new TypeError('@nowline/export: inputs.today must be a Date (UTC midnight) or undefined.');
|
|
215
|
-
}
|
|
216
|
-
if (typeof inputs?.sourcePath !== 'string' || inputs.sourcePath.length === 0) {
|
|
217
|
-
throw new TypeError('@nowline/export: inputs.sourcePath must be a non-empty string.');
|
|
218
|
-
}
|
|
226
|
+
checkHostAndInputs(host, inputs);
|
|
219
227
|
const stage = await stageDocument(source, inputs, host);
|
|
228
|
+
return exportStaged(stage, format, inputs, host);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Export a roadmap the caller already staged with `stageRoadmap`, so a
|
|
232
|
+
* surface that stages once (for diagnostics or layout insights) does not
|
|
233
|
+
* parse and lay out again. The stage must have been laid out with the same
|
|
234
|
+
* theme, today, width, locale and non-working display as `inputs`; a
|
|
235
|
+
* mismatch throws. The caller owns the stage's own errors and warnings
|
|
236
|
+
* (`exportDocument` maps them to the kernel's contract first); this applies
|
|
237
|
+
* `noLinks`, `strict` and `onWarning` to the render only.
|
|
238
|
+
*
|
|
239
|
+
* The SVG is rendered only for the formats that use it (svg, html, png, pdf).
|
|
240
|
+
*/
|
|
241
|
+
export async function exportStaged(stage, format, inputs, host) {
|
|
242
|
+
checkHostAndInputs(host, inputs);
|
|
243
|
+
checkStageMatches(stage, inputs);
|
|
244
|
+
const assetResolver = hostAssetResolver(host);
|
|
245
|
+
const portableSvg = () => renderStaged(stage, inputs, assetResolver);
|
|
246
|
+
const rasterSvg = (fonts) => renderStaged(stage, inputs, assetResolver, pinnedFamilies(fonts));
|
|
247
|
+
const exportInputs = {
|
|
248
|
+
model: stage.model,
|
|
249
|
+
ast: stage.ast,
|
|
250
|
+
resolved: stage.resolved,
|
|
251
|
+
schedule: stage.schedule,
|
|
252
|
+
sourcePath: inputs.sourcePath,
|
|
253
|
+
today: inputs.today,
|
|
254
|
+
};
|
|
220
255
|
if (format === 'svg') {
|
|
221
|
-
return toUtf8(
|
|
256
|
+
return toUtf8(await portableSvg());
|
|
222
257
|
}
|
|
223
258
|
if (format === 'json') {
|
|
224
|
-
|
|
259
|
+
const source = stage.document.textDocument.getText();
|
|
260
|
+
return toUtf8(serializeDocumentToJson(stage.document, source, inputs.sourcePath));
|
|
225
261
|
}
|
|
226
262
|
if (format === 'html') {
|
|
263
|
+
const svg = await portableSvg();
|
|
227
264
|
const mod = await import('@nowline/export-html');
|
|
228
|
-
const html = await mod.exportHtml(
|
|
265
|
+
const html = await mod.exportHtml(exportInputs, svg);
|
|
229
266
|
return toUtf8(html);
|
|
230
267
|
}
|
|
231
268
|
if (format === 'mermaid') {
|
|
232
269
|
const mod = await import('@nowline/export-mermaid');
|
|
233
|
-
const md = mod.exportMermaid(
|
|
270
|
+
const md = mod.exportMermaid(exportInputs);
|
|
234
271
|
return toUtf8(md);
|
|
235
272
|
}
|
|
236
273
|
if (format === 'msproj') {
|
|
237
274
|
const mod = await import('@nowline/export-msproj');
|
|
238
|
-
const xml = mod.exportMsProjXml(
|
|
275
|
+
const xml = mod.exportMsProjXml(exportInputs, {
|
|
239
276
|
startDate: inputs.msprojStart,
|
|
277
|
+
onLossy: inputs.onNotice,
|
|
240
278
|
});
|
|
241
279
|
return toUtf8(xml);
|
|
242
280
|
}
|
|
243
281
|
if (format === 'xlsx') {
|
|
244
282
|
const mod = await import('@nowline/export-xlsx');
|
|
245
|
-
const bytes = await mod.exportXlsx(
|
|
283
|
+
const bytes = await mod.exportXlsx(exportInputs, { generated: inputs.today });
|
|
246
284
|
return bytes;
|
|
247
285
|
}
|
|
248
286
|
if (format === 'png') {
|
|
287
|
+
// The portable render still runs first: its warnings reach
|
|
288
|
+
// `onWarning` (or throw under strict) before any raster work.
|
|
289
|
+
await portableSvg();
|
|
249
290
|
if (!inputs.fonts) {
|
|
250
291
|
throw new TypeError('@nowline/export: inputs.fonts is required for png format.');
|
|
251
292
|
}
|
|
@@ -254,22 +295,23 @@ export async function exportDocument(source, format, inputs, host) {
|
|
|
254
295
|
await pngMod.initPngWasm(wasmBytes);
|
|
255
296
|
// Raster with the pinned bundled family so the SVG resvg rasterizes
|
|
256
297
|
// names exactly the embedded font (matches the live preview).
|
|
257
|
-
const
|
|
258
|
-
const bytes = await pngMod.exportPng(
|
|
298
|
+
const svg = await rasterSvg(inputs.fonts);
|
|
299
|
+
const bytes = await pngMod.exportPng(exportInputs, svg, {
|
|
259
300
|
scale: inputs.pngScale ?? 2,
|
|
260
301
|
fonts: inputs.fonts,
|
|
261
302
|
});
|
|
262
303
|
return bytes;
|
|
263
304
|
}
|
|
264
305
|
if (format === 'pdf') {
|
|
306
|
+
await portableSvg();
|
|
265
307
|
if (!inputs.fonts) {
|
|
266
308
|
throw new TypeError('@nowline/export: inputs.fonts is required for pdf format.');
|
|
267
309
|
}
|
|
268
310
|
const mod = await import('@nowline/export-pdf');
|
|
269
311
|
// Same pinned-family raster SVG as PNG so PDF text names the embedded
|
|
270
312
|
// font explicitly and stays consistent with preview + PNG.
|
|
271
|
-
const
|
|
272
|
-
const bytes = await mod.exportPdf(
|
|
313
|
+
const svg = await rasterSvg(inputs.fonts);
|
|
314
|
+
const bytes = await mod.exportPdf(exportInputs, svg, {
|
|
273
315
|
pageSize: inputs.pageSize,
|
|
274
316
|
orientation: inputs.orientation,
|
|
275
317
|
marginPt: inputs.marginPt,
|