@markuplint/language-server 0.0.0-stage → 5.1.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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -2
  3. package/bin/markuplint-language-server.mjs +3 -0
  4. package/lib/const.d.ts +41 -0
  5. package/lib/const.js +44 -0
  6. package/lib/i18n.d.ts +19 -0
  7. package/lib/i18n.js +63 -0
  8. package/lib/index.d.ts +41 -0
  9. package/lib/index.js +40 -0
  10. package/lib/lsp.d.ts +8 -0
  11. package/lib/lsp.js +8 -0
  12. package/lib/protocol.d.ts +10 -0
  13. package/lib/protocol.js +9 -0
  14. package/lib/server/client-channel.d.ts +38 -0
  15. package/lib/server/client-channel.js +89 -0
  16. package/lib/server/code-actions.d.ts +36 -0
  17. package/lib/server/code-actions.js +101 -0
  18. package/lib/server/convert-diagnostics.d.ts +28 -0
  19. package/lib/server/convert-diagnostics.js +82 -0
  20. package/lib/server/debug.d.ts +3 -0
  21. package/lib/server/debug.js +7 -0
  22. package/lib/server/document-events.d.ts +78 -0
  23. package/lib/server/document-events.js +299 -0
  24. package/lib/server/get-accessibility-by-location.d.ts +6 -0
  25. package/lib/server/get-accessibility-by-location.js +19 -0
  26. package/lib/server/get-module.d.ts +127 -0
  27. package/lib/server/get-module.js +257 -0
  28. package/lib/server/index.d.ts +1 -0
  29. package/lib/server/index.js +5 -0
  30. package/lib/server/initialize-params.d.ts +32 -0
  31. package/lib/server/initialize-params.js +28 -0
  32. package/lib/server/module-notices.d.ts +28 -0
  33. package/lib/server/module-notices.js +41 -0
  34. package/lib/server/parser-load-failure.d.ts +36 -0
  35. package/lib/server/parser-load-failure.js +85 -0
  36. package/lib/server/server.d.ts +20 -0
  37. package/lib/server/server.js +96 -0
  38. package/lib/server/suppression-support.d.ts +57 -0
  39. package/lib/server/suppression-support.js +233 -0
  40. package/lib/server/v2.d.ts +12 -0
  41. package/lib/server/v2.js +109 -0
  42. package/lib/server/v3.d.ts +19 -0
  43. package/lib/server/v3.js +208 -0
  44. package/lib/server/v4.d.ts +85 -0
  45. package/lib/server/v4.js +292 -0
  46. package/lib/server/v5.d.ts +89 -0
  47. package/lib/server/v5.js +339 -0
  48. package/lib/types.d.ts +88 -0
  49. package/lib/types.js +1 -0
  50. package/lib/utils/get-file-path.d.ts +15 -0
  51. package/lib/utils/get-file-path.js +45 -0
  52. package/lib/utils/resolve-working-directory.d.ts +44 -0
  53. package/lib/utils/resolve-working-directory.js +266 -0
  54. package/locales/en.json +7 -0
  55. package/locales/ja.json +21 -0
  56. package/package.json +59 -5
