@drawmotive/textgraph 0.2.2-alpha.2 → 0.2.2-alpha.3
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 +18 -5
- package/docs/api-v1.md +21 -4
- package/generated/wasm/DrawMotive.TextGraph.Bridge.wasm +0 -0
- package/generated/wasm/ExCSS.wasm +0 -0
- package/generated/wasm/Graphics.Core.wasm +0 -0
- package/generated/wasm/Grpc.Core.Api.wasm +0 -0
- package/generated/wasm/HarfBuzzSharp.wasm +0 -0
- package/generated/wasm/MagicOnion.Abstractions.wasm +0 -0
- package/generated/wasm/MagicOnion.Serialization.MessagePack.wasm +0 -0
- package/generated/wasm/MagicOnion.Shared.wasm +0 -0
- package/generated/wasm/Markdig.wasm +0 -0
- package/generated/wasm/Math.Core.wasm +0 -0
- package/generated/wasm/MessagePack.Annotations.wasm +0 -0
- package/generated/wasm/MessagePack.wasm +0 -0
- package/generated/wasm/Polly.Core.wasm +0 -0
- package/generated/wasm/Polly.wasm +0 -0
- package/generated/wasm/RBush.wasm +0 -0
- package/generated/wasm/SkiaSharp.HarfBuzz.wasm +0 -0
- package/generated/wasm/SkiaSharp.wasm +0 -0
- package/generated/wasm/System.Collections.NonGeneric.wasm +0 -0
- package/generated/wasm/System.ComponentModel.TypeConverter.wasm +0 -0
- package/generated/wasm/System.Console.wasm +0 -0
- package/generated/wasm/System.Net.Http.wasm +0 -0
- package/generated/wasm/System.Net.Primitives.wasm +0 -0
- package/generated/wasm/System.ObjectModel.wasm +0 -0
- package/generated/wasm/System.Private.CoreLib.wasm +0 -0
- package/generated/wasm/System.Private.Uri.wasm +0 -0
- package/generated/wasm/System.Private.Xml.Linq.wasm +0 -0
- package/generated/wasm/System.Private.Xml.wasm +0 -0
- package/generated/wasm/System.Xml.Linq.wasm +0 -0
- package/generated/wasm/System.Xml.XDocument.wasm +0 -0
- package/generated/wasm/dotnet.boot.js +47 -27
- package/generated/wasm/dotnet.native.js +18 -2
- package/generated/wasm/dotnet.native.wasm +0 -0
- package/generated/wasm-manifest.js +67 -42
- package/generated/wasm-manifest.json +67 -42
- package/package.json +1 -1
- package/schemas/render-svg.schema.json +65 -0
- package/schemas/wasm-manifest.schema.json +1 -1
- package/src/index.d.ts +15 -0
- package/src/index.js +24 -17
- package/src/runtime/dotnet.js +6 -4
- package/src/runtime/manifest.js +7 -1
- package/src/runtime/rendering.js +49 -7
package/src/index.d.ts
CHANGED
|
@@ -55,6 +55,7 @@ export interface TextGraphInstance {
|
|
|
55
55
|
renderPng(source: string, options?: TextGraphRenderPngOptions<'bytes'>): Promise<TextGraphRenderPngResult<Uint8Array>>;
|
|
56
56
|
renderPng(source: string, options: TextGraphRenderPngOptions<'base64'> & { encoding: 'base64' }): Promise<TextGraphRenderPngResult<string>>;
|
|
57
57
|
renderPng(source: string, options: TextGraphRenderPngOptions): Promise<TextGraphRenderPngResult<Uint8Array | string>>;
|
|
58
|
+
renderSvg(source: string, options?: TextGraphRenderSvgOptions): Promise<TextGraphRenderSvgResult>;
|
|
58
59
|
readonly<T>(operation: () => T | Promise<T>): Promise<T>;
|
|
59
60
|
mutate<T>(operation: () => T | Promise<T>): Promise<T>;
|
|
60
61
|
dispose(): Promise<void>;
|
|
@@ -146,6 +147,20 @@ export type TextGraphRenderPngResult<Png extends Uint8Array | string = Uint8Arra
|
|
|
146
147
|
readonly displayWidth?: number; readonly displayHeight?: number; readonly diagnostics: readonly TextGraphRenderDiagnostic[] }
|
|
147
148
|
| { readonly success: false; readonly diagnostics: readonly TextGraphRenderDiagnostic[] };
|
|
148
149
|
|
|
150
|
+
/** Vector export uses logical diagram units and requires textgraph-render-svg-v1. */
|
|
151
|
+
export interface TextGraphRenderSvgOptions {
|
|
152
|
+
/** Optional language hint for ambiguous Han text, e.g. ja or zh-CN. */
|
|
153
|
+
language?: string;
|
|
154
|
+
/** Non-negative padding in diagram units. Defaults to 10. */
|
|
155
|
+
padding?: number;
|
|
156
|
+
/** Cancellation cannot preempt native work already running on this thread. */
|
|
157
|
+
signal?: AbortSignal;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export type TextGraphRenderSvgResult =
|
|
161
|
+
| { readonly success: true; readonly svg: string; readonly displayWidth: number; readonly displayHeight: number; readonly diagnostics: readonly TextGraphRenderDiagnostic[] }
|
|
162
|
+
| { readonly success: false; readonly diagnostics: readonly TextGraphRenderDiagnostic[] };
|
|
163
|
+
|
|
149
164
|
export interface TextGraphRuntimeInfo extends TextGraphRuntimeManifest {
|
|
150
165
|
readonly protocolVersion: 1;
|
|
151
166
|
readonly capabilities: readonly string[];
|
package/src/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createManagedInstance, throwIfAborted, validateAbi, waitForInitialResou
|
|
|
3
3
|
import { decodeValidation, validateSignal } from './runtime/validation.js';
|
|
4
4
|
import { DrawMotiveError } from './runtime/errors.js';
|
|
5
5
|
import { normalizeFontAssets, normalizeLanguagePacks } from './runtime/language-packs.js';
|
|
6
|
-
import { decodeRender, normalizeRenderOptions } from './runtime/rendering.js';
|
|
6
|
+
import { decodeRender, decodeSvgRender, normalizeRenderOptions, normalizeSvgOptions } from './runtime/rendering.js';
|
|
7
7
|
import manifest from '../generated/wasm-manifest.js';
|
|
8
8
|
|
|
9
9
|
export { validateTextGraphAdapters } from './adapters.js';
|
|
@@ -35,25 +35,32 @@ export async function initializeTextGraph(options = {}) {
|
|
|
35
35
|
...(typeof runtime.execute === 'function' ? ['textgraph-render-v1'] : []),
|
|
36
36
|
];
|
|
37
37
|
const info = Object.freeze({ packageName: abiManifest.packageName, packageVersion: abiManifest.packageVersion, abiVersion: runtime.abiVersion, protocolVersion: 1, capabilities: Object.freeze([...capabilities]) });
|
|
38
|
+
// Every export format uses the same operation queue and resource-preparing executor.
|
|
39
|
+
const render = async (source, callOptions, normalize, decode, capability, format) => {
|
|
40
|
+
if (instance.state !== 'ready') throw new DrawMotiveError('INSTANCE_DISPOSED', 'The runtime instance is disposing or disposed');
|
|
41
|
+
const { request, encoding, signal } = normalize(source, callOptions);
|
|
42
|
+
throwIfAborted(signal);
|
|
43
|
+
if (typeof runtime.execute !== 'function' || !capabilities.includes(capability)) throw new DrawMotiveError('UNSUPPORTED_CAPABILITY', `Runtime does not support ${format} rendering`);
|
|
44
|
+
return instance.mutate(async () => {
|
|
45
|
+
throwIfAborted(signal);
|
|
46
|
+
let response;
|
|
47
|
+
try { response = await runtime.execute(JSON.stringify(request), { signal }); }
|
|
48
|
+
catch (cause) {
|
|
49
|
+
throwIfAborted(signal);
|
|
50
|
+
if (cause instanceof DrawMotiveError || cause?.name === 'AbortError') throw cause;
|
|
51
|
+
throw new DrawMotiveError('RUNTIME_FAILED', `${format} rendering could not execute`, { cause });
|
|
52
|
+
}
|
|
53
|
+
throwIfAborted(signal);
|
|
54
|
+
return decode(response, encoding);
|
|
55
|
+
});
|
|
56
|
+
};
|
|
38
57
|
return Object.assign(instance, {
|
|
39
58
|
info,
|
|
40
59
|
async renderPng(source, callOptions = {}) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return instance.mutate(async () => {
|
|
46
|
-
throwIfAborted(signal);
|
|
47
|
-
let response;
|
|
48
|
-
try { response = await runtime.execute(JSON.stringify(request), { signal }); }
|
|
49
|
-
catch (cause) {
|
|
50
|
-
throwIfAborted(signal);
|
|
51
|
-
if (cause instanceof DrawMotiveError || cause?.name === 'AbortError') throw cause;
|
|
52
|
-
throw new DrawMotiveError('RUNTIME_FAILED', 'PNG rendering could not execute', { cause });
|
|
53
|
-
}
|
|
54
|
-
throwIfAborted(signal);
|
|
55
|
-
return decodeRender(response, encoding);
|
|
56
|
-
});
|
|
60
|
+
return render(source, callOptions, normalizeRenderOptions, decodeRender, 'textgraph-render-v1', 'PNG');
|
|
61
|
+
},
|
|
62
|
+
async renderSvg(source, callOptions = {}) {
|
|
63
|
+
return render(source, callOptions, normalizeSvgOptions, decodeSvgRender, 'textgraph-render-svg-v1', 'SVG');
|
|
57
64
|
},
|
|
58
65
|
async validate(source, callOptions = {}) {
|
|
59
66
|
if (instance.state !== 'ready') throw new DrawMotiveError('INSTANCE_DISPOSED', 'The runtime instance is disposing or disposed');
|
package/src/runtime/dotnet.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRuntimeAssetPlan } from './shared.js';
|
|
2
2
|
import { DrawMotiveError } from './errors.js';
|
|
3
|
-
import { validateManifest, validateBootConfig, validationCapability, renderingCapability, isRuntimeAsset } from './manifest.js';
|
|
3
|
+
import { validateManifest, validateBootConfig, validationCapability, renderingCapability, svgRenderingCapability, supportsRendering, isRuntimeAsset } from './manifest.js';
|
|
4
4
|
import { throwIfAborted } from './lifecycle.js';
|
|
5
5
|
import { readVerifiedAsset } from './integrity.js';
|
|
6
6
|
import { createRenderingExecutor } from './render-resources.js';
|
|
@@ -69,8 +69,10 @@ export async function startTextGraphRuntime(options, manifest, readAsset) {
|
|
|
69
69
|
throw new DrawMotiveError('ABI_MISMATCH', 'Bridge and manifest disagree');
|
|
70
70
|
}
|
|
71
71
|
if (!Array.isArray(info.capabilities) || !info.capabilities.includes(validationCapability)) throw new DrawMotiveError('UNSUPPORTED_CAPABILITY', 'Bridge does not implement validation');
|
|
72
|
-
const renders = manifest.capabilities
|
|
73
|
-
|
|
72
|
+
const renders = supportsRendering(manifest.capabilities);
|
|
73
|
+
for (const capability of [renderingCapability, svgRenderingCapability]) {
|
|
74
|
+
if (manifest.capabilities.includes(capability) && !info.capabilities.includes(capability)) throw new DrawMotiveError('UNSUPPORTED_CAPABILITY', 'Bridge does not implement rendering');
|
|
75
|
+
}
|
|
74
76
|
if (manifest.capabilities.includes(fontCapability) && !info.capabilities.includes(fontCapability)) throw new DrawMotiveError('UNSUPPORTED_CAPABILITY', 'Bridge does not implement lazy fonts');
|
|
75
77
|
if (renders && typeof bridge[manifest.bridge.execute] !== 'function') throw new DrawMotiveError('ABI_MISMATCH', 'Rendering bridge export is missing');
|
|
76
78
|
return createBridgeRuntime({ manifest, options, readAsset, bridge, info });
|
|
@@ -83,7 +85,7 @@ export async function startTextGraphRuntime(options, manifest, readAsset) {
|
|
|
83
85
|
|
|
84
86
|
/** Owns managed bridge resources; the instance queue drains calls before invoking disposal. */
|
|
85
87
|
export function createBridgeRuntime({ manifest, options, readAsset, bridge, info }) {
|
|
86
|
-
const renders = manifest.capabilities
|
|
88
|
+
const renders = supportsRendering(manifest.capabilities);
|
|
87
89
|
return {
|
|
88
90
|
// A bridge may support operations intentionally omitted by the selected
|
|
89
91
|
// manifest; advertise only the intersection that this loader exposes.
|
package/src/runtime/manifest.js
CHANGED
|
@@ -2,6 +2,12 @@ import { DrawMotiveError } from './errors.js';
|
|
|
2
2
|
|
|
3
3
|
export const validationCapability = 'textgraph-validate-v1';
|
|
4
4
|
export const renderingCapability = 'textgraph-render-v1';
|
|
5
|
+
export const svgRenderingCapability = 'textgraph-render-svg-v1';
|
|
6
|
+
|
|
7
|
+
/** Both image capabilities require the same managed executor and rendering resources. */
|
|
8
|
+
export function supportsRendering(capabilities) {
|
|
9
|
+
return [renderingCapability, svgRenderingCapability].some(capability => capabilities.includes(capability));
|
|
10
|
+
}
|
|
5
11
|
|
|
6
12
|
/** Only native runtime formats belong to startup; fonts, themes and licenses are separate assets. */
|
|
7
13
|
export function isRuntimeAsset(asset) {
|
|
@@ -43,7 +49,7 @@ export function validateManifest(manifest) {
|
|
|
43
49
|
|| manifest.bridge?.type !== 'DrawMotive.TextGraph.Bridge.Program' || manifest.bridge?.info !== 'GetRuntimeInfo'
|
|
44
50
|
|| manifest.bridge?.validate !== 'Validate') invalid();
|
|
45
51
|
if (manifest.bridge.execute !== undefined && manifest.bridge.execute !== 'Execute') invalid();
|
|
46
|
-
if (manifest.capabilities
|
|
52
|
+
if (supportsRendering(manifest.capabilities) && (!manifest.rendering || manifest.bridge.execute !== 'Execute')) invalid();
|
|
47
53
|
if (manifest.rendering !== undefined) {
|
|
48
54
|
const rendering = manifest.rendering;
|
|
49
55
|
if (!rendering || !paths.has(rendering.theme) || !rendering.theme.endsWith('.css')
|
package/src/runtime/rendering.js
CHANGED
|
@@ -2,20 +2,35 @@ import { DrawMotiveError } from './errors.js';
|
|
|
2
2
|
import { decodeDiagnostics, validateSignal } from './validation.js';
|
|
3
3
|
|
|
4
4
|
const stages = ['parse', 'semantic', 'layout', 'render', 'font'];
|
|
5
|
+
const invalid = message => { throw new DrawMotiveError('INVALID_ARGUMENT', message); };
|
|
5
6
|
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
const invalid = message => { throw new DrawMotiveError('INVALID_ARGUMENT', message); };
|
|
7
|
+
/** Both export formats share source, logical padding, language and cancellation semantics. */
|
|
8
|
+
function normalizeCommonOptions(source, options) {
|
|
9
9
|
if (typeof source !== 'string' || !options || typeof options !== 'object' || Array.isArray(options)) invalid('Rendering requires a source string and options object');
|
|
10
|
-
const {
|
|
10
|
+
const { padding = 10, signal, language } = options;
|
|
11
11
|
if (language !== undefined && (typeof language !== 'string' || !/^[a-z]{2,8}(?:-[A-Za-z0-9]+)*$/.test(language))) invalid('language must be a language tag');
|
|
12
|
+
// The native geometry contract uses float32; reject values lost at that boundary.
|
|
13
|
+
if (!Number.isFinite(padding) || !Number.isFinite(Math.fround(padding)) || padding < 0) invalid('padding must be non-negative and representable as a finite float32');
|
|
14
|
+
validateSignal(signal);
|
|
15
|
+
return { padding, signal, request: { protocolVersion: 1, operation: 'render', source, ...(language === undefined ? {} : { language }) } };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Normalize export settings before queuing; native receives only the render contract. */
|
|
19
|
+
export function normalizeRenderOptions(source, options) {
|
|
20
|
+
const { padding, signal, request } = normalizeCommonOptions(source, options);
|
|
21
|
+
const { encoding = 'bytes', scale = 1, maxWidth } = options;
|
|
12
22
|
if (!['bytes', 'base64'].includes(encoding)) invalid('encoding must be bytes or base64');
|
|
13
23
|
// The native geometry contract uses float32; reject values lost at that boundary.
|
|
14
24
|
if (!Number.isFinite(scale) || !Number.isFinite(Math.fround(scale)) || Math.fround(scale) <= 0) invalid('scale must be positive and representable as a finite float32');
|
|
15
|
-
if (!Number.isFinite(padding) || !Number.isFinite(Math.fround(padding)) || padding < 0) invalid('padding must be non-negative and representable as a finite float32');
|
|
16
25
|
if (maxWidth !== undefined && (!Number.isSafeInteger(maxWidth) || maxWidth <= 0)) invalid('maxWidth must be a positive integer');
|
|
17
|
-
|
|
18
|
-
|
|
26
|
+
return { encoding, signal, request: { ...request, export: { format: 'png', scale, padding, ...(maxWidth === undefined ? {} : { maxWidth }) } } };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** SVG accepts only logical export settings; PNG density and encoding have no vector meaning. */
|
|
30
|
+
export function normalizeSvgOptions(source, options) {
|
|
31
|
+
const { padding, signal, request } = normalizeCommonOptions(source, options);
|
|
32
|
+
if (Reflect.ownKeys(options).some(key => !['padding', 'language', 'signal'].includes(key))) invalid('Unknown SVG rendering option');
|
|
33
|
+
return { signal, request: { ...request, export: { format: 'svg', padding } } };
|
|
19
34
|
}
|
|
20
35
|
|
|
21
36
|
/** Accept canonical base64 only; platform atob implementations otherwise tolerate corruption. */
|
|
@@ -59,6 +74,33 @@ export function decodeRender(json, encoding) {
|
|
|
59
74
|
}
|
|
60
75
|
}
|
|
61
76
|
|
|
77
|
+
/** Check the vector document boundary without requiring a DOM or loading external XML resources. */
|
|
78
|
+
export function decodeSvgRender(json) {
|
|
79
|
+
try {
|
|
80
|
+
const value = JSON.parse(json);
|
|
81
|
+
if (value?.protocolVersion !== 1 || typeof value.success !== 'boolean') throw new Error('Invalid envelope');
|
|
82
|
+
const diagnostics = decodeDiagnostics(value.diagnostics, stages);
|
|
83
|
+
if (value.success === diagnostics.some(item => item.severity === 'error')) throw new Error('Inconsistent success');
|
|
84
|
+
if (!value.success) {
|
|
85
|
+
if (['svg', 'displayWidth', 'displayHeight', 'png', 'width', 'height'].some(key => Object.hasOwn(value, key))) throw new Error('Failure contains image data');
|
|
86
|
+
return Object.freeze({ success: false, diagnostics });
|
|
87
|
+
}
|
|
88
|
+
if (![value.displayWidth, value.displayHeight].every(n => Number.isFinite(n) && n > 0)) throw new Error('Invalid display dimensions');
|
|
89
|
+
// This is an envelope check, not a general XML parser: the trusted native
|
|
90
|
+
// exporter owns element syntax. Require its SVG root, namespace and complete document.
|
|
91
|
+
const document = typeof value.svg === 'string' && /^(?:<\?xml\b[^?]*\?>\s*)?<svg(?=\s|\/?>)([^>]*?)(?:\/>|>[\s\S]*<\/svg\s*>)$/.exec(value.svg.trim());
|
|
92
|
+
if (!document) throw new Error('Invalid SVG document');
|
|
93
|
+
// Consume complete attribute values so namespace-like text inside another
|
|
94
|
+
// quoted attribute cannot impersonate the root namespace.
|
|
95
|
+
const attributes = [...document[1].matchAll(/(?:^|\s)([A-Za-z_:][A-Za-z0-9_.:-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)];
|
|
96
|
+
const namespaces = attributes.filter(attribute => attribute[1] === 'xmlns');
|
|
97
|
+
if (namespaces.length !== 1 || (namespaces[0][2] ?? namespaces[0][3]) !== 'http://www.w3.org/2000/svg') throw new Error('Invalid SVG namespace');
|
|
98
|
+
return Object.freeze({ success: true, svg: value.svg, displayWidth: value.displayWidth, displayHeight: value.displayHeight, diagnostics });
|
|
99
|
+
} catch (cause) {
|
|
100
|
+
throw new DrawMotiveError('INVALID_RESPONSE', 'The SVG rendering response violates protocol 1', { cause });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
62
104
|
/** Resource setup and disposal failures are operational, separate from diagram diagnostics. */
|
|
63
105
|
export function decodeConfiguration(json, operation = 'configuration') {
|
|
64
106
|
let value;
|