@mermaid-js/language-service 0.0.0-stage → 13.0.0-alpha.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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +43 -2
  3. package/dist/ast.d.ts +13 -0
  4. package/dist/ast.js +32 -0
  5. package/dist/completion.d.ts +19 -0
  6. package/dist/completion.js +51 -0
  7. package/dist/config/completion.d.ts +4 -0
  8. package/dist/config/completion.js +430 -0
  9. package/dist/config/front-matter.d.ts +50 -0
  10. package/dist/config/front-matter.js +232 -0
  11. package/dist/config/hover.d.ts +13 -0
  12. package/dist/config/hover.js +43 -0
  13. package/dist/config/schema.d.ts +58 -0
  14. package/dist/config/schema.js +98 -0
  15. package/dist/context.d.ts +37 -0
  16. package/dist/context.js +1 -0
  17. package/dist/diagnostics.d.ts +16 -0
  18. package/dist/diagnostics.js +80 -0
  19. package/dist/facets.d.ts +32 -0
  20. package/dist/facets.js +1 -0
  21. package/dist/folding.d.ts +14 -0
  22. package/dist/folding.js +34 -0
  23. package/dist/freeze.d.ts +6 -0
  24. package/dist/freeze.js +15 -0
  25. package/dist/generated/config-schema.gen.d.ts +3 -0
  26. package/dist/generated/config-schema.gen.js +6479 -0
  27. package/dist/generated/facets.gen.d.ts +8 -0
  28. package/dist/generated/facets.gen.js +110 -0
  29. package/dist/hover.d.ts +12 -0
  30. package/dist/hover.js +54 -0
  31. package/dist/index.d.ts +38 -0
  32. package/dist/index.js +22 -0
  33. package/dist/lines.d.ts +49 -0
  34. package/dist/lines.js +97 -0
  35. package/dist/markdown.d.ts +12 -0
  36. package/dist/markdown.js +67 -0
  37. package/dist/semantic-tokens.d.ts +26 -0
  38. package/dist/semantic-tokens.js +135 -0
  39. package/dist/service.d.ts +35 -0
  40. package/dist/service.js +167 -0
  41. package/dist/snapshot.d.ts +62 -0
  42. package/dist/snapshot.js +205 -0
  43. package/dist/symbols.d.ts +53 -0
  44. package/dist/symbols.js +229 -0
  45. package/dist/testing/goldens.d.ts +40 -0
  46. package/dist/testing/goldens.js +196 -0
  47. package/dist/testing/index.d.ts +25 -0
  48. package/dist/testing/index.js +24 -0
  49. package/package.json +67 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 - 2022 Knut Sveidqvist
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,44 @@
1
- # Temporary Holding Version
1
+ # @mermaid-js/language-service
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ An editor-agnostic language service for Mermaid: diagnostics, go to definition, references, rename, hover, completion (front-matter `config:` included), folding, semantic tokens, and a cursor ↔ AST map for live previews. It has no runtime dependencies. `@mermaid-js/core` is a peer, and each diagram's language support (its _facet_) is an optional peer that loads on `preload()`.
4
+
5
+ Use it directly, or through an adapter: `@mermaid-js/language-server` for any LSP editor, `@mermaid-js/language-service-codemirror` for CodeMirror 6, `@mermaid-js/language-service-monaco` for Monaco.
6
+
7
+ ## Setup
8
+
9
+ ```sh
10
+ npm install @mermaid-js/language-service @mermaid-js/core @mermaid-js/diagram-flowchart
11
+ ```
12
+
13
+ ```ts
14
+ import { createLanguageService } from '@mermaid-js/language-service';
15
+
16
+ const service = createLanguageService();
17
+ await service.preload(); // load the facets before the first update
18
+
19
+ const doc = service.update('file:///a.mmd', text); // synchronous, complete, immutable
20
+ doc.diagnostics();
21
+ doc.completions(offset);
22
+ doc.rename(offset, 'NewName'); // { edits } or { refused: reason }
23
+ doc.lineFoldingRanges(); // folds by line, as editors show them
24
+ doc.lineSemanticTokens(); // tokens cut per line
25
+ ```
26
+
27
+ Call `preload()` (or `preload({ types: ['flowchart'] })`) once, before the first `update`. A type whose facet is not loaded gets a `LANGUAGE_NOT_LOADED` diagnostic and generic features only; a facet that fails to load gets `LANGUAGE_LOAD_FAILED`, and one whose parser throws gets `LANGUAGE_FACET_FAILED`. `preload` never rejects.
28
+
29
+ Positions are UTF-16 offsets into the whole text (front matter included). `doc.lines()` converts to 0-based line and column.
30
+
31
+ ## Third-party diagram types
32
+
33
+ Pass their rows to `createLanguageService({ facets: [{ type, id, detector, load }] })`. A facet is written against the types this package re-exports (`LanguageFacet`, `ParseResult`, `ProbeResult`, `DocumentView`, `HoverTarget`, …). The shared golden suite runs from `@mermaid-js/language-service/testing` (Node only).
34
+
35
+ The language server does not load third-party facets by default: a client opts in with `initializationOptions: { mermaid: { plugins: true } }` (D157), so opening an untrusted folder runs none of its code.
36
+
37
+ ## Bundlers
38
+
39
+ - **Optional peers.** The first-party facets are dynamic imports of optional peers, such as `@mermaid-js/diagram-flowchart/language`. Bundlers resolve them at build time: install the diagram packages you want, or mark the missing ones external. Otherwise the build can fail instead of the service reporting `LANGUAGE_LOAD_FAILED`.
40
+ - **No top-level `await` of `preload()` in a Vite entry.** The facet and config-schema chunks import shared code from the entry chunk. An entry that awaits them at top level never finishes loading, with no error. Wrap the setup in a function: `async function start() { await service.preload(); … } void start();`.
41
+
42
+ ## Limitations
43
+
44
+ - Language support exists for flowchart, sequence, class, state and ER diagrams. Other types get generic features: comments, directives, and front-matter config.
package/dist/ast.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `astAt` / `rangeOf` (R-LANG-6, D97): between source offsets and semantic AST ids, so a host
3
+ * with a live preview maps the cursor to `[data-ast]` elements and a clicked element back.
4
+ */
5
+ import type { AstId, AstNode, Range } from '@mermaid-js/core/parser-kit';
6
+ /**
7
+ * Ids of the AST nodes around `offset` (a node contains the offsets from its start to its end,
8
+ * both included), innermost first: deeper nodes first, then shorter ones. The document root
9
+ * is left out.
10
+ */
11
+ export declare function astAt(root: AstNode | undefined, offset: number): AstId[];
12
+ /** Every AST id → its source range. */
13
+ export declare function astRanges(root: AstNode | undefined): Map<AstId, Range>;
package/dist/ast.js ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Ids of the AST nodes around `offset` (a node contains the offsets from its start to its end,
3
+ * both included), innermost first: deeper nodes first, then shorter ones. The document root
4
+ * is left out.
5
+ */
6
+ export function astAt(root, offset) {
7
+ if (!root)
8
+ return [];
9
+ const hits = [];
10
+ const walk = (node, depth) => {
11
+ for (const c of node.children ?? []) {
12
+ if (c.range.start > offset || offset > c.range.end)
13
+ continue;
14
+ hits.push({ id: c.id, depth, size: c.range.end - c.range.start });
15
+ walk(c, depth + 1);
16
+ }
17
+ };
18
+ walk(root, 0);
19
+ return hits.sort((a, b) => b.depth - a.depth || a.size - b.size).map((h) => h.id);
20
+ }
21
+ /** Every AST id → its source range. */
22
+ export function astRanges(root) {
23
+ const out = new Map();
24
+ const walk = (node) => {
25
+ out.set(node.id, node.range);
26
+ for (const c of node.children ?? [])
27
+ walk(c);
28
+ };
29
+ if (root)
30
+ walk(root);
31
+ return out;
32
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Completion (DESIGN §4.5, D95, D96). Front matter answers first: an offset inside it gets
3
+ * front-matter keys and values from the config schema, and nothing else. The diagram text is
4
+ * the facet's: the text is parsed again with `probe: offset` and the facet maps the probe to
5
+ * items. A text whose type is not detected (empty, or a header being typed) gets every loaded
6
+ * facet's answer on its first line: the diagram keywords.
7
+ */
8
+ import type { CompletionItem } from '@mermaid-js/core/parser-kit';
9
+ import type { FrontMatterTree } from './config/front-matter.ts';
10
+ import type { DocumentContext } from './context.ts';
11
+ /** Whether the front matter (fences included; an unclosed one to the end) owns `offset`. */
12
+ export declare function inFrontMatter(fm: FrontMatterTree, offset: number): boolean;
13
+ export declare function completions(ctx: DocumentContext, fm: FrontMatterTree | undefined, offset: number): CompletionItem[];
14
+ /**
15
+ * Characters after which an editor asks for completions without a typed word: front-matter
16
+ * `:`, arrows (`-`, `=`, `.`), `&`, `@`, `{` (shape data) and `,`. Every adapter uses this one
17
+ * list (LSP `triggerCharacters`, Monaco's, CodeMirror's activation while typing).
18
+ */
19
+ export declare const COMPLETION_TRIGGERS: readonly string[];
@@ -0,0 +1,51 @@
1
+ import { frontMatterCompletions } from "./config/completion.js";
2
+ /** Whether the front matter (fences included; an unclosed one to the end) owns `offset`. */
3
+ export function inFrontMatter(fm, offset) {
4
+ return fm.range.start <= offset && (offset < fm.range.end || fm.unclosed);
5
+ }
6
+ /** `facet`'s items for `text` at `offset`. */
7
+ function facetItems(facet, text, offset) {
8
+ const result = facet.parse(text, { keepTokens: true, probe: offset });
9
+ return result.probe ? facet.completions(result.probe, { text, result }) : [];
10
+ }
11
+ /** The header keywords of every loaded facet, on the first line of an undetected text. */
12
+ function headerItems(ctx, offset) {
13
+ const { text, start } = ctx;
14
+ const nl = text.slice(start).search(/[\r\n]/);
15
+ const lineEnd = nl < 0 ? text.length : start + nl;
16
+ if (offset < start || offset > lineEnd)
17
+ return [];
18
+ const head = text.slice(0, lineEnd);
19
+ const out = [];
20
+ for (const facet of ctx.available) {
21
+ for (const item of facetItems(facet, head, offset)) {
22
+ if (item.kind === 'keyword' && !out.some((x) => x.label === item.label))
23
+ out.push(item);
24
+ }
25
+ }
26
+ return out;
27
+ }
28
+ export function completions(ctx, fm, offset) {
29
+ if (fm !== undefined && inFrontMatter(fm, offset))
30
+ return frontMatterCompletions(ctx, fm, offset);
31
+ if (ctx.entry === undefined)
32
+ return headerItems(ctx, offset);
33
+ if (!ctx.facet)
34
+ return [];
35
+ return facetItems(ctx.facet, ctx.text, offset);
36
+ }
37
+ /**
38
+ * Characters after which an editor asks for completions without a typed word: front-matter
39
+ * `:`, arrows (`-`, `=`, `.`), `&`, `@`, `{` (shape data) and `,`. Every adapter uses this one
40
+ * list (LSP `triggerCharacters`, Monaco's, CodeMirror's activation while typing).
41
+ */
42
+ export const COMPLETION_TRIGGERS = Object.freeze([
43
+ ':',
44
+ '-',
45
+ '=',
46
+ '.',
47
+ '&',
48
+ '@',
49
+ '{',
50
+ ',',
51
+ ]);
@@ -0,0 +1,4 @@
1
+ import type { CompletionItem } from '@mermaid-js/core/parser-kit';
2
+ import type { DocumentContext } from '../context.ts';
3
+ import type { FrontMatterTree } from './front-matter.ts';
4
+ export declare function frontMatterCompletions(ctx: DocumentContext, fm: FrontMatterTree, offset: number): CompletionItem[];
@@ -0,0 +1,430 @@
1
+ /**
2
+ * Front-matter completion (D96): keys and values of front matter in block or flow style, with
3
+ * plain or quoted keys (#169). Top level:
4
+ * `title`, `displayMode`, `config`. Under `config`: the schema's keys, leaving out removed and
5
+ * moved keys, the keys diagram text cannot set (core's secure list) and `look` (D153); a
6
+ * diagram section only when it is the one the renderer reads for the detected type (every
7
+ * section while no type is detected yet). Values: the key's enum, or `true`/`false`.
8
+ * Inserted text follows the document's line ends and the line's indent (tabs or spaces); in
9
+ * flow style a key is inserted as `key: `, and a quoted key as its name only.
10
+ *
11
+ * The cursor's place comes from the text, not the tree: a key being typed is not YAML yet.
12
+ * One forward scan over the lines before the cursor finds the flow collections (`{…}`, `[…]`)
13
+ * and skips those that close before it; inside one, the scan of it gives the keys on the way.
14
+ * In block style the parent keys come from one backward scan over the lines above. Both are
15
+ * linear.
16
+ */
17
+ import { lineEnd } from '@mermaid-js/core/parser-kit';
18
+ import { configSection, liveKeys, mapAt, secureKeys } from "./front-matter.js";
19
+ import { FRONT_MATTER_KEYS } from "./hover.js";
20
+ import { SchemaView } from "./schema.js";
21
+ /** A key as completion reads it: plain, or a `"…"` or `'…'` key whose text is a plain key. */
22
+ const KEY = String.raw `(?:([\w$][\w$-]*)|"([\w$-]+)"|'([\w$-]+)')`;
23
+ /** A key being typed: indent, an opening quote if any, the text so far. */
24
+ const KEY_LINE = /^([ \t]*)(["']?)([\w$-]*)$/;
25
+ const VALUE_LINE = new RegExp(String.raw `^([ \t]*)${KEY}[ \t]*:[ \t]+([^\s#'"]*)$`);
26
+ /** A parent line: `key:` with nothing (or a comment) after it. */
27
+ const PARENT_LINE = new RegExp(String.raw `^${KEY}[ \t]*:[ \t]*(?:#.*)?$`);
28
+ /** A line that opens a flow collection: `key: {`, `- [`, or a bare `{` (a root document). */
29
+ const FLOW_LINE = new RegExp(String.raw `^([ \t]*)(?:${KEY}[ \t]*:[ \t]+|(-[ \t]+))?[{[]`);
30
+ const PLAIN_KEY = /[\w$-]/;
31
+ /** Characters of a plain value inside a flow collection, as far as completion goes. */
32
+ const FLOW_VALUE = /[^\s,[\]{}#'"]/;
33
+ const FLOW_BLANK = /[\s,[\]{}]/;
34
+ /** The key a {@link KEY} match names, from its first group at `at`. */
35
+ function keyOf(m, at) {
36
+ return (m[at] ?? m[at + 1] ?? m[at + 2]);
37
+ }
38
+ /** Where the line holding `offset` starts. */
39
+ function lineStartOf(text, offset, floor) {
40
+ return Math.max(floor, text.lastIndexOf('\n', offset - 1) + 1);
41
+ }
42
+ /** End of the run of characters matching `re` from `at` (stops at `limit`). */
43
+ function runEnd(text, at, limit, re) {
44
+ let i = at;
45
+ while (i < limit && re.test(text.charAt(i)))
46
+ i++;
47
+ return i;
48
+ }
49
+ /**
50
+ * The keys of the block mappings that hold a line indented by `indent`, outermost first, or
51
+ * `undefined` when the lines above do not nest block mappings down to it.
52
+ */
53
+ function parentPath(text, fm, lineStart, indent) {
54
+ const base = fm.indent;
55
+ if (indent < base)
56
+ return undefined;
57
+ const path = [];
58
+ let threshold = indent;
59
+ let end = lineStart;
60
+ while (threshold > base && end > fm.yamlRange.start) {
61
+ const start = lineStartOf(text, end - 1, fm.yamlRange.start);
62
+ const line = text.slice(start, end).replace(/\r?\n$/, '');
63
+ end = start;
64
+ const content = line.trimStart();
65
+ if (content === '' || content.startsWith('#'))
66
+ continue;
67
+ const ind = line.length - content.length;
68
+ if (ind >= threshold)
69
+ continue;
70
+ const key = PARENT_LINE.exec(content.trimEnd());
71
+ if (key === null)
72
+ return undefined;
73
+ path.unshift(keyOf(key, 1));
74
+ threshold = ind;
75
+ }
76
+ return threshold <= base ? path : undefined;
77
+ }
78
+ /** A short type for the list: `string`, `boolean`, `object`, … */
79
+ function shortType(schema, node) {
80
+ if (schema.shape(node) !== 'leaf')
81
+ return 'object';
82
+ const n = schema.resolve(node);
83
+ return n.type === undefined ? 'any' : [n.type].flat().join(' | ');
84
+ }
85
+ /** A string as a YAML plain scalar when it reads back as itself, else single-quoted. */
86
+ function yamlString(s) {
87
+ const plain = /^[A-Za-z_][\w.-]*$/.test(s) && !/^(?:true|false|null|yes|no|on|off|y|n)$/i.test(s);
88
+ return plain ? s : `'${s.replace(/'/g, "''")}'`;
89
+ }
90
+ /** Whether a root config key is offered: a diagram section only when it is the type's own. */
91
+ function offered(ctx, schema, key) {
92
+ if (key === 'look' || secureKeys().has(key))
93
+ return false;
94
+ if (!schema.isDiagramSection(key) || ctx.entry === undefined)
95
+ return true;
96
+ return key === configSection(ctx);
97
+ }
98
+ /** Whether a config path is somewhere completion offers nothing (`sequence.*` in a flowchart). */
99
+ function closedPath(ctx, schema, path) {
100
+ return (path.length > 0 && !offered(ctx, schema, path[0])) || path.includes('look');
101
+ }
102
+ /** The document's line end, and the indent one level below `indent`. */
103
+ function layout(text, fm, indent) {
104
+ const eol = text.slice(fm.range.start, fm.range.end).includes('\r\n') ? '\r\n' : '\n';
105
+ return [eol, indent + (indent.includes('\t') ? '\t' : ' ')];
106
+ }
107
+ function keyItems(ctx, fm, parent, range, style, indent) {
108
+ const [eol, inner] = layout(ctx.text, fm, indent);
109
+ const insert = (key, object) => {
110
+ if (style === 'name')
111
+ return key;
112
+ return object && style === 'block' ? `${key}:${eol}${inner}` : `${key}: `;
113
+ };
114
+ // Keys already in this mapping, other than the one being typed.
115
+ const present = new Set((mapAt(fm.root, parent)?.pairs ?? [])
116
+ .filter((p) => p.keyRange.end < range.start || p.keyRange.start > range.end)
117
+ .map((p) => p.key));
118
+ if (parent.length === 0) {
119
+ return Object.entries(FRONT_MATTER_KEYS)
120
+ .filter(([key]) => !present.has(key))
121
+ .map(([key, doc]) => ({
122
+ label: key,
123
+ kind: 'property',
124
+ insertText: insert(key, key === 'config'),
125
+ range,
126
+ detail: key === 'config' ? 'object' : 'string',
127
+ documentation: doc,
128
+ }));
129
+ }
130
+ if (parent[0] !== 'config' || ctx.schema === undefined)
131
+ return [];
132
+ const schema = new SchemaView(ctx.schema);
133
+ const path = parent.slice(1);
134
+ if (closedPath(ctx, schema, path))
135
+ return [];
136
+ const node = schema.at(path);
137
+ if (node === undefined || schema.shape(node) !== 'closed')
138
+ return [];
139
+ const props = schema.properties(node);
140
+ return liveKeys(props)
141
+ .filter((key) => !present.has(key) && key !== 'look')
142
+ .filter((key) => path.length > 0 || offered(ctx, schema, key))
143
+ .map((key) => {
144
+ const child = props.get(key);
145
+ const description = schema.resolve(child).description?.trim();
146
+ const item = {
147
+ label: key,
148
+ kind: 'property',
149
+ insertText: insert(key, schema.shape(child) !== 'leaf'),
150
+ range,
151
+ detail: shortType(schema, child),
152
+ };
153
+ return description === undefined ? item : { ...item, documentation: description };
154
+ });
155
+ }
156
+ function valueItems(ctx, path, range) {
157
+ if (ctx.schema === undefined)
158
+ return [];
159
+ const schema = new SchemaView(ctx.schema);
160
+ const config = path.slice(1);
161
+ let at;
162
+ // Top-level `displayMode` is `config.gantt.displayMode`: offered where gantt config is.
163
+ if (path.length === 1 && path[0] === 'displayMode') {
164
+ if (offered(ctx, schema, 'gantt'))
165
+ at = schema.at(['gantt', 'displayMode']);
166
+ }
167
+ else if (path[0] === 'config' && config.length > 0 && !closedPath(ctx, schema, config))
168
+ at = schema.at(config);
169
+ if (at === undefined)
170
+ return [];
171
+ const node = schema.resolve(at);
172
+ const docs = node['meta:enum'] ?? {};
173
+ const types = node.type === undefined ? [] : [node.type].flat();
174
+ const values = node.enum ?? (types.includes('boolean') ? [true, false] : []);
175
+ return values.map((v) => {
176
+ const text = typeof v === 'string' ? yamlString(v) : JSON.stringify(v);
177
+ const doc = typeof v === 'string' && Object.hasOwn(docs, v) ? docs[v] : undefined;
178
+ const item = { label: text, kind: 'value', insertText: text, range };
179
+ const withDetail = v === node.default ? { ...item, detail: 'default' } : item;
180
+ return doc === undefined ? withDetail : { ...withDetail, documentation: doc };
181
+ });
182
+ }
183
+ /** The offset after a quoted value opening at `at` (or `limit` when it is never closed). */
184
+ function quotedEnd(text, at, limit) {
185
+ const q = text.charAt(at);
186
+ for (let i = at + 1; i < limit; i++) {
187
+ const c = text.charAt(i);
188
+ if (q === '"' && c === '\\')
189
+ i++;
190
+ else if (c === q && q === "'" && text.charAt(i + 1) === q)
191
+ i++;
192
+ else if (c === q)
193
+ return i + 1;
194
+ }
195
+ return limit;
196
+ }
197
+ /** The place at `range` in `frame`, or `null` when nothing is offered there. */
198
+ function placeIn(frame, range, quoted) {
199
+ if (frame.path === undefined || !frame.map || frame.done)
200
+ return null;
201
+ if (!frame.value)
202
+ return { kind: 'key', path: frame.path, range, quoted };
203
+ if (frame.key === undefined || quoted)
204
+ return null;
205
+ return { kind: 'value', path: [...frame.path, frame.key], range, quoted };
206
+ }
207
+ /**
208
+ * A scan of the flow collection opening at `open`, whose keys from the root are `path`, up to
209
+ * `limit`, for the cursor at `offset`. Each step reads one token at `i`, and returns the
210
+ * cursor's place (`null`: nothing is offered there) once the token holds the cursor.
211
+ */
212
+ class FlowScan {
213
+ text;
214
+ path;
215
+ offset;
216
+ limit;
217
+ stack = [];
218
+ i;
219
+ constructor(text, open, path, offset, limit) {
220
+ this.text = text;
221
+ this.path = path;
222
+ this.offset = offset;
223
+ this.limit = limit;
224
+ this.i = open;
225
+ }
226
+ /** The cursor's place when it is inside the collection, else the offset after it. */
227
+ run() {
228
+ const { text, offset } = this;
229
+ while (this.i < this.limit) {
230
+ const c = text.charAt(this.i);
231
+ const top = this.stack.at(-1);
232
+ if (c === '{' || c === '[') {
233
+ // The cursor right before a bracket is between tokens, as before a `,`.
234
+ if (top !== undefined && this.i === offset)
235
+ return placeIn(top, this.here(), false);
236
+ this.open(c, top);
237
+ continue;
238
+ }
239
+ if (top === undefined)
240
+ return this.i;
241
+ if (this.i === offset && (FLOW_BLANK.test(c) || c === ':' || c === '#'))
242
+ return placeIn(top, this.here(), false);
243
+ const r = this.step(c, top);
244
+ if (r !== undefined)
245
+ return r;
246
+ if (this.stack.length === 0)
247
+ return this.i;
248
+ }
249
+ const top = this.stack.at(-1);
250
+ return top === undefined ? this.i : placeIn(top, this.here(), false);
251
+ }
252
+ here() {
253
+ return { start: this.offset, end: this.offset };
254
+ }
255
+ /** A nested collection: its path goes through the key whose value it is. */
256
+ open(c, top) {
257
+ let inner = this.path;
258
+ if (top !== undefined) {
259
+ const keyed = top.map && top.value && top.key !== undefined && top.path !== undefined;
260
+ inner = keyed ? [...top.path, top.key] : undefined;
261
+ top.done = true;
262
+ }
263
+ this.stack.push({ map: c === '{', path: inner, value: false, done: false });
264
+ this.i++;
265
+ }
266
+ /** One token other than an opening bracket; the place when it holds the cursor. */
267
+ step(c, top) {
268
+ const { text } = this;
269
+ if (c === '}' || c === ']') {
270
+ this.stack.pop();
271
+ this.i++;
272
+ }
273
+ else if (c === ',') {
274
+ top.key = undefined;
275
+ top.value = false;
276
+ top.done = false;
277
+ this.i++;
278
+ }
279
+ else if (/\s/.test(c)) {
280
+ this.i++;
281
+ }
282
+ else if (c === '#' && /\s/.test(text.charAt(this.i - 1))) {
283
+ const end = Math.min(lineEnd(text, this.i), this.limit);
284
+ if (this.offset > this.i && this.offset <= end)
285
+ return null;
286
+ this.i = end;
287
+ }
288
+ else if (c === ':' && top.map && !top.value) {
289
+ top.value = true;
290
+ this.i++;
291
+ }
292
+ else if (c === '"' || c === "'") {
293
+ return this.quoted(c, top);
294
+ }
295
+ else {
296
+ return this.plain(top);
297
+ }
298
+ return undefined;
299
+ }
300
+ /** A quoted scalar; the cursor inside the quotes types a key. */
301
+ quoted(q, top) {
302
+ const { text, offset } = this;
303
+ const at = this.i;
304
+ const end = quotedEnd(text, at, this.limit);
305
+ const closed = end > at + 1 && text.charAt(end - 1) === q;
306
+ if (offset > at && (offset < end || !closed)) {
307
+ if (!/^[\w$-]*$/.test(text.slice(at + 1, offset)))
308
+ return null;
309
+ return placeIn(top, { start: at + 1, end: runEnd(text, offset, this.limit, PLAIN_KEY) }, true);
310
+ }
311
+ if (top.map && !top.value)
312
+ top.key = text.slice(at + 1, end - 1);
313
+ else
314
+ top.done = true;
315
+ this.i = end;
316
+ return undefined;
317
+ }
318
+ /** A plain scalar: to a flow indicator, the line end, `: ` after a key, or ` #`. */
319
+ plain(top) {
320
+ const { text, offset } = this;
321
+ const start = this.i;
322
+ const inKey = top.map && !top.value;
323
+ let i = start;
324
+ while (i < this.limit) {
325
+ const d = text.charAt(i);
326
+ if (',[]{}\r\n'.includes(d))
327
+ break;
328
+ if (d === ':' && inKey && FLOW_BLANK.test(text.charAt(i + 1) || ' '))
329
+ break;
330
+ if (d === '#' && /\s/.test(text.charAt(i - 1)))
331
+ break;
332
+ i++;
333
+ }
334
+ this.i = i;
335
+ if (offset >= start && offset <= i) {
336
+ const re = inKey ? PLAIN_KEY : FLOW_VALUE;
337
+ for (let k = start; k < offset; k++)
338
+ if (!re.test(text.charAt(k)))
339
+ return null;
340
+ return placeIn(top, { start, end: runEnd(text, offset, this.limit, re) }, false);
341
+ }
342
+ if (inKey)
343
+ top.key = text.slice(start, i).trim();
344
+ else
345
+ top.done = true;
346
+ return undefined;
347
+ }
348
+ }
349
+ /**
350
+ * The cursor's place when it is inside a flow collection, from one forward scan over the lines
351
+ * before it that skips the collections closing before it. `undefined` when it is not inside one
352
+ * (block style), `null` when it is but nothing is offered there.
353
+ */
354
+ function flowPlace(text, fm, offset) {
355
+ const yaml = fm.yamlRange;
356
+ let lineStart = yaml.start;
357
+ let first = true;
358
+ while (lineStart < yaml.end && lineStart <= offset) {
359
+ const end = runEnd(text, lineStart, yaml.end, /[^\r\n]/);
360
+ const line = text.slice(lineStart, end);
361
+ let next = end;
362
+ const m = FLOW_LINE.exec(line);
363
+ if (m !== null) {
364
+ const open = lineStart + m[0].length - 1;
365
+ const indent = m[1].length;
366
+ // The keys on the way: `key: {` under its block parents, none for `- {`, a root `{`.
367
+ let path = false;
368
+ if (m[2] !== undefined || m[3] !== undefined || m[4] !== undefined) {
369
+ const parent = parentPath(text, fm, lineStart, indent);
370
+ path = parent === undefined ? undefined : [...parent, keyOf(m, 2)];
371
+ }
372
+ else if (m[5] !== undefined)
373
+ path = undefined;
374
+ else if (first && indent === fm.indent)
375
+ path = [];
376
+ if (path !== false && open < offset) {
377
+ const r = new FlowScan(text, open, path, offset, yaml.end).run();
378
+ if (typeof r !== 'number')
379
+ return r;
380
+ next = r;
381
+ }
382
+ }
383
+ if (line.trim() !== '' && !line.trimStart().startsWith('#'))
384
+ first = false;
385
+ const e = runEnd(text, next, yaml.end, /[^\r\n]/);
386
+ if (e >= yaml.end)
387
+ break;
388
+ lineStart = e + (text.startsWith('\r\n', e) ? 2 : 1);
389
+ }
390
+ return undefined;
391
+ }
392
+ export function frontMatterCompletions(ctx, fm, offset) {
393
+ const { text } = ctx;
394
+ const yaml = fm.yamlRange;
395
+ if (offset < yaml.start || offset > yaml.end)
396
+ return [];
397
+ const lineStart = lineStartOf(text, offset, yaml.start);
398
+ const cursorLineEnd = runEnd(text, offset, yaml.end, /[^\r\n]/);
399
+ const prefix = text.slice(lineStart, offset);
400
+ const flow = flowPlace(text, fm, offset);
401
+ if (flow === null)
402
+ return [];
403
+ if (flow !== undefined) {
404
+ if (flow.kind === 'value')
405
+ return valueItems(ctx, flow.path, flow.range);
406
+ const colon = /^[ \t]*:/.test(text.slice(flow.range.end, cursorLineEnd));
407
+ return keyItems(ctx, fm, flow.path, flow.range, flow.quoted || colon ? 'name' : 'flow', '');
408
+ }
409
+ const key = KEY_LINE.exec(prefix);
410
+ if (key !== null) {
411
+ const indent = key[1];
412
+ const parent = parentPath(text, fm, lineStart, indent.length);
413
+ if (parent === undefined)
414
+ return [];
415
+ const end = runEnd(text, offset, cursorLineEnd, PLAIN_KEY);
416
+ const colon = /^[ \t]*:/.test(text.slice(end, cursorLineEnd));
417
+ const style = key[2] !== '' || colon ? 'name' : 'block';
418
+ return keyItems(ctx, fm, parent, { start: offset - key[3].length, end }, style, indent);
419
+ }
420
+ const value = VALUE_LINE.exec(prefix);
421
+ if (value !== null) {
422
+ const parent = parentPath(text, fm, lineStart, value[1].length);
423
+ if (parent === undefined)
424
+ return [];
425
+ const end = runEnd(text, offset, cursorLineEnd, /[^\s#]/);
426
+ const range = { start: offset - value[5].length, end };
427
+ return valueItems(ctx, [...parent, keyOf(value, 2)], range);
428
+ }
429
+ return [];
430
+ }
@@ -0,0 +1,50 @@
1
+ import type { Diagnostic, Range } from '@mermaid-js/core/parser-kit';
2
+ import type { YamlMapNode, YamlNode, YamlPair } from '@mermaid-js/core/preprocess';
3
+ import type { DocumentContext } from '../context.ts';
4
+ import type { SchemaNode } from './schema.ts';
5
+ /** The front matter of one snapshot, read once. */
6
+ export interface FrontMatterTree {
7
+ /** The whole block, fences included; an unclosed one runs to the end of the text. */
8
+ readonly range: Range;
9
+ /** The YAML between the fences (to the end of the text when unclosed). */
10
+ readonly yamlRange: Range;
11
+ /** The fence's indent, which every YAML line shares. */
12
+ readonly indent: number;
13
+ /** No closing fence: the block runs to the end of the text, as preprocess reads it. */
14
+ readonly unclosed: boolean;
15
+ /** The document's root node; `null` when nothing could be read. */
16
+ readonly root: YamlNode | null;
17
+ /**
18
+ * The tree mode's YAML diagnostics (#168): one per bad line, where preprocess (and so the
19
+ * renderer, D91) stops at the first. Empty for an unclosed block, which preprocess does not
20
+ * read as YAML.
21
+ */
22
+ readonly diagnostics: readonly Diagnostic[];
23
+ }
24
+ export declare function frontMatterTree(ctx: DocumentContext): FrontMatterTree | undefined;
25
+ /**
26
+ * The config section the renderer resolves this type's config with (mermaid's `sectionOf`:
27
+ * the plugin id): a first-party row's generated `section` (none for a type with no v13
28
+ * package), a third-party row's id.
29
+ */
30
+ export declare function configSection(ctx: DocumentContext): string | undefined;
31
+ /** The mapping at a key path from the root (the first pair of a key), if there is one. */
32
+ export declare function mapAt(root: YamlNode | null, path: readonly string[]): YamlMapNode | undefined;
33
+ /** The key whose text holds `offset` (or ends at it), with its path from the root. */
34
+ export declare function keyAt(root: YamlNode | null, offset: number): {
35
+ path: string[];
36
+ pair: YamlPair;
37
+ } | undefined;
38
+ /** The known key closest to `key`, when one is close enough to be a typo of it. */
39
+ export declare function closest(key: string, known: Iterable<string>): string | undefined;
40
+ /** Keys a user can still set (not removed or moved). */
41
+ export declare function liveKeys(props: Map<string, SchemaNode>): string[];
42
+ /** Keys diagram text cannot set: core's integrator-only and secure keys (R-SEC-5, R-SEC-6). */
43
+ export declare function secureKeys(): ReadonlySet<string>;
44
+ /**
45
+ * The config warnings a render gives for this front matter (core `resolveText`: unknown,
46
+ * removed, moved and secure keys, `look`, unsafe values), each on the key it names, an
47
+ * unknown one with a "did you mean" among the keys diagram text can set. None for an
48
+ * unclosed front matter or YAML with errors, which a render reads no config from.
49
+ */
50
+ export declare function configDiagnostics(ctx: DocumentContext, fm: FrontMatterTree | undefined): Diagnostic[];