@@ -0,0 +1,101 @@
1
+ import { CodeActionKind } from 'vscode-languageserver';
2
+ /**
3
+ * Code Action kind for "Fix all markuplint issues".
4
+ * Follows the `source.fixAll.<provider>` convention used by ESLint and others.
5
+ */
6
+ export const SOURCE_FIX_ALL_MARKUPLINT = `${CodeActionKind.SourceFixAll}.markuplint`;
7
+ /**
8
+ * Converts a 0-based character offset to an LSP Position using the source code.
9
+ *
10
+ * @param sourceCode - The full raw source text
11
+ * @param offset - A zero-based character offset into the source
12
+ * @returns A 0-based LSP Position (line and character)
13
+ */
14
+ export function offsetToPosition(sourceCode, offset) {
15
+ const lines = sourceCode.slice(0, offset).split('\n');
16
+ return { line: lines.length - 1, character: (lines.at(-1) ?? '').length };
17
+ }
18
+ /**
19
+ * Converts a markuplint TextEdit (offset-based) to an LSP Range.
20
+ */
21
+ function toRange(sourceCode, edit) {
22
+ return {
23
+ start: offsetToPosition(sourceCode, edit.range[0]),
24
+ end: offsetToPosition(sourceCode, edit.range[1]),
25
+ };
26
+ }
27
+ /**
28
+ * Creates per-violation QuickFix Code Actions.
29
+ *
30
+ * @param uri - The document URI to associate edits with
31
+ * @param diagnostics - The diagnostics from the Code Action request context
32
+ * @param fixState - The latest lint result containing violations and fix data
33
+ * @returns An array of QuickFix Code Actions, one per fixable violation
34
+ */
35
+ export function createQuickFixActions(uri,
36
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
37
+ diagnostics, fixState) {
38
+ const actions = [];
39
+ for (const diagnostic of diagnostics) {
40
+ const index = diagnostic.data?.violationIndex;
41
+ if (index == null)
42
+ continue;
43
+ const violation = fixState.violations[index];
44
+ if (!violation?.fix)
45
+ continue;
46
+ const edits = violation.fix.edits.map(edit => ({
47
+ range: toRange(fixState.sourceCode, edit),
48
+ newText: edit.text,
49
+ }));
50
+ actions.push({
51
+ title: `Fix: ${diagnostic.message}`,
52
+ kind: CodeActionKind.QuickFix,
53
+ diagnostics: [diagnostic],
54
+ isPreferred: true,
55
+ edit: { changes: { [uri]: edits } },
56
+ });
57
+ }
58
+ return actions;
59
+ }
60
+ /**
61
+ * Creates a SourceFixAll Code Action (`source.fixAll.markuplint`).
62
+ *
63
+ * Uses firstPassEdits from FixSummary for cursor-safe individual edits.
64
+ * Falls back to full-document replacement when firstPassEdits is unavailable.
65
+ *
66
+ * @param uri - The document URI to associate edits with
67
+ * @param diagnostics - The diagnostics to attach to the Code Action
68
+ * @param fixState - The latest lint result containing violations and fix data
69
+ * @returns A SourceFixAll Code Action, or `null` if nothing to fix
70
+ */
71
+ export function createFixAllAction(uri,
72
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
73
+ diagnostics, fixState) {
74
+ if (fixState.fixedCode === fixState.sourceCode)
75
+ return null;
76
+ let textEdits;
77
+ if (fixState.fixSummary?.firstPassEdits && fixState.fixSummary.firstPassEdits.length > 0) {
78
+ textEdits = fixState.fixSummary.firstPassEdits.map(edit => ({
79
+ range: toRange(fixState.sourceCode, edit),
80
+ newText: edit.text,
81
+ }));
82
+ }
83
+ else {
84
+ const lines = fixState.sourceCode.split('\n');
85
+ textEdits = [
86
+ {
87
+ range: {
88
+ start: { line: 0, character: 0 },
89
+ end: { line: lines.length - 1, character: lines.at(-1).length },
90
+ },
91
+ newText: fixState.fixedCode,
92
+ },
93
+ ];
94
+ }
95
+ return {
96
+ title: 'Fix all markuplint issues',
97
+ kind: SOURCE_FIX_ALL_MARKUPLINT,
98
+ diagnostics: [...diagnostics],
99
+ edit: { changes: { [uri]: textEdits } },
100
+ };
101
+ }
@@ -0,0 +1,28 @@
1
+ import type { MLResultInfo } from 'markuplint';
2
+ import type { SeverityMap } from '../types.js';
3
+ import type { Diagnostic } from 'vscode-languageserver/node.js';
4
+ /**
5
+ * Re-reports diagnostics with the severities a client asked for.
6
+ *
7
+ * Runs on the LSP severity rather than on the markuplint one, so one place covers the v2–v5
8
+ * handlers. `convertDiagnostics` maps the two one to one, which makes the keys of the map
9
+ * mean the same on both sides.
10
+ *
11
+ * @param diagnostics - The diagnostics to re-report
12
+ * @param map - The client's `severityMap`; severities it does not list are kept
13
+ * @returns New diagnostics. The input is not modified.
14
+ */
15
+ export declare function applySeverityMap<T extends Diagnostic>(diagnostics: readonly T[], map: SeverityMap | undefined): T[];
16
+ /**
17
+ * Converts markuplint violations into LSP diagnostics.
18
+ *
19
+ * Each diagnostic includes a `data.violationIndex` property that maps back
20
+ * to the original violation for use in Code Action resolution.
21
+ *
22
+ * @param result - The markuplint lint result, or null if no result is available
23
+ * @returns An array of LSP diagnostics augmented with line and col numbers
24
+ */
25
+ export declare function convertDiagnostics(result: MLResultInfo | null): (Diagnostic & {
26
+ line: number;
27
+ col: number;
28
+ })[];
@@ -0,0 +1,82 @@
1
+ import { DiagnosticSeverity } from 'vscode-languageserver/node.js';
2
+ import { NAME, WEBSITE_URL_RULE_PAGE } from '../const.js';
3
+ const REPORTED = {
4
+ error: DiagnosticSeverity.Error,
5
+ warning: DiagnosticSeverity.Warning,
6
+ info: DiagnosticSeverity.Information,
7
+ hint: DiagnosticSeverity.Hint,
8
+ };
9
+ /**
10
+ * Re-reports diagnostics with the severities a client asked for.
11
+ *
12
+ * Runs on the LSP severity rather than on the markuplint one, so one place covers the v2–v5
13
+ * handlers. `convertDiagnostics` maps the two one to one, which makes the keys of the map
14
+ * mean the same on both sides.
15
+ *
16
+ * @param diagnostics - The diagnostics to re-report
17
+ * @param map - The client's `severityMap`; severities it does not list are kept
18
+ * @returns New diagnostics. The input is not modified.
19
+ */
20
+ export function applySeverityMap(diagnostics, map) {
21
+ if (!map) {
22
+ return [...diagnostics];
23
+ }
24
+ return diagnostics.map(diagnostic => {
25
+ const from = diagnostic.severity === DiagnosticSeverity.Error
26
+ ? 'error'
27
+ : diagnostic.severity === DiagnosticSeverity.Warning
28
+ ? 'warning'
29
+ : diagnostic.severity === DiagnosticSeverity.Information
30
+ ? 'info'
31
+ : undefined;
32
+ const to = from && map[from];
33
+ return to ? { ...diagnostic, severity: REPORTED[to] } : diagnostic;
34
+ });
35
+ }
36
+ /**
37
+ * Converts markuplint violations into LSP diagnostics.
38
+ *
39
+ * Each diagnostic includes a `data.violationIndex` property that maps back
40
+ * to the original violation for use in Code Action resolution.
41
+ *
42
+ * @param result - The markuplint lint result, or null if no result is available
43
+ * @returns An array of LSP diagnostics augmented with line and col numbers
44
+ */
45
+ export function convertDiagnostics(result) {
46
+ const diagnostics = [];
47
+ if (!result) {
48
+ return diagnostics;
49
+ }
50
+ for (const [i, violation] of result.violations.entries()) {
51
+ diagnostics.push({
52
+ severity: violation.severity === 'error'
53
+ ? DiagnosticSeverity.Error
54
+ : violation.severity === 'warning'
55
+ ? DiagnosticSeverity.Warning
56
+ : DiagnosticSeverity.Information,
57
+ line: violation.line,
58
+ col: violation.col,
59
+ range: {
60
+ start: {
61
+ line: Math.max(violation.line - 1, 0),
62
+ character: Math.max(violation.col - 1, 0),
63
+ },
64
+ end: {
65
+ line: Math.max(violation.line - 1, 0),
66
+ character: Math.max(violation.col + violation.raw.length - 1, 0),
67
+ },
68
+ },
69
+ message: violation.message +
70
+ (violation.specConformance ? ` [${violation.specConformance}]` : '') +
71
+ (violation.reason ? ' / ' + violation.reason : '') +
72
+ (violation.name ? ` (${violation.ruleId})` : ''),
73
+ source: NAME,
74
+ code: violation.name ?? violation.ruleId,
75
+ codeDescription: {
76
+ href: `${WEBSITE_URL_RULE_PAGE}${violation.ruleId}`,
77
+ },
78
+ data: { violationIndex: i },
79
+ });
80
+ }
81
+ return diagnostics;
82
+ }
@@ -0,0 +1,3 @@
1
+ import debug from 'debug';
2
+ export declare const log: debug.Debugger;
3
+ export declare function verbosely(): void;
@@ -0,0 +1,7 @@
1
+ import debug from 'debug';
2
+ export const log = debug('vscode:markuplint');
3
+ const NAMESPACES = 'vscode:markuplint*,markuplint*,ml-*';
4
+ export function verbosely() {
5
+ debug.enable(NAMESPACES);
6
+ log(`[Debug] Enable: "${NAMESPACES}"`);
7
+ }
@@ -0,0 +1,78 @@
1
+ import type { Module, ModuleResolver } from './get-module.js';
2
+ import type { Config, LangConfigs, Log } from '../types.js';
3
+ import type { WorkingDirectoryEntry } from '../utils/resolve-working-directory.js';
4
+ import type { CodeAction, CodeActionParams, PublishDiagnosticsParams, HoverParams } from 'vscode-languageserver/node.js';
5
+ import type { TextDocument } from 'vscode-languageserver-textdocument';
6
+ /**
7
+ * The configuration of a language for a client that sends no `langConfigs`. It enables
8
+ * linting and leaves `defaultConfig` unset, so config discovery and the
9
+ * `markuplint:recommended` fallback behave as they do in the CLI.
10
+ */
11
+ export declare const DEFAULT_LANG_CONFIG: Config;
12
+ /**
13
+ * Callback for publishing diagnostics from the language server to the client.
14
+ */
15
+ export type SendDiagnostics = (params: PublishDiagnosticsParams) => void;
16
+ /**
17
+ * Options for creating the document event handlers.
18
+ */
19
+ export type EventHandlerOptions = {
20
+ /** Loads the markuplint module for a document's working directory */
21
+ resolveModule: ModuleResolver;
22
+ /** The user's locale (e.g. `"en"`, `"ja"`) */
23
+ locale: string;
24
+ /**
25
+ * Per-language configuration from the client's settings. Without it every document is
26
+ * linted with {@link DEFAULT_LANG_CONFIG}.
27
+ */
28
+ langConfigs?: LangConfigs;
29
+ /** User-configured working directories for monorepo support */
30
+ workingDirectories?: readonly WorkingDirectoryEntry[];
31
+ /** Absolute paths of the workspace folders */
32
+ workspaceFolders: readonly string[];
33
+ log: Log;
34
+ diagnosticsLog: Log;
35
+ errorLog: Log;
36
+ sendDiagnostics: SendDiagnostics;
37
+ /**
38
+ * Called on every document open with the module that document is linted with.
39
+ * The status bar therefore reflects the most recently opened document, not the active editor.
40
+ */
41
+ reportStatus: (mod: Module) => void;
42
+ /** Path to the git binary used for bulk suppression. */
43
+ gitPath?: string;
44
+ };
45
+ /**
46
+ * Determines the working directory a document belongs to.
47
+ *
48
+ * Both config discovery and markuplint module resolution are anchored here, so the two never
49
+ * disagree about which installation governs the document.
50
+ *
51
+ * @param document - The text document
52
+ * @param workspaceFolders - Absolute paths of the workspace folders
53
+ * @param workingDirectories - User-configured working directories
54
+ * @param log - Logger for general messages
55
+ * @returns The matched working directory, or the document's own directory when nothing matches
56
+ */
57
+ export declare function resolveDocumentWorkspace(document: TextDocument, workspaceFolders: readonly string[], workingDirectories: readonly WorkingDirectoryEntry[] | undefined, log: Log): string;
58
+ /**
59
+ * Creates version-aware event handlers for document open, change, code action, and hover events.
60
+ *
61
+ * Each document is linted with the markuplint module resolved from its own working directory,
62
+ * and dispatched to the handler (v2, v3, v4, or v5) matching that module's version.
63
+ *
64
+ * @param options - Configuration including the module resolver, locale, and settings
65
+ * @returns An object containing `onDidOpen`, `onDidChangeContent`, `onDidClose`, `onCodeAction`, and `onHover` handlers
66
+ */
67
+ export declare function createEventHandlers(options: EventHandlerOptions): {
68
+ onDidOpen(document: TextDocument): void;
69
+ onDidChangeContent(document: TextDocument): void;
70
+ onDidClose(document: TextDocument): void;
71
+ onCodeAction(params: CodeActionParams): CodeAction[];
72
+ onHover(params: HoverParams): Promise<{
73
+ contents: {
74
+ kind: "markdown";
75
+ value: string;
76
+ };
77
+ } | undefined>;
78
+ };
@@ -0,0 +1,299 @@
1
+ import { isFatalError } from 'markuplint/suppressions';
2
+ import { satisfies, lt, gte } from 'semver';
3
+ import { MarkupKind } from 'vscode-languageserver/node.js';
4
+ import { t } from '../i18n.js';
5
+ import { getFilePath } from '../utils/get-file-path.js';
6
+ import { resolveWorkingDirectory } from '../utils/resolve-working-directory.js';
7
+ import { createParserErrorHandler } from './parser-load-failure.js';
8
+ import * as v2 from './v2.js';
9
+ import * as v3 from './v3.js';
10
+ import * as v4 from './v4.js';
11
+ import * as v5 from './v5.js';
12
+ /**
13
+ * The configuration of a language for a client that sends no `langConfigs`. It enables
14
+ * linting and leaves `defaultConfig` unset, so config discovery and the
15
+ * `markuplint:recommended` fallback behave as they do in the CLI.
16
+ */
17
+ export const DEFAULT_LANG_CONFIG = {
18
+ enable: true,
19
+ debug: false,
20
+ hover: { accessibility: { enable: true } },
21
+ };
22
+ /**
23
+ * Determines the working directory a document belongs to.
24
+ *
25
+ * Both config discovery and markuplint module resolution are anchored here, so the two never
26
+ * disagree about which installation governs the document.
27
+ *
28
+ * @param document - The text document
29
+ * @param workspaceFolders - Absolute paths of the workspace folders
30
+ * @param workingDirectories - User-configured working directories
31
+ * @param log - Logger for general messages
32
+ * @returns The matched working directory, or the document's own directory when nothing matches
33
+ */
34
+ export function resolveDocumentWorkspace(
35
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
36
+ document, workspaceFolders, workingDirectories, log) {
37
+ const filePath = getFilePath(document.uri, document.languageId);
38
+ const absoluteFilePath = `${filePath.dirname}/${filePath.basename}`;
39
+ const resolved = resolveWorkingDirectory(absoluteFilePath, workspaceFolders, workingDirectories);
40
+ const workspace = resolved?.directory ?? filePath.dirname;
41
+ if (resolved) {
42
+ log(`Resolved working directory: ${workspace} (for ${filePath.basename})`, 'debug');
43
+ }
44
+ return workspace;
45
+ }
46
+ /**
47
+ * Creates version-aware event handlers for document open, change, code action, and hover events.
48
+ *
49
+ * Each document is linted with the markuplint module resolved from its own working directory,
50
+ * and dispatched to the handler (v2, v3, v4, or v5) matching that module's version.
51
+ *
52
+ * @param options - Configuration including the module resolver, locale, and settings
53
+ * @returns An object containing `onDidOpen`, `onDidChangeContent`, `onDidClose`, `onCodeAction`, and `onHover` handlers
54
+ */
55
+ export function createEventHandlers(
56
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
57
+ options) {
58
+ const documentModules = new Map();
59
+ const queues = new Map();
60
+ /**
61
+ * Documents whose close has been received but not yet carried out. The module of a
62
+ * document is only forgotten when the queued close runs, which is after any open still in
63
+ * progress; until then change, code action and hover requests must not reach the engine.
64
+ */
65
+ const closedDocuments = new Map();
66
+ function activeModule(uri) {
67
+ return closedDocuments.has(uri) ? undefined : documentModules.get(uri);
68
+ }
69
+ /**
70
+ * Runs `task` after every earlier open/close of the same document has finished.
71
+ *
72
+ * Opening is asynchronous (module resolution, then engine setup), so a close that
73
+ * arrives meanwhile would find nothing to release and the engine created afterwards
74
+ * would leak; a reopen that overlapped the release would start a second engine on
75
+ * the file while the first one's watcher is still shutting down.
76
+ *
77
+ * `task` must handle its own failures; only an error that must not be swallowed (a
78
+ * fatal one) may escape, and it is rethrown outside the queue so that it surfaces
79
+ * as an uncaught exception instead of stalling the document's later events.
80
+ */
81
+ function enqueue(uri, task) {
82
+ const tail = (queues.get(uri) ?? Promise.resolve())
83
+ .then(task)
84
+ .catch((error) => {
85
+ queueMicrotask(() => {
86
+ throw error;
87
+ });
88
+ })
89
+ .then(() => {
90
+ if (queues.get(uri) === tail) {
91
+ queues.delete(uri);
92
+ }
93
+ });
94
+ queues.set(uri, tail);
95
+ }
96
+ function parserErrorHandler(mod, languageId) {
97
+ return createParserErrorHandler({
98
+ languageId,
99
+ isLocalModule: mod.isLocalModule,
100
+ version: mod.version,
101
+ workspace: mod.workspace,
102
+ fallbackReason: mod.fallbackReason,
103
+ }, options.errorLog);
104
+ }
105
+ function dispatchOpen(mod,
106
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
107
+ document,
108
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
109
+ langConfig, workspace) {
110
+ const onError = parserErrorHandler(mod, document.languageId);
111
+ if (satisfies(mod.version, '2.x')) {
112
+ return v2.onDidOpen(document, mod.markuplint.MLEngine, langConfig, options.locale, options.sendDiagnostics, onError, workspace, options.log);
113
+ }
114
+ if (satisfies(mod.version, '3.x')) {
115
+ return v3.onDidOpen(document, mod.markuplint.MLEngine, langConfig, options.locale, options.log, options.diagnosticsLog, options.sendDiagnostics, onError, workspace);
116
+ }
117
+ if (satisfies(mod.version, '4.x')) {
118
+ return v4.onDidOpen(document, mod.markuplint.MLEngine, langConfig, options.locale, options.log, options.diagnosticsLog, options.sendDiagnostics, onError, workspace);
119
+ }
120
+ // v5+
121
+ return v5.onDidOpen(document, mod.markuplint.MLEngine, langConfig, options.locale, options.log, options.diagnosticsLog, options.sendDiagnostics, onError, workspace);
122
+ }
123
+ return {
124
+ onDidOpen(
125
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
126
+ document) {
127
+ const languageId = document.languageId;
128
+ const langConfig = options.langConfigs ? (options.langConfigs[languageId] ?? null) : DEFAULT_LANG_CONFIG;
129
+ if (!langConfig?.enable) {
130
+ options.log(`Disabled for languageId:${languageId} according to the settings of the client.`, 'warn');
131
+ return;
132
+ }
133
+ options.log(`Evaluate ${document.uri} from languageId:${languageId}`, 'info');
134
+ const workspace = resolveDocumentWorkspace(document, options.workspaceFolders, options.workingDirectories, options.log);
135
+ closedDocuments.delete(document.uri);
136
+ enqueue(document.uri, async () => {
137
+ let mod;
138
+ try {
139
+ mod = documentModules.get(document.uri) ?? (await options.resolveModule(workspace));
140
+ }
141
+ catch (error) {
142
+ if (isFatalError(error)) {
143
+ throw error;
144
+ }
145
+ options.errorLog(`Failed to load markuplint for ${workspace}: ${error}`);
146
+ return;
147
+ }
148
+ // Recorded before the engine exists so change events arriving during the
149
+ // asynchronous engine setup are routed to the same version handler.
150
+ documentModules.set(document.uri, mod);
151
+ options.reportStatus(mod);
152
+ try {
153
+ await dispatchOpen(mod, document, langConfig, workspace);
154
+ }
155
+ catch (error) {
156
+ if (isFatalError(error)) {
157
+ throw error;
158
+ }
159
+ options.errorLog(`Failed to open ${document.uri} with markuplint ${mod.version}: ${error}`);
160
+ }
161
+ });
162
+ },
163
+ onDidChangeContent(
164
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
165
+ document) {
166
+ const mod = activeModule(document.uri);
167
+ if (!mod) {
168
+ return;
169
+ }
170
+ const onError = parserErrorHandler(mod, document.languageId);
171
+ if (satisfies(mod.version, '2.x')) {
172
+ v2.onDidChangeContent(document, onError);
173
+ return;
174
+ }
175
+ if (satisfies(mod.version, '3.x')) {
176
+ v3.onDidChangeContent(document, options.log, onError);
177
+ return;
178
+ }
179
+ if (satisfies(mod.version, '4.x')) {
180
+ v4.onDidChangeContent(document, options.log, onError);
181
+ return;
182
+ }
183
+ // v5+
184
+ v5.onDidChangeContent(document, options.log, onError);
185
+ },
186
+ onDidClose(
187
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
188
+ document) {
189
+ const uri = document.uri;
190
+ const closing = Symbol(uri);
191
+ closedDocuments.set(uri, closing);
192
+ enqueue(uri, async () => {
193
+ const mod = documentModules.get(uri);
194
+ if (!mod) {
195
+ if (closedDocuments.get(uri) === closing) {
196
+ closedDocuments.delete(uri);
197
+ }
198
+ return;
199
+ }
200
+ documentModules.delete(uri);
201
+ const release = satisfies(mod.version, '2.x')
202
+ ? v2.onDidClose
203
+ : satisfies(mod.version, '3.x')
204
+ ? v3.onDidClose
205
+ : satisfies(mod.version, '4.x')
206
+ ? v4.onDidClose
207
+ : v5.onDidClose;
208
+ try {
209
+ await release(uri);
210
+ }
211
+ catch (error) {
212
+ if (isFatalError(error)) {
213
+ throw error;
214
+ }
215
+ options.errorLog(`Failed to release markuplint for ${uri}: ${error}`);
216
+ }
217
+ finally {
218
+ // The client keeps showing the last diagnostics of a document until told
219
+ // otherwise. Sent after the release: the engine's listeners are gone by then,
220
+ // so no lint can publish over it.
221
+ options.sendDiagnostics({ uri, diagnostics: [] });
222
+ if (closedDocuments.get(uri) === closing) {
223
+ closedDocuments.delete(uri);
224
+ }
225
+ }
226
+ });
227
+ },
228
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
229
+ onCodeAction(params) {
230
+ const uri = params.textDocument.uri;
231
+ const mod = activeModule(uri);
232
+ if (!mod) {
233
+ options.log(`Code Actions skipped: no module for ${uri}`, 'debug');
234
+ return [];
235
+ }
236
+ // Code Actions require v5.0.0+ (Violation.fix data + lint event fixSummary)
237
+ // Use '5.0.0-0' to include alpha/beta prereleases
238
+ if (!gte(mod.version, '5.0.0-0')) {
239
+ options.log(`Code Actions skipped: markuplint ${mod.version} < 5.0.0`, 'debug');
240
+ return [];
241
+ }
242
+ const actions = v5.onCodeAction(params);
243
+ options.log(`Code Actions: ${actions.length} for ${uri}`, 'debug');
244
+ return actions;
245
+ },
246
+ async onHover(
247
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
248
+ params) {
249
+ const hoverSettings = (options.langConfigs ? options.langConfigs['html'] : DEFAULT_LANG_CONFIG)?.hover
250
+ .accessibility;
251
+ if (!hoverSettings?.enable) {
252
+ return;
253
+ }
254
+ const mod = activeModule(params.textDocument.uri);
255
+ if (!mod) {
256
+ return;
257
+ }
258
+ const ariaVersion = hoverSettings.ariaVersion ?? mod.ariaRecommendedVersion;
259
+ if (lt(mod.version, '4.0.0')) {
260
+ const node = v3.getNodeWithAccessibilityProps(params.textDocument, params.position, ariaVersion);
261
+ if (!node) {
262
+ return;
263
+ }
264
+ const heading = `\`<${node.nodeName}>\` **${t('Computed Accessibility Properties')}**:\n`;
265
+ const props = node.exposed
266
+ ? `${Object.entries(node.aria)
267
+ .map(([key, value]) => `- ${key}: ${value}`)
268
+ .join('\n')}`
269
+ : `\n**${t('No exposed to accessibility tree')}** (${t('hidden element')})`;
270
+ return {
271
+ contents: {
272
+ kind: MarkupKind.Markdown,
273
+ value: heading + props,
274
+ },
275
+ };
276
+ }
277
+ const aria = satisfies(mod.version, '4.x')
278
+ ? await v4.getNodeWithAccessibilityProps(params.textDocument, params.position, ariaVersion)
279
+ : await v5.getNodeWithAccessibilityProps(params.textDocument, params.position, ariaVersion);
280
+ if (!aria) {
281
+ return;
282
+ }
283
+ const heading = `\`<${aria.nodeName}>\` **${t('Computed Accessibility Properties')}**:\n`;
284
+ const body = 'unknown' in aria
285
+ ? `\n**${t('Unknown')}**`
286
+ : aria.exposed
287
+ ? `${Object.entries(aria.labels)
288
+ .map(([key, value]) => `- ${key}: ${value}`)
289
+ .join('\n')}`
290
+ : `\n**${t('No exposed to accessibility tree')}** (${t('hidden element')})`;
291
+ return {
292
+ contents: {
293
+ kind: MarkupKind.Markdown,
294
+ value: heading + body,
295
+ },
296
+ };
297
+ },
298
+ };
299
+ }
@@ -0,0 +1,6 @@
1
+ import type { ARIAVersion } from '@markuplint/ml-spec';
2
+ import type { MLEngine } from 'markuplint';
3
+ export declare function getAccessibilityByLocation(engine: MLEngine, line: number, col: number, ariaVersion: ARIAVersion): Promise<{
4
+ node: string;
5
+ aria: import("@markuplint/ml-core").AccessibilityProperties | null;
6
+ } | null>;
@@ -0,0 +1,19 @@
1
+ export async function getAccessibilityByLocation(
2
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
3
+ engine, line, col, ariaVersion) {
4
+ if (!engine?.document) {
5
+ await engine.exec();
6
+ }
7
+ if (!engine?.document) {
8
+ return null;
9
+ }
10
+ const node = engine.document.searchNodeByLocation(line, col);
11
+ if (!node || !node.is(node.ELEMENT_NODE)) {
12
+ return null;
13
+ }
14
+ const aria = engine.document.getAccessibilityProp(node, ariaVersion);
15
+ return {
16
+ node: node.localName,
17
+ aria,
18
+ };
19
+ }