@nowline/embed 0.2.2
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/LICENSE +190 -0
- package/README.md +118 -0
- package/dist/auto-scan.d.ts +23 -0
- package/dist/auto-scan.d.ts.map +1 -0
- package/dist/auto-scan.js +71 -0
- package/dist/auto-scan.js.map +1 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +119 -0
- package/dist/index.js.map +1 -0
- package/dist/meta.json +10226 -0
- package/dist/no-op-include-resolver.d.ts +4 -0
- package/dist/no-op-include-resolver.d.ts.map +1 -0
- package/dist/no-op-include-resolver.js +18 -0
- package/dist/no-op-include-resolver.js.map +1 -0
- package/dist/nowline.esm.js +38295 -0
- package/dist/nowline.esm.js.map +7 -0
- package/dist/nowline.min.js +2663 -0
- package/dist/nowline.min.js.map +7 -0
- package/dist/pipeline.d.ts +28 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +94 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/theme.d.ts +5 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +32 -0
- package/dist/theme.js.map +1 -0
- package/package.json +59 -0
- package/src/auto-scan.ts +107 -0
- package/src/index.ts +176 -0
- package/src/no-op-include-resolver.ts +22 -0
- package/src/pipeline.ts +143 -0
- package/src/theme.ts +33 -0
package/src/pipeline.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Render pipeline shared by `nowline.render(source)` and the auto-scan
|
|
2
|
+
// path. Mirrors the shape of `packages/vscode-extension/src/preview/
|
|
3
|
+
// render-pipeline.ts` but stripped of every Node-only dependency: no
|
|
4
|
+
// `fs`, no `path`, no asset resolver. Includes are resolved against a
|
|
5
|
+
// no-op `readFile` so a file containing `include "./other.nowline"`
|
|
6
|
+
// renders the parts that survive without a network fetch.
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
createNowlineServices,
|
|
10
|
+
type NowlineFile,
|
|
11
|
+
type NowlineServices,
|
|
12
|
+
resolveIncludes,
|
|
13
|
+
} from '@nowline/core';
|
|
14
|
+
import { layoutRoadmap, type ThemeName } from '@nowline/layout';
|
|
15
|
+
import { renderSvg } from '@nowline/renderer';
|
|
16
|
+
import { URI } from 'langium';
|
|
17
|
+
import { isNoOpIncludeDiagnosticMessage, noOpIncludeReadFile } from './no-op-include-resolver.js';
|
|
18
|
+
|
|
19
|
+
export interface EmbedRenderOptions {
|
|
20
|
+
theme?: ThemeName;
|
|
21
|
+
today?: Date;
|
|
22
|
+
locale?: string;
|
|
23
|
+
width?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Override the deterministic id prefix used for in-SVG `<style>`
|
|
26
|
+
* scoping. Each block on a page should use a unique prefix so two
|
|
27
|
+
* roadmaps cannot bleed styles into each other; the auto-scan path
|
|
28
|
+
* generates a per-block prefix and threads it here.
|
|
29
|
+
*/
|
|
30
|
+
idPrefix?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface EmbedParseResult {
|
|
34
|
+
ast: NowlineFile;
|
|
35
|
+
/** Lexer + parser + Langium validation diagnostics, normalized to strings. */
|
|
36
|
+
errors: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface CachedServices {
|
|
40
|
+
shared: ReturnType<typeof createNowlineServices>['shared'];
|
|
41
|
+
Nowline: NowlineServices;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let cachedServices: CachedServices | undefined;
|
|
45
|
+
let docCounter = 0;
|
|
46
|
+
let includeWarningEmitted = false;
|
|
47
|
+
|
|
48
|
+
function getServices(): CachedServices {
|
|
49
|
+
if (!cachedServices) cachedServices = createNowlineServices();
|
|
50
|
+
return cachedServices;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function freshUri(): URI {
|
|
54
|
+
return URI.parse(`memory:///nowline-embed-${++docCounter}.nowline`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function parseSource(source: string): Promise<EmbedParseResult> {
|
|
58
|
+
const services = getServices();
|
|
59
|
+
const docFactory = services.shared.workspace.LangiumDocumentFactory;
|
|
60
|
+
const doc = docFactory.fromString<NowlineFile>(source, freshUri());
|
|
61
|
+
await services.shared.workspace.DocumentBuilder.build([doc], { validation: true });
|
|
62
|
+
|
|
63
|
+
const errors: string[] = [];
|
|
64
|
+
for (const e of doc.parseResult.lexerErrors) errors.push(e.message);
|
|
65
|
+
for (const e of doc.parseResult.parserErrors) errors.push(e.message);
|
|
66
|
+
for (const d of doc.diagnostics ?? []) {
|
|
67
|
+
if (d.severity === 1) errors.push(d.message);
|
|
68
|
+
}
|
|
69
|
+
return { ast: doc.parseResult.value, errors };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function renderSource(
|
|
73
|
+
source: string,
|
|
74
|
+
options: EmbedRenderOptions = {},
|
|
75
|
+
): Promise<string> {
|
|
76
|
+
const parsed = await parseSource(source);
|
|
77
|
+
if (parsed.errors.length > 0) {
|
|
78
|
+
throw new EmbedRenderError(
|
|
79
|
+
`Failed to parse Nowline source: ${parsed.errors.join('; ')}`,
|
|
80
|
+
parsed.errors,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const services = getServices();
|
|
85
|
+
const resolved = await resolveIncludes(parsed.ast, '/embed.nowline', {
|
|
86
|
+
services: services.Nowline,
|
|
87
|
+
readFile: noOpIncludeReadFile,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
let sawIncludeWarning = false;
|
|
91
|
+
const blockingErrors: string[] = [];
|
|
92
|
+
for (const diag of resolved.diagnostics) {
|
|
93
|
+
if (diag.severity !== 'error') continue;
|
|
94
|
+
if (isNoOpIncludeDiagnosticMessage(diag.message)) {
|
|
95
|
+
sawIncludeWarning = true;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
blockingErrors.push(diag.message);
|
|
99
|
+
}
|
|
100
|
+
if (blockingErrors.length > 0) {
|
|
101
|
+
throw new EmbedRenderError(
|
|
102
|
+
`Failed to resolve Nowline source: ${blockingErrors.join('; ')}`,
|
|
103
|
+
blockingErrors,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (sawIncludeWarning && !includeWarningEmitted) {
|
|
107
|
+
includeWarningEmitted = true;
|
|
108
|
+
console.warn(
|
|
109
|
+
'nowline: `include` directives are skipped in the browser embed (single-file mode). ' +
|
|
110
|
+
'Render multi-file roadmaps with the CLI or the GitHub Action.',
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const model = layoutRoadmap(parsed.ast, resolved, {
|
|
115
|
+
theme: options.theme,
|
|
116
|
+
today: options.today,
|
|
117
|
+
locale: options.locale,
|
|
118
|
+
width: options.width,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return renderSvg(model, {
|
|
122
|
+
idPrefix: options.idPrefix,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export class EmbedRenderError extends Error {
|
|
127
|
+
constructor(
|
|
128
|
+
message: string,
|
|
129
|
+
public readonly details: string[],
|
|
130
|
+
) {
|
|
131
|
+
super(message);
|
|
132
|
+
this.name = 'EmbedRenderError';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Test-only escape hatch. The console.warn is intentionally emitted at
|
|
137
|
+
// most once per page load; tests that exercise the warning path need to
|
|
138
|
+
// reset the latch between cases.
|
|
139
|
+
export function __resetEmbedPipelineForTests(): void {
|
|
140
|
+
includeWarningEmitted = false;
|
|
141
|
+
cachedServices = undefined;
|
|
142
|
+
docCounter = 0;
|
|
143
|
+
}
|
package/src/theme.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Theme resolution for the embed.
|
|
2
|
+
//
|
|
3
|
+
// Precedence (highest to lowest):
|
|
4
|
+
// 1. `initialize({ theme })` flag (light / dark / auto).
|
|
5
|
+
// 2. The file's own `nowline v1 theme:` directive — handled inside
|
|
6
|
+
// layout, so we just don't override it when the embed config says
|
|
7
|
+
// `'auto'` and we have no system preference reading.
|
|
8
|
+
// 3. The browser's `prefers-color-scheme` media query.
|
|
9
|
+
//
|
|
10
|
+
// `prefers-color-scheme` is read **once on init**, not reactively, so
|
|
11
|
+
// flipping the OS theme mid-session does not cause every embedded
|
|
12
|
+
// roadmap on the page to repaint. This matches Mermaid's posture and
|
|
13
|
+
// keeps the embed deterministic for screenshot tools.
|
|
14
|
+
|
|
15
|
+
import type { ThemeName } from '@nowline/layout';
|
|
16
|
+
|
|
17
|
+
export type EmbedTheme = ThemeName | 'auto';
|
|
18
|
+
|
|
19
|
+
export function resolveSystemTheme(): ThemeName {
|
|
20
|
+
if (typeof globalThis === 'undefined') return 'light';
|
|
21
|
+
const win = (globalThis as { matchMedia?: (q: string) => { matches: boolean } }).matchMedia;
|
|
22
|
+
if (typeof win !== 'function') return 'light';
|
|
23
|
+
try {
|
|
24
|
+
return win('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
25
|
+
} catch {
|
|
26
|
+
return 'light';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function effectiveTheme(theme: EmbedTheme | undefined, systemTheme: ThemeName): ThemeName {
|
|
31
|
+
if (theme === 'light' || theme === 'dark') return theme;
|
|
32
|
+
return systemTheme;
|
|
33
|
+
}
|