@nowline/embed 0.2.5 → 0.4.1

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/src/pipeline.ts CHANGED
@@ -1,20 +1,28 @@
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.
1
+ // Thin shim around `@nowline/browser`'s pipeline. The browser package
2
+ // owns parse / resolveIncludes / layout / render; the embed layers two
3
+ // embed-specific behaviours on top:
4
+ //
5
+ // - Throws `EmbedRenderError` on failure instead of returning a
6
+ // discriminated union. The Mermaid-compatible `nowline.render(source)`
7
+ // surface promises a string; throwing matches the documented v1
8
+ // contract and keeps the auto-scan path's per-block error handling
9
+ // simple.
10
+ // - Latches a once-per-page-load `console.warn` the first time an
11
+ // `include` directive is encountered. The browser pipeline emits a
12
+ // structured callback for each skip; the embed converts that into a
13
+ // single, deduped user-visible message.
7
14
 
8
15
  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';
16
+ __resetBrowserPipelineForTests,
17
+ type RenderOptions as BrowserRenderOptions,
18
+ parseSource as browserParseSource,
19
+ renderSource as browserRenderSource,
20
+ type DiagnosticRow,
21
+ type ParseResult,
22
+ } from '@nowline/browser';
23
+ import type { ThemeName } from '@nowline/layout';
24
+
25
+ const EMBED_SOURCE_PATH = '/embed.nowline';
18
26
 
19
27
  export interface EmbedRenderOptions {
20
28
  theme?: ThemeName;
@@ -31,96 +39,52 @@ export interface EmbedRenderOptions {
31
39
  }
32
40
 
33
41
  export interface EmbedParseResult {
34
- ast: NowlineFile;
42
+ ast: ParseResult['ast'];
35
43
  /** Lexer + parser + Langium validation diagnostics, normalized to strings. */
36
44
  errors: string[];
37
45
  }
38
46
 
39
- interface CachedServices {
40
- shared: ReturnType<typeof createNowlineServices>['shared'];
41
- Nowline: NowlineServices;
42
- }
43
-
44
- let cachedServices: CachedServices | undefined;
45
- let docCounter = 0;
46
47
  let includeWarningEmitted = false;
47
48
 
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
49
  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 };
50
+ const { ast, diagnostics } = await browserParseSource(source, {
51
+ filePath: EMBED_SOURCE_PATH,
52
+ });
53
+ return {
54
+ ast,
55
+ errors: diagnostics
56
+ .filter((d: DiagnosticRow) => d.severity === 'error')
57
+ .map((d) => d.message),
58
+ };
70
59
  }
71
60
 
72
61
  export async function renderSource(
73
62
  source: string,
74
63
  options: EmbedRenderOptions = {},
75
64
  ): 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, {
65
+ const browserOptions: BrowserRenderOptions = {
66
+ filePath: EMBED_SOURCE_PATH,
115
67
  theme: options.theme,
116
68
  today: options.today,
117
69
  locale: options.locale,
118
70
  width: options.width,
119
- });
120
-
121
- return renderSvg(model, {
122
71
  idPrefix: options.idPrefix,
123
- });
72
+ onSkippedInclude: () => {
73
+ if (!includeWarningEmitted) {
74
+ includeWarningEmitted = true;
75
+ console.warn(
76
+ 'nowline: `include` directives are skipped in the browser embed (single-file mode). ' +
77
+ 'Render multi-file roadmaps with the CLI or the GitHub Action.',
78
+ );
79
+ }
80
+ },
81
+ };
82
+
83
+ const result = await browserRenderSource(source, browserOptions);
84
+ if (result.kind === 'svg') return result.svg;
85
+
86
+ const messages = result.diagnostics.filter((d) => d.severity === 'error').map((d) => d.message);
87
+ throw new EmbedRenderError(`Failed to render Nowline source: ${messages.join('; ')}`, messages);
124
88
  }
125
89
 
