@nvl/sveltex-math-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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 N. V. Lang
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 ADDED
@@ -0,0 +1,111 @@
1
+ # `@nvl/sveltex-math-language-server`
2
+
3
+ > [!WARNING]
4
+ > **This package is in alpha.** It is brand new and under active development.
5
+ > Its API, behaviour, and configuration may change at any time, and breaking
6
+ > changes should be expected before version `1.0.0`.
7
+
8
+ A small, standalone Language Server Protocol (LSP) implementation that provides
9
+ editor assistance for **TeX math** — the math written inside the `$…$` /
10
+ `$$…$$` / `\(…\)` / `\[…\]` regions of a [SvelTeX](https://sveltex.dev)
11
+ document.
12
+
13
+ It is spawned by [`@nvl/sveltex-language-server`](../sveltex-language-server),
14
+ which feeds it each math region of a `.sveltex` file as its own tiny virtual
15
+ TeX document, but it is an ordinary LSP server and can be launched directly by
16
+ any editor over stdio.
17
+
18
+ ## Features
19
+
20
+ - **Command completion** — triggered on `\`. As you type `\fra…` the server
21
+ offers `\frac`, `\frak`, … Inside `\begin{…}` / `\end{…}` it offers
22
+ environment names instead.
23
+ - **Hover** — hovering a command shows its signature when it takes arguments
24
+ (`\sqrt[index]{radicand}`), the Unicode glyph it stands for when it is a
25
+ symbol (`∮ (contour integral)`), a one-line description, and the package and
26
+ backend that provide it.
27
+
28
+ ## Two backends, two command sets
29
+
30
+ The server runs in one of two modes, chosen by the LSP `initialize` request's
31
+ `initializationOptions`:
32
+
33
+ ```jsonc
34
+ { "backend": "mathjax" } // or "katex"
35
+ ```
36
+
37
+ This matters because **KaTeX and MathJax support different sets of commands**.
38
+ A command list that mixed them would suggest commands that silently fail to
39
+ render. So the server ships one accurate list per backend and offers only the
40
+ commands the selected backend actually understands.
41
+
42
+ If `initializationOptions.backend` is absent or unrecognised the server falls
43
+ back to `mathjax` (SvelTeX's default math backend).
44
+
45
+ ## How the command lists are sourced
46
+
47
+ The lists are **not** transcribed from prose documentation — that drifts. They
48
+ are extracted directly from each backend's own package source by
49
+ [`scripts/generate-commands.ts`](./scripts/generate-commands.ts), which writes
50
+ [`src/data/commands.generated.ts`](./src/data/commands.generated.ts):
51
+
52
+ - **KaTeX** declares its commands in four side-effect-populated tables —
53
+ `functions`, `symbols`, `macros` and `environments` (the default exports of
54
+ the matching files under `katex/src/`). KaTeX supports a finite, enumerable
55
+ set; those four tables _are_ that set.
56
+ - **MathJax** registers its TeX macros/symbols/environments through a global
57
+ `MapHandler`. The generator patches `MapHandler.register`, imports the
58
+ packages the default `input/tex` configuration loads — plus the ones its
59
+ `autoload` extension pulls in on demand (`mhchem`, `cancel`, `braket`, …) —
60
+ and reads the registered token keys back out. Packages that need an explicit
61
+ `\require{}` (`physics`, `mathtools`, …) are intentionally excluded: they are
62
+ not available out of the box, so offering them would be a false promise.
63
+
64
+ On top of the bare command set, the generator enriches each command with
65
+ documentation metadata — a usage signature, the providing package and a
66
+ one-line description — curated from each engine's reference docs and kept in
67
+ `scripts/data/{katex,mathjax}-docs.json`. Symbol commands additionally carry
68
+ the Unicode glyph they render (read from the engines' own symbol tables) and
69
+ that glyph's Unicode standard name (looked up in the Unicode Character
70
+ Database). All of it is baked into the generated file, so hover stays a pure,
71
+ offline lookup.
72
+
73
+ `katex` and `@mathjax/src` are **devDependencies only** — the published package
74
+ ships the generated static data and has no runtime dependency on either. Run
75
+
76
+ ```sh
77
+ pnpm --filter @nvl/sveltex-math-language-server generate
78
+ ```
79
+
80
+ to regenerate the lists after a `katex` / `@mathjax/src` version bump.
81
+
82
+ ## Architecture
83
+
84
+ ```
85
+ src/data/commands.generated.ts ← generated, per-backend command tables
86
+ src/core/commands.ts ← CommandTable: indexed prefix/exact lookup
87
+ src/core/context.ts ← TeX caret-context lexing (\cmd, \begin{…})
88
+ src/core/describe.ts ← human-readable command descriptions
89
+ src/core/features.ts ← pure completion + hover (LSP payloads)
90
+ src/core/server.ts ← createServer(connection): the orchestrator
91
+ src/index.ts ← startServer() (stdio convenience wrapper)
92
+ bin/server.js ← #!/usr/bin/env node → startServer()
93
+ ```
94
+
95
+ `createServer(connection)` is **transport-agnostic** — it never imports
96
+ `vscode` and never touches `process.stdin`. The same core therefore backs the
97
+ stdio `bin/server.js` and any in-process host, exactly like
98
+ `@nvl/sveltex-language-server`.
99
+
100
+ ## Development
101
+
102
+ ```sh
103
+ pnpm --filter @nvl/sveltex-math-language-server generate # refresh command data
104
+ pnpm --filter @nvl/sveltex-math-language-server build # type-check + emit dist/
105
+ pnpm --filter @nvl/sveltex-math-language-server test # run unit/integration tests
106
+ pnpm --filter @nvl/sveltex-math-language-server lint # eslint + tsc --noEmit
107
+ ```
108
+
109
+ The test suite covers the per-backend command tables, the caret-context lexer,
110
+ completion filtering and hover, and an end-to-end check that spawns
111
+ `bin/server.js` and drives it over stdio.
package/bin/server.js ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ // Executable entry point for the SvelTeX math language server.
3
+ //
4
+ // This file is intentionally tiny and dependency-free: editors and host
5
+ // servers (notably `@nvl/sveltex-language-server`, which spawns one of these
6
+ // per math backend) launch it directly. All real work lives in the compiled
7
+ // core under `../dist`.
8
+
9
+ import { startServer } from '../dist/index.js';
10
+
11
+ startServer();
@@ -0,0 +1,104 @@
1
+ /**
2
+ * The category a TeX command falls into.
3
+ *
4
+ * - `function`: a command that consumes arguments (`\frac`, `\sqrt`, `\text`).
5
+ * - `symbol`: a no-argument command standing for a glyph (`\alpha`, `\sum`).
6
+ * - `macro`: a command defined in terms of others (`\TeX`, `\operatorname`).
7
+ * - `environment`: a name used with `\begin{...}` / `\end{...}` (`align`,
8
+ * `matrix`). Completing `\begin{` offers these.
9
+ */
10
+ export type CommandCategory = 'function' | 'symbol' | 'macro' | 'environment';
11
+ /**
12
+ * One TeX command supported by a backend.
13
+ *
14
+ * @remarks
15
+ * `name` never includes the leading backslash — it is the bare control word
16
+ * (e.g. `frac`). The backslash is added by the completion/hover layer so the
17
+ * data is uniform regardless of how the user typed the trigger.
18
+ *
19
+ * Only `name` and `category` are always present: they come from the backend's
20
+ * own package source and so are exhaustive and exact. The remaining fields are
21
+ * documentation metadata, merged in from each engine's reference docs by
22
+ * `scripts/generate-commands.ts`; any of them may be absent for a given
23
+ * command, and the hover/completion layer degrades gracefully when they are.
24
+ */
25
+ export interface MathCommand {
26
+ /** The command name without its leading backslash (e.g. `frac`). */
27
+ name: string;
28
+ /** Which {@link CommandCategory} the command belongs to. */
29
+ category: CommandCategory;
30
+ /**
31
+ * A usage signature spelling out the command's arguments, e.g.
32
+ * `\sqrt[degree]{radicand}`. Present only for commands that take
33
+ * arguments — a no-argument command (`\alpha`, `\sin`) has none.
34
+ */
35
+ signature?: string;
36
+ /**
37
+ * The single Unicode character the command stands for, e.g. `α` for
38
+ * `\alpha` or `∮` for `\oint`. Present only for commands that denote one
39
+ * Unicode glyph.
40
+ */
41
+ unicode?: string;
42
+ /**
43
+ * The Unicode standard name of {@link MathCommand.unicode}, lower-cased
44
+ * (e.g. `greek small letter alpha`, `contour integral`). Present only
45
+ * alongside `unicode`, and only when the name could be resolved.
46
+ */
47
+ unicodeName?: string;
48
+ /**
49
+ * The backend package/extension the command belongs to — a MathJax
50
+ * extension name (`base`, `ams`, `physics`, …) or, for KaTeX, the section
51
+ * of its support table the command is documented under.
52
+ */
53
+ package?: string;
54
+ /** A one-line, human-readable description of what the command does. */
55
+ description?: string;
56
+ }
57
+ /** The math backends this server can emulate. */
58
+ export type MathLspBackend = 'katex' | 'mathjax';
59
+ /**
60
+ * An indexed, queryable view of one backend's command set.
61
+ *
62
+ * Built once per backend by {@link createCommandTable} and cached, so repeated
63
+ * completion requests do no repeated work.
64
+ */
65
+ export declare class CommandTable {
66
+ #private;
67
+ private constructor();
68
+ /**
69
+ * Builds a {@link CommandTable} from a raw command list.
70
+ *
71
+ * @param commands - The backend's commands (typically one of the generated
72
+ * arrays). Order does not matter; the table sorts internally.
73
+ */
74
+ static create(commands: readonly MathCommand[]): CommandTable;
75
+ /** The total number of commands in the table. */
76
+ get size(): number;
77
+ /** Every command in the table, sorted by name. */
78
+ get all(): readonly MathCommand[];
79
+ /**
80
+ * Looks a command up by its exact (backslash-free) name.
81
+ *
82
+ * @param name - The bare command name, e.g. `frac`.
83
+ * @returns The command, or `undefined` if the backend does not support it.
84
+ */
85
+ get(name: string): MathCommand | undefined;
86
+ /**
87
+ * Returns the commands whose name starts with `prefix`.
88
+ *
89
+ * @param prefix - A bare (backslash-free) prefix. An empty prefix matches
90
+ * every command.
91
+ * @param environmentsOnly - When `true`, only `environment` commands are
92
+ * considered (for `\begin{...}` completion); when `false`, environments are
93
+ * excluded (for ordinary `\command` completion).
94
+ * @returns The matching commands, sorted by name.
95
+ */
96
+ withPrefix(prefix: string, environmentsOnly: boolean): readonly MathCommand[];
97
+ }
98
+ /**
99
+ * Returns the {@link CommandTable} for a backend, building it on first use.
100
+ *
101
+ * @param backend - `'katex'` or `'mathjax'`.
102
+ * @returns The backend's command table.
103
+ */
104
+ export declare function createCommandTable(backend: MathLspBackend): CommandTable;
@@ -0,0 +1,94 @@
1
+ // File description: The TeX-command model plus the per-backend lookup API.
2
+ //
3
+ // `@nvl/sveltex-math-language-server` answers completion and hover requests for
4
+ // TeX math written in SvelTeX `$...$` / `$$...$$` regions. The set of commands
5
+ // it offers must be EXACTLY what the selected backend understands — KaTeX and
6
+ // MathJax support overlapping but distinct sets. The raw lists are extracted
7
+ // from each backend's own package source by `scripts/generate-commands.ts`
8
+ // (see `../data/commands.generated.ts`); this module turns those flat lists
9
+ // into an indexed `CommandTable` for fast prefix and exact lookup.
10
+ import { KATEX_COMMANDS, MATHJAX_COMMANDS, } from '../data/commands.generated.js';
11
+ /**
12
+ * An indexed, queryable view of one backend's command set.
13
+ *
14
+ * Built once per backend by {@link createCommandTable} and cached, so repeated
15
+ * completion requests do no repeated work.
16
+ */
17
+ export class CommandTable {
18
+ /** All commands, sorted by name — the basis for completion lists. */
19
+ #all;
20
+ /** Exact-name index, for hover. */
21
+ #byName;
22
+ /** Only the `environment` commands, for `\begin{...}` completion. */
23
+ #environments;
24
+ /** Every command that is NOT an environment, for `\command` completion. */
25
+ #nonEnvironments;
26
+ constructor(commands) {
27
+ const sorted = [...commands].sort((a, b) => a.name.localeCompare(b.name));
28
+ this.#all = sorted;
29
+ this.#byName = new Map(sorted.map((c) => [c.name, c]));
30
+ this.#environments = sorted.filter((c) => c.category === 'environment');
31
+ this.#nonEnvironments = sorted.filter((c) => c.category !== 'environment');
32
+ }
33
+ /**
34
+ * Builds a {@link CommandTable} from a raw command list.
35
+ *
36
+ * @param commands - The backend's commands (typically one of the generated
37
+ * arrays). Order does not matter; the table sorts internally.
38
+ */
39
+ static create(commands) {
40
+ return new CommandTable(commands);
41
+ }
42
+ /** The total number of commands in the table. */
43
+ get size() {
44
+ return this.#all.length;
45
+ }
46
+ /** Every command in the table, sorted by name. */
47
+ get all() {
48
+ return this.#all;
49
+ }
50
+ /**
51
+ * Looks a command up by its exact (backslash-free) name.
52
+ *
53
+ * @param name - The bare command name, e.g. `frac`.
54
+ * @returns The command, or `undefined` if the backend does not support it.
55
+ */
56
+ get(name) {
57
+ return this.#byName.get(name);
58
+ }
59
+ /**
60
+ * Returns the commands whose name starts with `prefix`.
61
+ *
62
+ * @param prefix - A bare (backslash-free) prefix. An empty prefix matches
63
+ * every command.
64
+ * @param environmentsOnly - When `true`, only `environment` commands are
65
+ * considered (for `\begin{...}` completion); when `false`, environments are
66
+ * excluded (for ordinary `\command` completion).
67
+ * @returns The matching commands, sorted by name.
68
+ */
69
+ withPrefix(prefix, environmentsOnly) {
70
+ const pool = environmentsOnly
71
+ ? this.#environments
72
+ : this.#nonEnvironments;
73
+ if (prefix.length === 0)
74
+ return pool;
75
+ // Case-sensitive: TeX commands are case-sensitive (`\Pi` ≠ `\pi`).
76
+ return pool.filter((c) => c.name.startsWith(prefix));
77
+ }
78
+ }
79
+ /** Lazily-built, cached {@link CommandTable}s keyed by backend. */
80
+ const tableCache = new Map();
81
+ /**
82
+ * Returns the {@link CommandTable} for a backend, building it on first use.
83
+ *
84
+ * @param backend - `'katex'` or `'mathjax'`.
85
+ * @returns The backend's command table.
86
+ */
87
+ export function createCommandTable(backend) {
88
+ const cached = tableCache.get(backend);
89
+ if (cached)
90
+ return cached;
91
+ const table = CommandTable.create(backend === 'katex' ? KATEX_COMMANDS : MATHJAX_COMMANDS);
92
+ tableCache.set(backend, table);
93
+ return table;
94
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The command-typing context at a caret, as needed for completion.
3
+ */
4
+ export interface CompletionContext {
5
+ /**
6
+ * The command prefix already typed after the backslash, WITHOUT the
7
+ * backslash itself (e.g. for `\fra|` the prefix is `fra`). Empty when the
8
+ * caret sits immediately after a lone backslash.
9
+ */
10
+ prefix: string;
11
+ /**
12
+ * Offset of the backslash that opens the command being typed. The
13
+ * completion's replace range runs from here to the caret.
14
+ */
15
+ backslashOffset: number;
16
+ /**
17
+ * `true` when the command is the environment-name argument of a `\begin{`
18
+ * or `\end{` — i.e. the caret is inside the braces of `\begin{...}`. In
19
+ * that case there is no backslash; completion offers environment names.
20
+ */
21
+ isEnvironmentName: boolean;
22
+ }
23
+ /**
24
+ * The command found under (or immediately after) a caret, as needed for hover.
25
+ */
26
+ export interface CommandAtCaret {
27
+ /** The command name without its leading backslash (e.g. `frac`). */
28
+ name: string;
29
+ /** Offset of the opening backslash. */
30
+ start: number;
31
+ /** Offset one past the last character of the command. */
32
+ end: number;
33
+ }
34
+ /**
35
+ * Analyses the command-typing context at `offset` within `text`.
36
+ *
37
+ * Two shapes are recognised:
38
+ *
39
+ * 1. **Ordinary command** — the caret follows `\` and zero or more letters
40
+ * (`\`, `\al`, `\alpha`). The nearest preceding backslash that is not
41
+ * itself escaped opens the command.
42
+ * 2. **Environment name** — the caret is inside the braces of a `\begin{...}`
43
+ * or `\end{...}`; the partial environment name is returned with
44
+ * `isEnvironmentName: true`.
45
+ *
46
+ * @param text - The full TeX (math) document text.
47
+ * @param offset - The caret offset.
48
+ * @returns The {@link CompletionContext}, or `undefined` if the caret is not in
49
+ * a position where command completion makes sense.
50
+ */
51
+ export declare function completionContextAt(text: string, offset: number): CompletionContext | undefined;
52
+ /**
53
+ * Finds the TeX command that the caret at `offset` sits within or directly
54
+ * after — used to answer hover.
55
+ *
56
+ * A command is a backslash followed by either a run of letters (`\alpha`) or a
57
+ * single non-letter character (`\,`, `\#`). The caret matches if it lies
58
+ * anywhere from the backslash up to and including the position just past the
59
+ * command's last character.
60
+ *
61
+ * @param text - The full TeX (math) document text.
62
+ * @param offset - The caret offset.
63
+ * @returns The {@link CommandAtCaret}, or `undefined` if the caret is not on a
64
+ * command.
65
+ */
66
+ export declare function commandAtCaret(text: string, offset: number): CommandAtCaret | undefined;
@@ -0,0 +1,152 @@
1
+ // File description: TeX-aware caret-context analysis.
2
+ //
3
+ // Completion and hover both need to know what TeX construct the caret sits in:
4
+ // the command being typed (for completion) or the command under the cursor
5
+ // (for hover), and whether that command is the environment name slot of a
6
+ // `\begin{...}` / `\end{...}`. This module isolates that small bit of TeX
7
+ // lexing so the completion and hover handlers stay declarative.
8
+ /** Whether `ch` may appear in a multi-letter TeX control word. */
9
+ function isControlWordChar(ch) {
10
+ return /^[a-zA-Z]$/u.test(ch);
11
+ }
12
+ /**
13
+ * Analyses the command-typing context at `offset` within `text`.
14
+ *
15
+ * Two shapes are recognised:
16
+ *
17
+ * 1. **Ordinary command** — the caret follows `\` and zero or more letters
18
+ * (`\`, `\al`, `\alpha`). The nearest preceding backslash that is not
19
+ * itself escaped opens the command.
20
+ * 2. **Environment name** — the caret is inside the braces of a `\begin{...}`
21
+ * or `\end{...}`; the partial environment name is returned with
22
+ * `isEnvironmentName: true`.
23
+ *
24
+ * @param text - The full TeX (math) document text.
25
+ * @param offset - The caret offset.
26
+ * @returns The {@link CompletionContext}, or `undefined` if the caret is not in
27
+ * a position where command completion makes sense.
28
+ */
29
+ export function completionContextAt(text, offset) {
30
+ if (offset < 0 || offset > text.length)
31
+ return undefined;
32
+ // --- environment-name slot: `\begin{<here>` or `\end{<here>` -----------
33
+ const beforeCaret = text.slice(0, offset);
34
+ const envMatch = /\\(?:begin|end)\{([a-zA-Z*]*)$/u.exec(beforeCaret);
35
+ if (envMatch) {
36
+ return {
37
+ prefix: envMatch[1] ?? '',
38
+ backslashOffset: offset - (envMatch[1] ?? '').length,
39
+ isEnvironmentName: true,
40
+ };
41
+ }
42
+ // --- ordinary command: scan back over letters to a backslash -----------
43
+ let i = offset;
44
+ while (i > 0 && isControlWordChar(text.charAt(i - 1))) {
45
+ i -= 1;
46
+ }
47
+ // `i` now points just after the run of letters; the char before must be a
48
+ // backslash for this to be a command.
49
+ if (i === 0 || text.charAt(i - 1) !== '\\')
50
+ return undefined;
51
+ const backslashOffset = i - 1;
52
+ // A backslash is only a command opener if it is not itself escaped: count
53
+ // the unbroken run of backslashes ending here; an even count means the
54
+ // last one is escaped (`\\`), so it is line-break/literal, not a command.
55
+ let backslashes = 0;
56
+ let j = backslashOffset;
57
+ while (j >= 0 && text.charAt(j) === '\\') {
58
+ backslashes += 1;
59
+ j -= 1;
60
+ }
61
+ if (backslashes % 2 === 0)
62
+ return undefined;
63
+ return {
64
+ prefix: text.slice(i, offset),
65
+ backslashOffset,
66
+ isEnvironmentName: false,
67
+ };
68
+ }
69
+ /**
70
+ * Finds the TeX command that the caret at `offset` sits within or directly
71
+ * after — used to answer hover.
72
+ *
73
+ * A command is a backslash followed by either a run of letters (`\alpha`) or a
74
+ * single non-letter character (`\,`, `\#`). The caret matches if it lies
75
+ * anywhere from the backslash up to and including the position just past the
76
+ * command's last character.
77
+ *
78
+ * @param text - The full TeX (math) document text.
79
+ * @param offset - The caret offset.
80
+ * @returns The {@link CommandAtCaret}, or `undefined` if the caret is not on a
81
+ * command.
82
+ */
83
+ export function commandAtCaret(text, offset) {
84
+ if (offset < 0 || offset > text.length)
85
+ return undefined;
86
+ // Find the backslash at or before the caret that could open the command.
87
+ // Scan left over letters first (the caret may be in the middle of a word),
88
+ // then expect a backslash.
89
+ let left = offset;
90
+ while (left > 0 && isControlWordChar(text.charAt(left - 1))) {
91
+ left -= 1;
92
+ }
93
+ let backslashOffset;
94
+ if (left > 0 && text.charAt(left - 1) === '\\') {
95
+ backslashOffset = left - 1;
96
+ }
97
+ else if (
98
+ // Single-character command: caret right after `\` + one non-letter,
99
+ // e.g. hovering `\,`.
100
+ offset >= 2 &&
101
+ text.charAt(offset - 2) === '\\' &&
102
+ !isControlWordChar(text.charAt(offset - 1))) {
103
+ backslashOffset = offset - 2;
104
+ }
105
+ else if (
106
+ // Caret sits on the backslash itself or just before a single-char cmd.
107
+ offset >= 1 &&
108
+ text.charAt(offset - 1) === '\\') {
109
+ backslashOffset = offset - 1;
110
+ }
111
+ else if (offset < text.length && text.charAt(offset) === '\\') {
112
+ backslashOffset = offset;
113
+ }
114
+ if (backslashOffset === undefined)
115
+ return undefined;
116
+ // Reject an escaped backslash (`\\`).
117
+ let backslashes = 0;
118
+ let j = backslashOffset;
119
+ while (j >= 0 && text.charAt(j) === '\\') {
120
+ backslashes += 1;
121
+ j -= 1;
122
+ }
123
+ if (backslashes % 2 === 0)
124
+ return undefined;
125
+ // Read the command name after the backslash.
126
+ let end = backslashOffset + 1;
127
+ if (end < text.length && isControlWordChar(text.charAt(end))) {
128
+ while (end < text.length && isControlWordChar(text.charAt(end))) {
129
+ end += 1;
130
+ }
131
+ // An optional trailing `*` is part of starred command/environment names
132
+ // (it is not, however, part of a control word the caret is hovering —
133
+ // keep it simple and exclude it here; starred names are matched by the
134
+ // environment-name path of completion instead).
135
+ }
136
+ else if (end < text.length &&
137
+ !isControlWordChar(text.charAt(end)) &&
138
+ !/\s/u.test(text.charAt(end)) &&
139
+ // A backslash here would make the pair `\\` — an escaped backslash,
140
+ // not a `\<char>` command — so it is not a single-character command.
141
+ text.charAt(end) !== '\\') {
142
+ // Single-character command (`\,`).
143
+ end += 1;
144
+ }
145
+ const name = text.slice(backslashOffset + 1, end);
146
+ if (name.length === 0)
147
+ return undefined;
148
+ // The caret must actually be within `[backslashOffset, end]`.
149
+ if (offset < backslashOffset || offset > end)
150
+ return undefined;
151
+ return { name, start: backslashOffset, end };
152
+ }
@@ -0,0 +1,24 @@
1
+ import type { MathCommand, MathLspBackend } from './commands.js';
2
+ /**
3
+ * Returns the one-line description of a command: the documentation-sourced
4
+ * description when the generator merged one in, else a curated gloss, else the
5
+ * generic category sentence (which is always available).
6
+ *
7
+ * @param command - The command to describe.
8
+ */
9
+ export declare function describeCommand(command: MathCommand): string;
10
+ /**
11
+ * Builds the Markdown body shown on hover for a command.
12
+ *
13
+ * The body is, in order: a fenced `latex` block with the command's signature
14
+ * (its arguments spelled out when it takes any); for a command that stands for
15
+ * a Unicode glyph, that glyph and its Unicode standard name; the one-line
16
+ * description; and a dimmed footer naming the backend, the providing package
17
+ * (when known) and the category.
18
+ *
19
+ * @param command - The command being hovered.
20
+ * @param backend - The active backend, named in the footer so the user knows
21
+ * which engine's support they are seeing.
22
+ * @returns A Markdown string.
23
+ */
24
+ export declare function hoverMarkdown(command: MathCommand, backend: MathLspBackend): string;