@nowline/embed 0.0.0-dev.20260601071750.g04bdff9

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.
@@ -0,0 +1,28 @@
1
+ import { type ParseResult } from '@nowline/browser';
2
+ import type { ThemeName } from '@nowline/layout';
3
+ export interface EmbedRenderOptions {
4
+ theme?: ThemeName;
5
+ today?: Date;
6
+ locale?: string;
7
+ width?: number;
8
+ /**
9
+ * Override the deterministic id prefix used for in-SVG `<style>`
10
+ * scoping. Each block on a page should use a unique prefix so two
11
+ * roadmaps cannot bleed styles into each other; the auto-scan path
12
+ * generates a per-block prefix and threads it here.
13
+ */
14
+ idPrefix?: string;
15
+ }
16
+ export interface EmbedParseResult {
17
+ ast: ParseResult['ast'];
18
+ /** Lexer + parser + Langium validation diagnostics, normalized to strings. */
19
+ errors: string[];
20
+ }
21
+ export declare function parseSource(source: string): Promise<EmbedParseResult>;
22
+ export declare function renderSource(source: string, options?: EmbedRenderOptions): Promise<string>;
23
+ export declare class EmbedRenderError extends Error {
24
+ readonly details: string[];
25
+ constructor(message: string, details: string[]);
26
+ }
27
+ export declare function __resetEmbedPipelineForTests(): void;
28
+ //# sourceMappingURL=pipeline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAcA,OAAO,EAMH,KAAK,WAAW,EACnB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAIjD,MAAM,WAAW,kBAAkB;IAC/B,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,KAAK,CAAC,EAAE,IAAI,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC;IACxB,8EAA8E;IAC9E,MAAM,EAAE,MAAM,EAAE,CAAC;CACpB;AAID,wBAAsB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAU3E;AAED,wBAAsB,YAAY,CAC9B,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,kBAAuB,GACjC,OAAO,CAAC,MAAM,CAAC,CAwBjB;AAED,qBAAa,gBAAiB,SAAQ,KAAK;aAGnB,OAAO,EAAE,MAAM,EAAE;gBADjC,OAAO,EAAE,MAAM,EACC,OAAO,EAAE,MAAM,EAAE;CAKxC;AAKD,wBAAgB,4BAA4B,IAAI,IAAI,CAGnD"}
@@ -0,0 +1,65 @@
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.
14
+ import { __resetBrowserPipelineForTests, parseSource as browserParseSource, renderSource as browserRenderSource, } from '@nowline/browser';
15
+ const EMBED_SOURCE_PATH = '/embed.nowline';
16
+ let includeWarningEmitted = false;
17
+ export async function parseSource(source) {
18
+ const { ast, diagnostics } = await browserParseSource(source, {
19
+ filePath: EMBED_SOURCE_PATH,
20
+ });
21
+ return {
22
+ ast,
23
+ errors: diagnostics
24
+ .filter((d) => d.severity === 'error')
25
+ .map((d) => d.message),
26
+ };
27
+ }
28
+ export async function renderSource(source, options = {}) {
29
+ const browserOptions = {
30
+ filePath: EMBED_SOURCE_PATH,
31
+ theme: options.theme,
32
+ today: options.today,
33
+ locale: options.locale,
34
+ width: options.width,
35
+ idPrefix: options.idPrefix,
36
+ onSkippedInclude: () => {
37
+ if (!includeWarningEmitted) {
38
+ includeWarningEmitted = true;
39
+ console.warn('nowline: `include` directives are skipped in the browser embed (single-file mode). ' +
40
+ 'Render multi-file roadmaps with the CLI or the GitHub Action.');
41
+ }
42
+ },
43
+ };
44
+ const result = await browserRenderSource(source, browserOptions);
45
+ if (result.kind === 'svg')
46
+ return result.svg;
47
+ const messages = result.diagnostics.filter((d) => d.severity === 'error').map((d) => d.message);
48
+ throw new EmbedRenderError(`Failed to render Nowline source: ${messages.join('; ')}`, messages);
49
+ }
50
+ export class EmbedRenderError extends Error {
51
+ details;
52
+ constructor(message, details) {
53
+ super(message);
54
+ this.details = details;
55
+ this.name = 'EmbedRenderError';
56
+ }
57
+ }
58
+ // Test-only escape hatch. The console.warn is intentionally emitted at
59
+ // most once per page load; tests that exercise the warning path need to
60
+ // reset the latch between cases.
61
+ export function __resetEmbedPipelineForTests() {
62
+ includeWarningEmitted = false;
63
+ __resetBrowserPipelineForTests();
64
+ }
65
+ //# sourceMappingURL=pipeline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,uEAAuE;AACvE,oCAAoC;AACpC,EAAE;AACF,iEAAiE;AACjE,0EAA0E;AAC1E,mEAAmE;AACnE,sEAAsE;AACtE,aAAa;AACb,mEAAmE;AACnE,sEAAsE;AACtE,uEAAuE;AACvE,2CAA2C;AAE3C,OAAO,EACH,8BAA8B,EAE9B,WAAW,IAAI,kBAAkB,EACjC,YAAY,IAAI,mBAAmB,GAGtC,MAAM,kBAAkB,CAAC;AAG1B,MAAM,iBAAiB,GAAG,gBAAgB,CAAC;AAsB3C,IAAI,qBAAqB,GAAG,KAAK,CAAC;AAElC,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAc;IAC5C,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,GAAG,MAAM,kBAAkB,CAAC,MAAM,EAAE;QAC1D,QAAQ,EAAE,iBAAiB;KAC9B,CAAC,CAAC;IACH,OAAO;QACH,GAAG;QACH,MAAM,EAAE,WAAW;aACd,MAAM,CAAC,CAAC,CAAgB,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC;aACpD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;KAC7B,CAAC;AACN,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAC9B,MAAc,EACd,UAA8B,EAAE;IAEhC,MAAM,cAAc,GAAyB;QACzC,QAAQ,EAAE,iBAAiB;QAC3B,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,gBAAgB,EAAE,GAAG,EAAE;YACnB,IAAI,CAAC,qBAAqB,EAAE,CAAC;gBACzB,qBAAqB,GAAG,IAAI,CAAC;gBAC7B,OAAO,CAAC,IAAI,CACR,qFAAqF;oBACjF,+DAA+D,CACtE,CAAC;YACN,CAAC;QACL,CAAC;KACJ,CAAC;IAEF,MAAM,MAAM,GAAG,MAAM,mBAAmB,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACjE,IAAI,MAAM,CAAC,IAAI,KAAK,KAAK;QAAE,OAAO,MAAM,CAAC,GAAG,CAAC;IAE7C,MAAM,QAAQ,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;IAChG,MAAM,IAAI,gBAAgB,CAAC,oCAAoC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;AACpG,CAAC;AAED,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAGnB;IAFpB,YACI,OAAe,EACC,OAAiB;QAEjC,KAAK,CAAC,OAAO,CAAC,CAAC;QAFC,YAAO,GAAP,OAAO,CAAU;QAGjC,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACnC,CAAC;CACJ;AAED,uEAAuE;AACvE,wEAAwE;AACxE,iCAAiC;AACjC,MAAM,UAAU,4BAA4B;IACxC,qBAAqB,GAAG,KAAK,CAAC;IAC9B,8BAA8B,EAAE,CAAC;AACrC,CAAC"}
@@ -0,0 +1,45 @@
1
+ export declare const DEFAULT_SHARE_BASE = "https://free.nowline.io/open";
2
+ /**
3
+ * The `share` initialize option selects where share links point.
4
+ *
5
+ * - `true` — use DEFAULT_SHARE_BASE (the default).
6
+ * - `string` — a base URL with optional path; built via the URL API so
7
+ * `https://foo.com/open` → `https://foo.com/open#text=…`.
8
+ * - `false` / `'none'` — disable the share anchor entirely.
9
+ * - `{ textUrl, remoteUrl }` — escape hatch for non-hash URL shapes;
10
+ * `{text}` substituted with the base64url payload, `{url}` with the
11
+ * percent-encoded source URL.
12
+ */
13
+ export type ShareOption = boolean | 'none' | string | {
14
+ textUrl: string;
15
+ remoteUrl: string;
16
+ };
17
+ /**
18
+ * Encode source text → `#text=<base64url(zlib(utf8(source)))>`.
19
+ *
20
+ * The return value includes the `#text=` key so callers can use it
21
+ * directly as a URL fragment.
22
+ *
23
+ * Sync, single code path, no feature-detect.
24
+ */
25
+ export declare function encodeText(source: string): string;
26
+ export interface BuildShareLinkOptions {
27
+ /** The roadmap source text (used to build the #text= fragment). */
28
+ source: string;
29
+ /**
30
+ * Resolved source URL for the block (per-block → global → undefined).
31
+ * Only `https:` URLs are emitted as `#url=`; anything else falls
32
+ * back to the inline `#text=` encoding.
33
+ */
34
+ sourceUrl?: string | undefined;
35
+ /** The `share` option from InitializeOptions. */
36
+ share: ShareOption;
37
+ }
38
+ /**
39
+ * Build the full "Share on Nowline" URL for a rendered block.
40
+ *
41
+ * Returns `null` when `share` is `false` or `'none'`, signalling that
42
+ * no anchor should be rendered.
43
+ */
44
+ export declare function buildShareLink({ source, sourceUrl, share }: BuildShareLinkOptions): string | null;
45
+ //# sourceMappingURL=share.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"share.d.ts","sourceRoot":"","sources":["../src/share.ts"],"names":[],"mappings":"AAeA,eAAO,MAAM,kBAAkB,iCAAiC,CAAC;AAEjE;;;;;;;;;;GAUG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7F;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEjD;AAgBD,MAAM,WAAW,qBAAqB;IAClC,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,iDAAiD;IACjD,KAAK,EAAE,WAAW,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,qBAAqB,GAAG,MAAM,GAAG,IAAI,CAwBjG"}
package/dist/share.js ADDED
@@ -0,0 +1,75 @@
1
+ // Share-link generation for the "Share on Nowline" anchor that the
2
+ // auto-scan loop appends after each rendered SVG.
3
+ //
4
+ // Encoding grammar (normative — defined in specs/embed.md):
5
+ // #text=base64url(zlib(utf8(source)))
6
+ // #url=<https-url>
7
+ //
8
+ // zlib = RFC 1950 via fflate zlibSync (byte-compatible with native
9
+ // CompressionStream('deflate')); base64url strips padding and maps
10
+ // +→- /→_.
11
+ //
12
+ // Sync, works on every browser, no feature-detect.
13
+ import { zlibSync } from 'fflate';
14
+ export const DEFAULT_SHARE_BASE = 'https://free.nowline.io/open';
15
+ /**
16
+ * Encode source text → `#text=<base64url(zlib(utf8(source)))>`.
17
+ *
18
+ * The return value includes the `#text=` key so callers can use it
19
+ * directly as a URL fragment.
20
+ *
21
+ * Sync, single code path, no feature-detect.
22
+ */
23
+ export function encodeText(source) {
24
+ return `#text=${_encodePayload(source)}`;
25
+ }
26
+ /** base64url(zlib(utf8(source))) without the `#text=` prefix. */
27
+ function _encodePayload(source) {
28
+ const bytes = new TextEncoder().encode(source);
29
+ const compressed = zlibSync(bytes);
30
+ // Convert Uint8Array to binary string for btoa. Chunked to avoid
31
+ // call-stack limits on large payloads.
32
+ const chunk = 0x8000; // 32 KB — safe below JS engine stack limits
33
+ let bin = '';
34
+ for (let i = 0; i < compressed.length; i += chunk) {
35
+ bin += String.fromCharCode(...compressed.subarray(i, i + chunk));
36
+ }
37
+ return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
38
+ }
39
+ /**
40
+ * Build the full "Share on Nowline" URL for a rendered block.
41
+ *
42
+ * Returns `null` when `share` is `false` or `'none'`, signalling that
43
+ * no anchor should be rendered.
44
+ */
45
+ export function buildShareLink({ source, sourceUrl, share }) {
46
+ if (share === false || share === 'none') {
47
+ return null;
48
+ }
49
+ if (typeof share === 'object') {
50
+ // Template mode: { textUrl, remoteUrl }
51
+ if (sourceUrl !== undefined && _isHttps(sourceUrl)) {
52
+ return share.remoteUrl.replace('{url}', encodeURIComponent(sourceUrl));
53
+ }
54
+ return share.textUrl.replace('{text}', _encodePayload(source));
55
+ }
56
+ // share === true → DEFAULT_SHARE_BASE; share is a string → custom base URL
57
+ const base = share === true ? DEFAULT_SHARE_BASE : share;
58
+ const url = new URL(base);
59
+ if (sourceUrl !== undefined && _isHttps(sourceUrl)) {
60
+ url.hash = `url=${encodeURIComponent(sourceUrl)}`;
61
+ }
62
+ else {
63
+ url.hash = `text=${_encodePayload(source)}`;
64
+ }
65
+ return url.toString();
66
+ }
67
+ function _isHttps(url) {
68
+ try {
69
+ return new URL(url).protocol === 'https:';
70
+ }
71
+ catch {
72
+ return false;
73
+ }
74
+ }
75
+ //# sourceMappingURL=share.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"share.js","sourceRoot":"","sources":["../src/share.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,kDAAkD;AAClD,EAAE;AACF,4DAA4D;AAC5D,wCAAwC;AACxC,qBAAqB;AACrB,EAAE;AACF,mEAAmE;AACnE,mEAAmE;AACnE,WAAW;AACX,EAAE;AACF,mDAAmD;AAEnD,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAElC,MAAM,CAAC,MAAM,kBAAkB,GAAG,8BAA8B,CAAC;AAejE;;;;;;;GAOG;AACH,MAAM,UAAU,UAAU,CAAC,MAAc;IACrC,OAAO,SAAS,cAAc,CAAC,MAAM,CAAC,EAAE,CAAC;AAC7C,CAAC;AAED,iEAAiE;AACjE,SAAS,cAAc,CAAC,MAAc;IAClC,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC/C,MAAM,UAAU,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IACnC,iEAAiE;IACjE,uCAAuC;IACvC,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,4CAA4C;IAClE,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC;QAChD,GAAG,IAAI,MAAM,CAAC,YAAY,CAAC,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC;IACrE,CAAC;IACD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAChF,CAAC;AAeD;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAyB;IAC9E,IAAI,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;QACtC,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC5B,wCAAwC;QACxC,IAAI,SAAS,KAAK,SAAS,IAAI,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;YACjD,OAAO,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO,EAAE,kBAAkB,CAAC,SAAS,CAAC,CAAC,CAAC;QAC3E,CAAC;QACD,OAAO,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,QAAQ,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC;IACnE,CAAC;IAED,2EAA2E;IAC3E,MAAM,IAAI,GAAG,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,KAAK,CAAC;IACzD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC;IAE1B,IAAI,SAAS,KAAK,SAAS,IAAI,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QACjD,GAAG,CAAC,IAAI,GAAG,OAAO,kBAAkB,CAAC,SAAS,CAAC,EAAE,CAAC;IACtD,CAAC;SAAM,CAAC;QACJ,GAAG,CAAC,IAAI,GAAG,QAAQ,cAAc,CAAC,MAAM,CAAC,EAAE,CAAC;IAChD,CAAC;IAED,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;AAC1B,CAAC;AAED,SAAS,QAAQ,CAAC,GAAW;IACzB,IAAI,CAAC;QACD,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,KAAK,CAAC;IACjB,CAAC;AACL,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { type ThemeName } from '@nowline/layout';
2
+ export type EmbedTheme = ThemeName | 'greyscale' | 'auto';
3
+ export declare function resolveSystemTheme(): 'light' | 'dark';
4
+ export declare function effectiveTheme(theme: EmbedTheme | undefined, systemTheme: ThemeName): ThemeName;
5
+ //# sourceMappingURL=theme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAeA,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAGrE,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,WAAW,GAAG,MAAM,CAAC;AAE1D,wBAAgB,kBAAkB,IAAI,OAAO,GAAG,MAAM,CASrD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,SAAS,EAAE,WAAW,EAAE,SAAS,GAAG,SAAS,CAG/F"}
package/dist/theme.js ADDED
@@ -0,0 +1,34 @@
1
+ // Theme resolution for the embed.
2
+ //
3
+ // Precedence (highest to lowest):
4
+ // 1. `initialize({ theme })` flag (light / dark / grayscale / auto;
5
+ // `greyscale` is accepted and canonicalized to `grayscale`).
6
+ // 2. The file's own `nowline v1 theme:` directive — handled inside
7
+ // layout, so we just don't override it when the embed config says
8
+ // `'auto'` and we have no system preference reading.
9
+ // 3. The browser's `prefers-color-scheme` media query.
10
+ //
11
+ // `prefers-color-scheme` is read **once on init**, not reactively, so
12
+ // flipping the OS theme mid-session does not cause every embedded
13
+ // roadmap on the page to repaint. This matches Mermaid's posture and
14
+ // keeps the embed deterministic for screenshot tools.
15
+ import { normalizeThemeName } from '@nowline/layout';
16
+ export function resolveSystemTheme() {
17
+ if (typeof globalThis === 'undefined')
18
+ return 'light';
19
+ const win = globalThis.matchMedia;
20
+ if (typeof win !== 'function')
21
+ return 'light';
22
+ try {
23
+ return win('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
24
+ }
25
+ catch {
26
+ return 'light';
27
+ }
28
+ }
29
+ export function effectiveTheme(theme, systemTheme) {
30
+ if (theme && theme !== 'auto')
31
+ return normalizeThemeName(theme) ?? systemTheme;
32
+ return systemTheme;
33
+ }
34
+ //# sourceMappingURL=theme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.js","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,EAAE;AACF,kCAAkC;AAClC,sEAAsE;AACtE,kEAAkE;AAClE,qEAAqE;AACrE,uEAAuE;AACvE,0DAA0D;AAC1D,yDAAyD;AACzD,EAAE;AACF,sEAAsE;AACtE,kEAAkE;AAClE,qEAAqE;AACrE,sDAAsD;AAEtD,OAAO,EAAE,kBAAkB,EAAkB,MAAM,iBAAiB,CAAC;AAKrE,MAAM,UAAU,kBAAkB;IAC9B,IAAI,OAAO,UAAU,KAAK,WAAW;QAAE,OAAO,OAAO,CAAC;IACtD,MAAM,GAAG,GAAI,UAAmE,CAAC,UAAU,CAAC;IAC5F,IAAI,OAAO,GAAG,KAAK,UAAU;QAAE,OAAO,OAAO,CAAC;IAC9C,IAAI,CAAC;QACD,OAAO,GAAG,CAAC,8BAA8B,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;IAC1E,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,OAAO,CAAC;IACnB,CAAC;AACL,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAA6B,EAAE,WAAsB;IAChF,IAAI,KAAK,IAAI,KAAK,KAAK,MAAM;QAAE,OAAO,kBAAkB,CAAC,KAAK,CAAC,IAAI,WAAW,CAAC;IAC/E,OAAO,WAAW,CAAC;AACvB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@nowline/embed",
3
+ "version": "0.0.0-dev.20260601071750.g04bdff9",
4
+ "description": "Browser embed bundle for Nowline. Drop a <script> tag and ```nowline``` blocks render in place.",
5
+ "license": "Apache-2.0",
6
+ "engines": {
7
+ "node": ">=22",
8
+ "pnpm": ">=11"
9
+ },
10
+ "type": "module",
11
+ "sideEffects": false,
12
+ "main": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist/",
22
+ "src/"
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/lolay/nowline.git",
27
+ "directory": "packages/embed"
28
+ },
29
+ "homepage": "https://github.com/lolay/nowline",
30
+ "bugs": {
31
+ "url": "https://github.com/lolay/nowline/issues"
32
+ },
33
+ "keywords": [
34
+ "nowline",
35
+ "roadmap",
36
+ "gantt",
37
+ "embed",
38
+ "browser",
39
+ "cdn"
40
+ ],
41
+ "dependencies": {
42
+ "fflate": "^0.8.3",
43
+ "langium": "~4.2.4",
44
+ "@nowline/browser": "0.0.0-dev.20260601071750.g04bdff9",
45
+ "@nowline/core": "0.0.0-dev.20260601071750.g04bdff9",
46
+ "@nowline/layout": "0.0.0-dev.20260601071750.g04bdff9",
47
+ "@nowline/renderer": "0.0.0-dev.20260601071750.g04bdff9"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^25.9.1",
51
+ "esbuild": "^0.28.0",
52
+ "happy-dom": "^20.9.0",
53
+ "typescript": "^6.0.3",
54
+ "vitest": "^4.1.7"
55
+ },
56
+ "scripts": {
57
+ "build": "tsc -b tsconfig.json && node scripts/bundle.mjs",
58
+ "watch": "tsc -b tsconfig.json --watch",
59
+ "typecheck": "tsc --noEmit -p tsconfig.json",
60
+ "bundle": "node scripts/bundle.mjs",
61
+ "check-size": "node scripts/check-size.mjs",
62
+ "test": "vitest run",
63
+ "test:watch": "vitest"
64
+ }
65
+ }
@@ -0,0 +1,149 @@
1
+ // DOM scanner that finds `<pre><code class="language-nowline">…</code></pre>`
2
+ // blocks (or whatever selector the caller registered) and replaces each
3
+ // with its rendered SVG. Each block gets a unique `idPrefix` so two
4
+ // roadmaps on the same page never share `<style>` ids.
5
+
6
+ import type { ThemeName } from '@nowline/layout';
7
+ import { type EmbedRenderOptions, renderSource } from './pipeline.js';
8
+ import { buildShareLink, type ShareOption } from './share.js';
9
+
10
+ export interface AutoScanInputs {
11
+ selector: string;
12
+ theme?: ThemeName;
13
+ locale?: string;
14
+ width?: number;
15
+ today?: Date;
16
+ /**
17
+ * Controls the "Share on Nowline" anchor appended after each rendered
18
+ * SVG. Defaults to `true` when omitted (mirrors the config default).
19
+ */
20
+ share?: ShareOption;
21
+ /**
22
+ * Global source URL for all blocks. Per-block `data-nowline-source-url`
23
+ * overrides this for individual blocks.
24
+ */
25
+ sourceUrl?: string;
26
+ /**
27
+ * Document to scan. Defaults to `globalThis.document`. Tests inject
28
+ * a happy-dom document; the IIFE running on a real page picks up
29
+ * the live document.
30
+ */
31
+ document?: Document;
32
+ }
33
+
34
+ export interface AutoScanResult {
35
+ /** Number of code blocks that were successfully replaced with SVG. */
36
+ rendered: number;
37
+ /** Number of blocks that failed to render (logged to console.error). */
38
+ failed: number;
39
+ }
40
+
41
+ let runCounter = 0;
42
+
43
+ export async function runAutoScan(inputs: AutoScanInputs): Promise<AutoScanResult> {
44
+ const doc = inputs.document ?? (globalThis as { document?: Document }).document;
45
+ if (!doc) {
46
+ return { rendered: 0, failed: 0 };
47
+ }
48
+
49
+ const blocks = doc.querySelectorAll<HTMLElement>(inputs.selector);
50
+ let rendered = 0;
51
+ let failed = 0;
52
+ const baseRunId = ++runCounter;
53
+
54
+ // share defaults to true when omitted (mirrors config.share default)
55
+ const share: ShareOption = inputs.share ?? true;
56
+
57
+ const tasks: Array<Promise<void>> = [];
58
+ let blockIndex = 0;
59
+ for (const code of Array.from(blocks)) {
60
+ const target = pickReplacementTarget(code);
61
+ if (!target) {
62
+ continue;
63
+ }
64
+ const source = readBlockSource(code);
65
+ const idPrefix = `nl-r${baseRunId}-${blockIndex++}`;
66
+ const opts: EmbedRenderOptions = {
67
+ theme: inputs.theme,
68
+ locale: inputs.locale,
69
+ width: inputs.width,
70
+ today: inputs.today,
71
+ idPrefix,
72
+ };
73
+
74
+ // Capture DOM position before the async render. outerHTML replacement
75
+ // detaches `target` from the tree, so parent and nextSibling must be
76
+ // read now. The share anchor is inserted at the saved nextSibling
77
+ // position, which lands it as the immediate next sibling of the SVG.
78
+ const parent = target.parentElement;
79
+ const nextSibling = target.nextSibling;
80
+
81
+ // Per-block resolution order: data-nowline-source-url → global sourceUrl
82
+ const perBlockUrl = code.getAttribute('data-nowline-source-url') ?? undefined;
83
+ const resolvedSourceUrl = perBlockUrl ?? inputs.sourceUrl;
84
+
85
+ tasks.push(
86
+ renderSource(source, opts).then(
87
+ (svg) => {
88
+ replaceWithSvg(target, svg);
89
+ rendered++;
90
+ if (parent !== null) {
91
+ const href = buildShareLink({
92
+ source,
93
+ sourceUrl: resolvedSourceUrl,
94
+ share,
95
+ });
96
+ if (href !== null) {
97
+ const a = doc.createElement('a');
98
+ a.className = 'nowline-share';
99
+ a.href = href;
100
+ a.target = '_blank';
101
+ a.rel = 'noopener noreferrer';
102
+ a.textContent = 'Share on Nowline';
103
+ parent.insertBefore(a, nextSibling);
104
+ }
105
+ }
106
+ },
107
+ (err: unknown) => {
108
+ failed++;
109
+ console.error('nowline: render failed', err);
110
+ },
111
+ ),
112
+ );
113
+ }
114
+
115
+ await Promise.all(tasks);
116
+ return { rendered, failed };
117
+ }
118
+
119
+ // Markdown-rendered Nowline blocks usually look like
120
+ // `<pre><code class="language-nowline">…</code></pre>`. We replace the
121
+ // outer `<pre>` so the spacing the markdown processor reserved for the
122
+ // fenced block is reused by the SVG. When the matched element has no
123
+ // `<pre>` ancestor (custom hosting) we replace the matched element
124
+ // itself.
125
+ function pickReplacementTarget(matched: HTMLElement): HTMLElement | null {
126
+ const parent = matched.parentElement;
127
+ if (parent && parent.tagName.toUpperCase() === 'PRE') {
128
+ return parent;
129
+ }
130
+ return matched;
131
+ }
132
+
133
+ function readBlockSource(code: HTMLElement): string {
134
+ // `textContent` preserves whitespace and newlines; `innerText` would
135
+ // collapse them per CSS, which would corrupt indentation-sensitive
136
+ // .nowline source.
137
+ return code.textContent ?? '';
138
+ }
139
+
140
+ function replaceWithSvg(target: HTMLElement, svg: string): void {
141
+ // `outerHTML` parses the SVG string into a real `<svg>` element and
142
+ // swaps it into the DOM, preserving each render's per-`idPrefix`
143
+ // `<style>` scope so two blocks on the page can never bleed styles.
144
+ target.outerHTML = svg;
145
+ }
146
+
147
+ export function __resetAutoScanForTests(): void {
148
+ runCounter = 0;
149
+ }