126
90
  export class EmbedRenderError extends Error {
@@ -138,6 +102,5 @@ export class EmbedRenderError extends Error {
138
102
  // reset the latch between cases.
139
103
  export function __resetEmbedPipelineForTests(): void {
140
104
  includeWarningEmitted = false;
141
- cachedServices = undefined;
142
- docCounter = 0;
105
+ __resetBrowserPipelineForTests();
143
106
  }
@@ -1,4 +0,0 @@
1
- export declare const NOWLINE_EMBED_NOOP_INCLUDE_TAG = "__nowline_embed_noop_include__";
2
- export declare function noOpIncludeReadFile(absPath: string): Promise<string>;
3
- export declare function isNoOpIncludeDiagnosticMessage(message: string): boolean;
4
- //# sourceMappingURL=no-op-include-resolver.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"no-op-include-resolver.d.ts","sourceRoot":"","sources":["../src/no-op-include-resolver.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,8BAA8B,mCAAmC,CAAC;AAE/E,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI1E;AAED,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEvE"}
@@ -1,18 +0,0 @@
1
- // Browser-side `readFile` that always rejects with a stable, sniff-able
2
- // error. The render pipeline catches matching diagnostics and converts
3
- // them into a single `console.warn` (deduped), so authors notice when
4
- // they ship a file with `include` directives that the embed cannot
5
- // satisfy without a network fetch.
6
- //
7
- // A real HTTP-fetch resolver is intentionally out of scope for m4 (see
8
- // `specs/handoffs/handoff-m4-embed.md`): CORS, relative-URL semantics,
9
- // and waterfall performance each warrant their own decision and are
10
- // best handled behind an opt-in flag in a follow-up.
11
- export const NOWLINE_EMBED_NOOP_INCLUDE_TAG = '__nowline_embed_noop_include__';
12
- export async function noOpIncludeReadFile(absPath) {
13
- throw new Error(`${NOWLINE_EMBED_NOOP_INCLUDE_TAG}: include "${absPath}" was skipped — the embed runs in single-file mode.`);
14
- }
15
- export function isNoOpIncludeDiagnosticMessage(message) {
16
- return message.includes(NOWLINE_EMBED_NOOP_INCLUDE_TAG);
17
- }
18
- //# sourceMappingURL=no-op-include-resolver.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"no-op-include-resolver.js","sourceRoot":"","sources":["../src/no-op-include-resolver.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,uEAAuE;AACvE,sEAAsE;AACtE,mEAAmE;AACnE,mCAAmC;AACnC,EAAE;AACF,uEAAuE;AACvE,uEAAuE;AACvE,oEAAoE;AACpE,qDAAqD;AAErD,MAAM,CAAC,MAAM,8BAA8B,GAAG,gCAAgC,CAAC;AAE/E,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,OAAe;IACrD,MAAM,IAAI,KAAK,CACX,GAAG,8BAA8B,cAAc,OAAO,qDAAqD,CAC9G,CAAC;AACN,CAAC;AAED,MAAM,UAAU,8BAA8B,CAAC,OAAe;IAC1D,OAAO,OAAO,CAAC,QAAQ,CAAC,8BAA8B,CAAC,CAAC;AAC5D,CAAC"}
@@ -1,22 +0,0 @@
1
- // Browser-side `readFile` that always rejects with a stable, sniff-able
2
- // error. The render pipeline catches matching diagnostics and converts
3
- // them into a single `console.warn` (deduped), so authors notice when
4
- // they ship a file with `include` directives that the embed cannot
5
- // satisfy without a network fetch.
6
- //
7
- // A real HTTP-fetch resolver is intentionally out of scope for m4 (see
8
- // `specs/handoffs/handoff-m4-embed.md`): CORS, relative-URL semantics,
9
- // and waterfall performance each warrant their own decision and are
10
- // best handled behind an opt-in flag in a follow-up.
11
-
12
- export const NOWLINE_EMBED_NOOP_INCLUDE_TAG = '__nowline_embed_noop_include__';
13
-
14
- export async function noOpIncludeReadFile(absPath: string): Promise<string> {
15
- throw new Error(
16
- `${NOWLINE_EMBED_NOOP_INCLUDE_TAG}: include "${absPath}" was skipped — the embed runs in single-file mode.`,
17
- );
18
- }
19
-
20
- export function isNoOpIncludeDiagnosticMessage(message: string): boolean {
21
- return message.includes(NOWLINE_EMBED_NOOP_INCLUDE_TAG);
22
- }