@nvl/sveltex-language-server 0.2.0

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 (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +288 -0
  3. package/bin/server.js +10 -0
  4. package/dist/core/config.d.ts +126 -0
  5. package/dist/core/config.js +569 -0
  6. package/dist/core/diagnostics.d.ts +34 -0
  7. package/dist/core/diagnostics.js +67 -0
  8. package/dist/core/frontmatter-data.d.ts +74 -0
  9. package/dist/core/frontmatter-data.js +323 -0
  10. package/dist/core/frontmatter.d.ts +25 -0
  11. package/dist/core/frontmatter.js +348 -0
  12. package/dist/core/lsp-proxy.d.ts +77 -0
  13. package/dist/core/lsp-proxy.js +165 -0
  14. package/dist/core/mapper.d.ts +86 -0
  15. package/dist/core/mapper.js +223 -0
  16. package/dist/core/mapping.d.ts +59 -0
  17. package/dist/core/mapping.js +37 -0
  18. package/dist/core/markdown.d.ts +34 -0
  19. package/dist/core/markdown.js +215 -0
  20. package/dist/core/region-forwarding.d.ts +90 -0
  21. package/dist/core/region-forwarding.js +428 -0
  22. package/dist/core/region-virtual.d.ts +71 -0
  23. package/dist/core/region-virtual.js +131 -0
  24. package/dist/core/regions.d.ts +56 -0
  25. package/dist/core/regions.js +221 -0
  26. package/dist/core/remap.d.ts +84 -0
  27. package/dist/core/remap.js +272 -0
  28. package/dist/core/server-helpers.d.ts +109 -0
  29. package/dist/core/server-helpers.js +182 -0
  30. package/dist/core/server.d.ts +13 -0
  31. package/dist/core/server.js +604 -0
  32. package/dist/core/svelte-proxy.d.ts +100 -0
  33. package/dist/core/svelte-proxy.js +144 -0
  34. package/dist/core/texlab.d.ts +26 -0
  35. package/dist/core/texlab.js +121 -0
  36. package/dist/core/virtual-svelte.d.ts +32 -0
  37. package/dist/core/virtual-svelte.js +67 -0
  38. package/dist/index.d.ts +29 -0
  39. package/dist/index.js +46 -0
  40. package/package.json +73 -0
@@ -0,0 +1,569 @@
1
+ // File description: Locates the user's `svelte.config.*` — which always wires
2
+ // SvelTeX in as a preprocessor — and distills the SvelTeX configuration it
3
+ // carries into a `SveltexConfigSnapshot`: the minimal slice of configuration
4
+ // that the region detector ({@link computeRegions}) needs.
5
+ import { spawn } from 'node:child_process';
6
+ import { existsSync } from 'node:fs';
7
+ import { dirname, join } from 'node:path';
8
+ import { getDefaultMathConfig } from '@nvl/sveltex';
9
+ /**
10
+ * Returns a {@link SveltexConfigSnapshot} populated entirely from SvelTeX's
11
+ * built-in defaults. Used as the base that a loaded config is merged onto, and
12
+ * as the fallback when no config file exists or loading fails.
13
+ */
14
+ export function defaultConfigSnapshot() {
15
+ return {
16
+ verbatimTags: ['tex', 'latex', 'tikz', 'verb', 'verbatim'],
17
+ // The VS Code extension's `sveltex.latexTags` setting defaults to the
18
+ // same three; keep them in step.
19
+ latexTags: ['tex', 'latex', 'tikz'],
20
+ extensions: ['.sveltex'],
21
+ mathDelims: getDefaultMathConfig('mathjax').delims,
22
+ mathBackend: 'mathjax',
23
+ directives: { enabled: false, bracesArePartOfDirective: null },
24
+ texScaffolds: {},
25
+ configPath: undefined,
26
+ };
27
+ }
28
+ /**
29
+ * Candidate file names for a Svelte config, in priority order.
30
+ * `svelte.config.*` is the SvelTeX configuration's only reliable source:
31
+ * SvelTeX has to be registered there as a preprocessor to be active at all,
32
+ * whereas a dedicated `sveltex.config.*` file is optional. Recent Svelte
33
+ * tooling also accepts TypeScript configs, so those extensions are matched.
34
+ */
35
+ const SVELTE_CONFIG_FILE_NAMES = [
36
+ 'svelte.config.js',
37
+ 'svelte.config.mjs',
38
+ 'svelte.config.cjs',
39
+ 'svelte.config.ts',
40
+ 'svelte.config.mts',
41
+ 'svelte.config.cts',
42
+ ];
43
+ /**
44
+ * Searches `workspaceRoot` for a `svelte.config.*` file.
45
+ *
46
+ * @returns The absolute path of the first one found, or `undefined`.
47
+ */
48
+ export function findSvelteConfigFile(workspaceRoot) {
49
+ for (const name of SVELTE_CONFIG_FILE_NAMES) {
50
+ const candidate = join(workspaceRoot, name);
51
+ if (existsSync(candidate))
52
+ return candidate;
53
+ }
54
+ return undefined;
55
+ }
56
+ /**
57
+ * Narrowing helper: `true` for non-null objects.
58
+ */
59
+ function isObject(value) {
60
+ return typeof value === 'object' && value !== null;
61
+ }
62
+ /**
63
+ * Extracts the verbatim environment names from a user config object.
64
+ *
65
+ * A SvelTeX config's `verbatim` field is a record keyed by environment name, so
66
+ * its keys are exactly the verbatim tags.
67
+ */
68
+ function readVerbatimTags(config) {
69
+ const verbatim = config['verbatim'];
70
+ if (!isObject(verbatim))
71
+ return undefined;
72
+ const keys = Object.keys(verbatim);
73
+ return keys.length > 0 ? keys : undefined;
74
+ }
75
+ /**
76
+ * Extracts the LaTeX / TeX verbatim environment names from a user config
77
+ * object: the names (and aliases) of every `verbatim` entry whose `type` is
78
+ * `'tex'`.
79
+ *
80
+ * @returns The deduplicated tag list, or `undefined` if the config declares no
81
+ * `tex`-typed verbatim environment.
82
+ */
83
+ function readLatexTags(config) {
84
+ const verbatim = config['verbatim'];
85
+ if (!isObject(verbatim))
86
+ return undefined;
87
+ const tags = new Set();
88
+ for (const [name, entry] of Object.entries(verbatim)) {
89
+ if (!isObject(entry) || entry['type'] !== 'tex')
90
+ continue;
91
+ tags.add(name);
92
+ const aliases = entry['aliases'];
93
+ if (Array.isArray(aliases)) {
94
+ for (const alias of aliases) {
95
+ if (typeof alias === 'string')
96
+ tags.add(alias);
97
+ }
98
+ }
99
+ }
100
+ return tags.size > 0 ? [...tags] : undefined;
101
+ }
102
+ /**
103
+ * SvelTeX's default TeX preamble — used when a `type: 'tex'` verbatim entry
104
+ * declares none. Mirrors the documented default of `verbatim.<env>.preamble`.
105
+ */
106
+ const DEFAULT_TEX_PREAMBLE = [
107
+ '\\usepackage{microtype}',
108
+ '\\usepackage{tikz}',
109
+ '\\usepackage{mathtools}',
110
+ '\\usepackage{xcolor}',
111
+ ].join('\n');
112
+ /**
113
+ * Renders a verbatim entry's `documentClass` setting (a string, or a
114
+ * `{ name, options }` object, or absent) into a `\documentclass[…]{…}` line.
115
+ * SvelTeX's default class for TeX components is `standalone`.
116
+ */
117
+ function readDocumentClass(value) {
118
+ if (typeof value === 'string')
119
+ return `\\documentclass{${value}}`;
120
+ let name = 'standalone';
121
+ let options = [];
122
+ if (isObject(value)) {
123
+ if (typeof value['name'] === 'string')
124
+ name = value['name'];
125
+ if (Array.isArray(value['options'])) {
126
+ options = value['options'].filter((option) => typeof option === 'string');
127
+ }
128
+ }
129
+ return options.length > 0
130
+ ? `\\documentclass[${options.join(',')}]{${name}}`
131
+ : `\\documentclass{${name}}`;
132
+ }
133
+ /**
134
+ * Extracts a {@link TexScaffold} for every `type: 'tex'` verbatim environment
135
+ * in a user config object, keyed by lower-cased tag (its name and each alias).
136
+ *
137
+ * This is the `\documentclass` + `preamble` SvelTeX itself wraps the
138
+ * environment's content in; the LSP reuses it so TexLab sees the project's
139
+ * real packages and preamble macros. (SvelTeX additionally folds in
140
+ * preset-derived packages via its internal `extendedPreamble`; the explicit
141
+ * `preamble` string read here is the part that carries the user's intent.)
142
+ */
143
+ function readTexScaffolds(config) {
144
+ const verbatim = config['verbatim'];
145
+ if (!isObject(verbatim))
146
+ return {};
147
+ const scaffolds = {};
148
+ for (const [name, entry] of Object.entries(verbatim)) {
149
+ if (!isObject(entry) || entry['type'] !== 'tex')
150
+ continue;
151
+ const scaffold = {
152
+ documentClass: readDocumentClass(entry['documentClass']),
153
+ preamble: typeof entry['preamble'] === 'string'
154
+ ? entry['preamble']
155
+ : DEFAULT_TEX_PREAMBLE,
156
+ };
157
+ const tags = [name];
158
+ const aliases = entry['aliases'];
159
+ if (Array.isArray(aliases)) {
160
+ for (const alias of aliases) {
161
+ if (typeof alias === 'string')
162
+ tags.push(alias);
163
+ }
164
+ }
165
+ for (const tag of tags)
166
+ scaffolds[tag.toLowerCase()] = scaffold;
167
+ }
168
+ return scaffolds;
169
+ }
170
+ /**
171
+ * Extracts the math backend from a SvelTeX object.
172
+ *
173
+ * The backend can sit in two places: directly as a `mathBackend` property (a
174
+ * resolved `Sveltex` instance exposes one) or, for a config that just declares
175
+ * backend choices, as `backendChoices.mathBackend`.
176
+ *
177
+ * @returns The backend, or `undefined` if none is declared.
178
+ */
179
+ function readMathBackend(config) {
180
+ const isBackend = (value) => value === 'mathjax' ||
181
+ value === 'katex' ||
182
+ value === 'custom' ||
183
+ value === 'none';
184
+ if (isBackend(config['mathBackend']))
185
+ return config['mathBackend'];
186
+ const choices = config['backendChoices'];
187
+ if (isObject(choices) && isBackend(choices['mathBackend'])) {
188
+ return choices['mathBackend'];
189
+ }
190
+ return undefined;
191
+ }
192
+ /**
193
+ * Extracts math-delimiter settings from a user config object, falling back to
194
+ * `base` for any field the user did not specify.
195
+ */
196
+ function readMathDelims(config, base) {
197
+ const math = config['math'];
198
+ if (!isObject(math))
199
+ return base;
200
+ const delims = math['delims'];
201
+ if (!isObject(delims))
202
+ return base;
203
+ const inline = isObject(delims['inline']) ? delims['inline'] : {};
204
+ const display = isObject(delims['display']) ? delims['display'] : {};
205
+ return {
206
+ dollars: typeof delims['dollars'] === 'boolean'
207
+ ? delims['dollars']
208
+ : base.dollars,
209
+ inline: {
210
+ singleDollar: typeof inline['singleDollar'] === 'boolean'
211
+ ? inline['singleDollar']
212
+ : base.inline.singleDollar,
213
+ escapedParentheses: typeof inline['escapedParentheses'] === 'boolean'
214
+ ? inline['escapedParentheses']
215
+ : base.inline.escapedParentheses,
216
+ },
217
+ display: {
218
+ escapedSquareBrackets: typeof display['escapedSquareBrackets'] === 'boolean'
219
+ ? display['escapedSquareBrackets']
220
+ : base.display.escapedSquareBrackets,
221
+ },
222
+ doubleDollarSignsDisplay: delims['doubleDollarSignsDisplay'] === 'always' ||
223
+ delims['doubleDollarSignsDisplay'] === 'newline' ||
224
+ delims['doubleDollarSignsDisplay'] === 'fenced'
225
+ ? delims['doubleDollarSignsDisplay']
226
+ : base.doubleDollarSignsDisplay,
227
+ };
228
+ }
229
+ /**
230
+ * Extracts markdown directive settings from a user config object.
231
+ */
232
+ function readDirectives(config) {
233
+ const markdown = config['markdown'];
234
+ if (!isObject(markdown))
235
+ return { enabled: false };
236
+ const directives = markdown['directives'];
237
+ if (!isObject(directives))
238
+ return { enabled: false };
239
+ return {
240
+ enabled: typeof directives['enabled'] === 'boolean'
241
+ ? directives['enabled']
242
+ : false,
243
+ };
244
+ }
245
+ /**
246
+ * Extracts the SvelTeX file extensions from a user config object.
247
+ */
248
+ function readExtensions(config, base) {
249
+ const extensions = config['extensions'];
250
+ if (!Array.isArray(extensions))
251
+ return base;
252
+ const strings = extensions.filter((e) => typeof e === 'string');
253
+ return strings.length > 0 ? strings : base;
254
+ }
255
+ /** How long the config-loader child process may run before it is killed. */
256
+ const CONFIG_LOAD_TIMEOUT_MS = 10_000;
257
+ /** Upper bound on the JSON a config-loader child may emit, as a guard. */
258
+ const CONFIG_LOAD_MAX_BYTES = 16 * 1024 * 1024;
259
+ /**
260
+ * Source of the ES-module script run by the config-loader child process (see
261
+ * {@link loadConfigViaChild}).
262
+ *
263
+ * It imports the `svelte.config.*` named in the `SVELTEX_CONFIG_PATH`
264
+ * environment variable and writes a JSON rendering of the module namespace to
265
+ * file descriptor 3. A resolved `Sveltex` instance exposes `configuration` and
266
+ * `mathBackend` as getters — which `JSON.stringify` would drop — so each is
267
+ * copied onto a plain object, both where the instance is a direct export and
268
+ * where it sits inside a Svelte config's `preprocess`. {@link
269
+ * resolveConfigCandidate} then reads that rendering exactly as if it had
270
+ * imported the module itself.
271
+ *
272
+ * The script deliberately contains no backtick or `${...}`, so it survives
273
+ * being embedded verbatim in the template literal below.
274
+ */
275
+ const CONFIG_LOADER_SCRIPT = `
276
+ import { pathToFileURL } from 'node:url';
277
+ import { writeSync } from 'node:fs';
278
+
279
+ const isObject = (value) => typeof value === 'object' && value !== null;
280
+
281
+ const isSveltexInstance = (value) =>
282
+ isObject(value) &&
283
+ 'configuration' in value &&
284
+ 'mathBackend' in value &&
285
+ isObject(value.configuration);
286
+
287
+ const plainify = (value) =>
288
+ isSveltexInstance(value)
289
+ ? { configuration: value.configuration, mathBackend: value.mathBackend }
290
+ : value;
291
+
292
+ const mod = await import(pathToFileURL(process.env.SVELTEX_CONFIG_PATH).href);
293
+
294
+ const rendered = {};
295
+ for (const [key, value] of Object.entries(mod)) {
296
+ if (isSveltexInstance(value)) {
297
+ rendered[key] = plainify(value);
298
+ } else if (isObject(value) && 'preprocess' in value) {
299
+ const list = Array.isArray(value.preprocess)
300
+ ? value.preprocess
301
+ : [value.preprocess];
302
+ rendered[key] = { ...value, preprocess: list.map(plainify) };
303
+ } else {
304
+ rendered[key] = value;
305
+ }
306
+ }
307
+
308
+ writeSync(3, JSON.stringify(rendered));
309
+ `;
310
+ /**
311
+ * Extracts a one-line, human-readable summary from a child process's stderr:
312
+ * the line that names the error (`SyntaxError: …`, `Error [ERR_…]: …`) if
313
+ * there is one, else the first non-empty line. Capped so a stack trace cannot
314
+ * flood the log.
315
+ */
316
+ function summarizeStderr(stderr) {
317
+ const lines = stderr
318
+ .split('\n')
319
+ .map((line) => line.trim())
320
+ .filter((line) => line.length > 0);
321
+ const errorLine = lines.find((line) => /[A-Za-z]*Error\b/u.test(line));
322
+ return (errorLine ?? lines[0] ?? 'unknown error').slice(0, 300);
323
+ }
324
+ /**
325
+ * Imports a `svelte.config.*` in a short-lived child process and returns a
326
+ * JSON-safe rendering of its module namespace.
327
+ *
328
+ * A throwaway process has a throwaway ES-module cache, so every call re-reads
329
+ * the config *and everything it imports* — a separate `sveltex.config.*`,
330
+ * shared helper modules, … — none of which an in-process `import()` could
331
+ * invalidate (a cache-busting query only ever defeats the cache for the entry
332
+ * URL, never for the modules that entry transitively imports). This is what
333
+ * lets a live config reload actually observe edits.
334
+ *
335
+ * The rendering comes back over a private file descriptor 3, never stdout, so
336
+ * any logging the config (or `sveltex()`) performs cannot corrupt it.
337
+ *
338
+ * @param configPath - Absolute path of the `svelte.config.*` to import.
339
+ * @returns The parsed module namespace.
340
+ */
341
+ async function loadConfigViaChild(configPath) {
342
+ return new Promise((resolve, reject) => {
343
+ const child = spawn(process.execPath, ['--input-type=module', '--eval', CONFIG_LOADER_SCRIPT], {
344
+ cwd: dirname(configPath),
345
+ env: {
346
+ ...process.env,
347
+ // When this server itself runs under Electron (the VS Code
348
+ // extension host), the child must be told to behave as
349
+ // plain Node; the variable is harmless for a real Node
350
+ // `execPath`.
351
+ ELECTRON_RUN_AS_NODE: '1',
352
+ SVELTEX_CONFIG_PATH: configPath,
353
+ },
354
+ // stdin/stdout are dropped (config logging to stdout is
355
+ // discarded); stderr is captured so a failed load can report
356
+ // its real reason; the JSON rendering returns on the private
357
+ // fd 3.
358
+ stdio: ['ignore', 'ignore', 'pipe', 'pipe'],
359
+ // A config that hangs on import must not wedge the reloader.
360
+ timeout: CONFIG_LOAD_TIMEOUT_MS,
361
+ });
362
+ let settled = false;
363
+ const fail = (error) => {
364
+ if (settled)
365
+ return;
366
+ settled = true;
367
+ child.kill();
368
+ reject(error);
369
+ };
370
+ const resultPipe = child.stdio[3];
371
+ if (!resultPipe) {
372
+ fail(new Error('config loader: result pipe unavailable'));
373
+ return;
374
+ }
375
+ const chunks = [];
376
+ let size = 0;
377
+ resultPipe.on('data', (chunk) => {
378
+ size += chunk.length;
379
+ if (size > CONFIG_LOAD_MAX_BYTES) {
380
+ fail(new Error('config loader: output too large'));
381
+ return;
382
+ }
383
+ chunks.push(chunk);
384
+ });
385
+ // Collect the child's stderr so a failed import/eval can be reported
386
+ // with its real reason (`SyntaxError`, `Cannot find package`, …)
387
+ // rather than a bare exit code.
388
+ const errChunks = [];
389
+ child.stderr?.on('data', (chunk) => {
390
+ if (errChunks.length < 64)
391
+ errChunks.push(chunk);
392
+ });
393
+ child.on('error', fail);
394
+ child.on('close', (code) => {
395
+ if (settled)
396
+ return;
397
+ if (code !== 0) {
398
+ const stderr = Buffer.concat(errChunks).toString('utf8');
399
+ fail(new Error(stderr.trim()
400
+ ? summarizeStderr(stderr)
401
+ : `exited with code ${String(code)}`));
402
+ return;
403
+ }
404
+ settled = true;
405
+ try {
406
+ const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'));
407
+ resolve(isObject(parsed) ? parsed : {});
408
+ }
409
+ catch (error) {
410
+ reject(error instanceof Error
411
+ ? error
412
+ : new Error('config loader: invalid JSON output'));
413
+ }
414
+ });
415
+ });
416
+ }
417
+ /**
418
+ * Loads the SvelTeX configuration for a workspace and distills it into a
419
+ * {@link SveltexConfigSnapshot}.
420
+ *
421
+ * The configuration is read from the project's `svelte.config.*`: SvelTeX has
422
+ * to be registered there as a preprocessor to be active at all, so the
423
+ * resolved `Sveltex` instance — and thus the project's real verbatim tags,
424
+ * TeX preamble, math backend, … — is reachable from it. A dedicated
425
+ * `sveltex.config.*` file, if the project keeps one, is necessarily imported
426
+ * into `svelte.config.*`, so reading the latter captures it either way.
427
+ *
428
+ * @param workspaceRoot - Absolute path of the workspace folder to search.
429
+ * @param log - Optional sink for a one-line, human-readable account of the
430
+ * load outcome (config located and loaded, located but unloadable — with the
431
+ * reason —, or absent). Wired by the host to the editor's output channel so a
432
+ * misconfigured project is diagnosable rather than a silent fall-back.
433
+ * @returns The resolved snapshot. Loading never throws: a missing,
434
+ * syntactically broken, or otherwise unloadable config falls back to the
435
+ * built-in {@link defaultConfigSnapshot} — the LSP must never fail to start
436
+ * just because the configuration is unreadable.
437
+ *
438
+ * @remarks
439
+ * The config is imported in a short-lived child process ({@link
440
+ * loadConfigViaChild}), so each call — and thus each live reload — re-reads
441
+ * the config and everything it imports. A `.ts` config relies on the child
442
+ * Node's type-stripping support; the child reuses this server's own Node
443
+ * binary, so it strips types exactly where the server's runtime would, and on
444
+ * a Node too old for it the child errors and the defaults are used.
445
+ */
446
+ export async function loadConfigSnapshot(workspaceRoot, log) {
447
+ const base = defaultConfigSnapshot();
448
+ const configPath = findSvelteConfigFile(workspaceRoot);
449
+ if (!configPath) {
450
+ log?.('No svelte.config.* found — using the built-in defaults.');
451
+ return base;
452
+ }
453
+ let mod;
454
+ try {
455
+ mod = await loadConfigViaChild(configPath);
456
+ }
457
+ catch (error) {
458
+ // Loading must never fail the server: a missing, broken, or
459
+ // otherwise unloadable config falls back to the defaults (the
460
+ // located config path is still reported).
461
+ const reason = error instanceof Error ? error.message : String(error);
462
+ log?.(`Failed to load ${configPath}: ${reason}. ` +
463
+ 'Using the built-in defaults.');
464
+ return { ...base, configPath };
465
+ }
466
+ const { candidate, mathBackend, sveltexInstanceFound } = resolveConfigCandidate(mod);
467
+ const snapshot = {
468
+ verbatimTags: readVerbatimTags(candidate) ?? base.verbatimTags,
469
+ latexTags: readLatexTags(candidate) ?? base.latexTags,
470
+ extensions: readExtensions(candidate, base.extensions),
471
+ mathDelims: readMathDelims(candidate, base.mathDelims),
472
+ mathBackend: mathBackend ?? base.mathBackend,
473
+ directives: readDirectives(candidate),
474
+ texScaffolds: readTexScaffolds(candidate),
475
+ configPath,
476
+ };
477
+ if (!sveltexInstanceFound) {
478
+ log?.(`Loaded ${configPath}, but found no SvelTeX preprocessor in ` +
479
+ 'it — SvelTeX settings fall back to the built-in defaults.');
480
+ }
481
+ else {
482
+ const scaffoldTags = Object.keys(snapshot.texScaffolds);
483
+ log?.(`Loaded SvelTeX config from ${configPath} (math backend: ` +
484
+ `${snapshot.mathBackend}; LaTeX tags: ` +
485
+ `${snapshot.latexTags.join(', ') || 'none'}; TeX preamble ` +
486
+ `scaffolds: ${scaffoldTags.join(', ') || 'none'}).`);
487
+ }
488
+ return snapshot;
489
+ }
490
+ /**
491
+ * Narrowing helper: `true` for a resolved `Sveltex` instance — or anything
492
+ * that quacks like one — exposing both a `mathBackend` and a `configuration`
493
+ * object.
494
+ */
495
+ function isSveltexInstance(value) {
496
+ return (isObject(value) &&
497
+ 'mathBackend' in value &&
498
+ 'configuration' in value &&
499
+ isObject(value['configuration']));
500
+ }
501
+ /**
502
+ * Finds a `Sveltex` instance inside a Svelte config's `preprocess` field —
503
+ * the shape of a `svelte.config.*` that configures SvelTeX inline, e.g.
504
+ * `preprocess: [vitePreprocess(), await sveltex(...)]`. `preprocess` may also
505
+ * be a single preprocessor rather than an array.
506
+ *
507
+ * @returns The instance, or `undefined` if `value` carries no SvelTeX
508
+ * preprocessor.
509
+ */
510
+ function findSveltexInPreprocess(value) {
511
+ if (!isObject(value))
512
+ return undefined;
513
+ const preprocess = value['preprocess'];
514
+ const list = Array.isArray(preprocess) ? preprocess : [preprocess];
515
+ for (const entry of list) {
516
+ if (isSveltexInstance(entry))
517
+ return entry;
518
+ }
519
+ return undefined;
520
+ }
521
+ /**
522
+ * Picks, out of an imported `svelte.config.*` module, the object to read
523
+ * SvelTeX settings from and the math backend.
524
+ *
525
+ * The SvelTeX preprocessor — a resolved `Sveltex` instance, exposing
526
+ * `mathBackend` and a fully-merged `configuration` — is found either as a
527
+ * direct module export or, in the usual case, nested in the Svelte config's
528
+ * `preprocess` array (`preprocess: [..., await sveltex(...)]`). Failing that,
529
+ * a plain `default` / `config` object is used as a best effort.
530
+ *
531
+ * @param mod - The imported config module namespace.
532
+ * @returns The object to read region settings from, the math backend if one
533
+ * could be determined, and whether a resolved `Sveltex` preprocessor instance
534
+ * was actually found (as opposed to falling back to a plain config object).
535
+ */
536
+ function resolveConfigCandidate(mod) {
537
+ // (a) A resolved `Sveltex` instance exported directly.
538
+ for (const value of Object.values(mod)) {
539
+ if (isSveltexInstance(value)) {
540
+ return {
541
+ candidate: value.configuration,
542
+ mathBackend: readMathBackend(value),
543
+ sveltexInstanceFound: true,
544
+ };
545
+ }
546
+ }
547
+ // (b) A `Sveltex` instance nested in a Svelte config's `preprocess`.
548
+ for (const value of Object.values(mod)) {
549
+ const instance = findSveltexInPreprocess(value);
550
+ if (instance) {
551
+ return {
552
+ candidate: instance.configuration,
553
+ mathBackend: readMathBackend(instance),
554
+ sveltexInstanceFound: true,
555
+ };
556
+ }
557
+ }
558
+ // (c) A plain config object.
559
+ const candidate = isObject(mod['default'])
560
+ ? mod['default']
561
+ : isObject(mod['config'])
562
+ ? mod['config']
563
+ : mod;
564
+ return {
565
+ candidate,
566
+ mathBackend: readMathBackend(candidate),
567
+ sveltexInstanceFound: false,
568
+ };
569
+ }
@@ -0,0 +1,34 @@
1
+ import type { Diagnostic } from 'vscode-languageserver-protocol';
2
+ import type { SourceMap } from './mapper.js';
3
+ /**
4
+ * Maps a batch of diagnostics from the generated virtual document back to the
5
+ * source `.sveltex` document, dropping any that fall in a non-delegated region.
6
+ *
7
+ * @param diagnostics - Diagnostics as reported by the child Svelte server,
8
+ * with ranges in generated-document coordinates.
9
+ * @param sourceMap - The source map for the document the diagnostics belong to.
10
+ * @returns The subset of diagnostics whose range maps cleanly to source
11
+ * coordinates, with their ranges (and any related-information ranges) rewritten
12
+ * to the source document.
13
+ *
14
+ * @remarks
15
+ * A diagnostic is kept only when _both_ ends of its range map. This drops
16
+ * "unexpected token" style errors the Svelte compiler would otherwise raise
17
+ * over the whitespace that replaced a verbatim/code/math region — those regions
18
+ * are the LSP's own responsibility (stubbed for v1) and must not leak the
19
+ * embedded server's confusion to the user.
20
+ */
21
+ export declare function mapProxiedDiagnostics(diagnostics: Diagnostic[], sourceMap: SourceMap): Diagnostic[];
22
+ /**
23
+ * Merges proxied (already source-mapped) diagnostics with native diagnostics.
24
+ *
25
+ * @param proxied - Source-mapped diagnostics from {@link mapProxiedDiagnostics}.
26
+ * @param native - Diagnostics produced directly by the LSP (currently none;
27
+ * reserved for future LaTeX/math diagnostics).
28
+ * @returns The concatenated list.
29
+ *
30
+ * @remarks
31
+ * Kept as a separate seam so that, when native LaTeX/math diagnostics arrive,
32
+ * the publish path does not change.
33
+ */
34
+ export declare function mergeDiagnostics(proxied: Diagnostic[], native: Diagnostic[]): Diagnostic[];
@@ -0,0 +1,67 @@
1
+ // File description: Diagnostic merging. Combines diagnostics produced by the
2
+ // embedded Svelte language server (which run against the generated virtual
3
+ // `.svelte` document and must be mapped back to source coordinates) with any
4
+ // native diagnostics the LSP itself produces, and discards anything that lands
5
+ // outside a delegated region.
6
+ /**
7
+ * Maps a batch of diagnostics from the generated virtual document back to the
8
+ * source `.sveltex` document, dropping any that fall in a non-delegated region.
9
+ *
10
+ * @param diagnostics - Diagnostics as reported by the child Svelte server,
11
+ * with ranges in generated-document coordinates.
12
+ * @param sourceMap - The source map for the document the diagnostics belong to.
13
+ * @returns The subset of diagnostics whose range maps cleanly to source
14
+ * coordinates, with their ranges (and any related-information ranges) rewritten
15
+ * to the source document.
16
+ *
17
+ * @remarks
18
+ * A diagnostic is kept only when _both_ ends of its range map. This drops
19
+ * "unexpected token" style errors the Svelte compiler would otherwise raise
20
+ * over the whitespace that replaced a verbatim/code/math region — those regions
21
+ * are the LSP's own responsibility (stubbed for v1) and must not leak the
22
+ * embedded server's confusion to the user.
23
+ */
24
+ export function mapProxiedDiagnostics(diagnostics, sourceMap) {
25
+ const mapped = [];
26
+ for (const diagnostic of diagnostics) {
27
+ const range = sourceMap.generatedRangeToSource(diagnostic.range);
28
+ if (!range)
29
+ continue;
30
+ // `relatedInformation` may point into other locations of the same
31
+ // document; map those that can be mapped and drop those that cannot.
32
+ const related = diagnostic.relatedInformation
33
+ ?.map((info) => {
34
+ const infoRange = sourceMap.generatedRangeToSource(info.location.range);
35
+ if (!infoRange)
36
+ return undefined;
37
+ return {
38
+ ...info,
39
+ location: { ...info.location, range: infoRange },
40
+ };
41
+ })
42
+ .filter((info) => Boolean(info));
43
+ mapped.push({
44
+ ...diagnostic,
45
+ range,
46
+ ...(related && related.length > 0
47
+ ? { relatedInformation: related }
48
+ : {}),
49
+ });
50
+ }
51
+ return mapped;
52
+ }
53
+ /**
54
+ * Merges proxied (already source-mapped) diagnostics with native diagnostics.
55
+ *
56
+ * @param proxied - Source-mapped diagnostics from {@link mapProxiedDiagnostics}.
57
+ * @param native - Diagnostics produced directly by the LSP (currently none;
58
+ * reserved for future LaTeX/math diagnostics).
59
+ * @returns The concatenated list.
60
+ *
61
+ * @remarks
62
+ * Kept as a separate seam so that, when native LaTeX/math diagnostics arrive,
63
+ * the publish path does not change.
64
+ */
65
+ export function mergeDiagnostics(proxied, native) {
66
+ return [...proxied, ...native];
67
+ }