@markuplint/language-server 0.0.0-stage → 5.1.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 (55) hide show
  1. package/README.md +74 -2
  2. package/bin/markuplint-language-server.mjs +3 -0
  3. package/lib/const.d.ts +41 -0
  4. package/lib/const.js +44 -0
  5. package/lib/i18n.d.ts +19 -0
  6. package/lib/i18n.js +63 -0
  7. package/lib/index.d.ts +41 -0
  8. package/lib/index.js +40 -0
  9. package/lib/lsp.d.ts +8 -0
  10. package/lib/lsp.js +8 -0
  11. package/lib/protocol.d.ts +10 -0
  12. package/lib/protocol.js +9 -0
  13. package/lib/server/client-channel.d.ts +38 -0
  14. package/lib/server/client-channel.js +89 -0
  15. package/lib/server/code-actions.d.ts +36 -0
  16. package/lib/server/code-actions.js +101 -0
  17. package/lib/server/convert-diagnostics.d.ts +28 -0
  18. package/lib/server/convert-diagnostics.js +82 -0
  19. package/lib/server/debug.d.ts +3 -0
  20. package/lib/server/debug.js +7 -0
  21. package/lib/server/document-events.d.ts +78 -0
  22. package/lib/server/document-events.js +299 -0
  23. package/lib/server/get-accessibility-by-location.d.ts +6 -0
  24. package/lib/server/get-accessibility-by-location.js +19 -0
  25. package/lib/server/get-module.d.ts +127 -0
  26. package/lib/server/get-module.js +257 -0
  27. package/lib/server/index.d.ts +1 -0
  28. package/lib/server/index.js +5 -0
  29. package/lib/server/initialize-params.d.ts +32 -0
  30. package/lib/server/initialize-params.js +28 -0
  31. package/lib/server/module-notices.d.ts +28 -0
  32. package/lib/server/module-notices.js +41 -0
  33. package/lib/server/parser-load-failure.d.ts +36 -0
  34. package/lib/server/parser-load-failure.js +85 -0
  35. package/lib/server/server.d.ts +20 -0
  36. package/lib/server/server.js +96 -0
  37. package/lib/server/suppression-support.d.ts +57 -0
  38. package/lib/server/suppression-support.js +233 -0
  39. package/lib/server/v2.d.ts +12 -0
  40. package/lib/server/v2.js +109 -0
  41. package/lib/server/v3.d.ts +19 -0
  42. package/lib/server/v3.js +208 -0
  43. package/lib/server/v4.d.ts +85 -0
  44. package/lib/server/v4.js +292 -0
  45. package/lib/server/v5.d.ts +89 -0
  46. package/lib/server/v5.js +339 -0
  47. package/lib/types.d.ts +88 -0
  48. package/lib/types.js +1 -0
  49. package/lib/utils/get-file-path.d.ts +15 -0
  50. package/lib/utils/get-file-path.js +45 -0
  51. package/lib/utils/resolve-working-directory.d.ts +44 -0
  52. package/lib/utils/resolve-working-directory.js +266 -0
  53. package/locales/en.json +7 -0
  54. package/locales/ja.json +21 -0
  55. package/package.json +58 -5
@@ -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
+ }
@@ -0,0 +1,127 @@
1
+ import type { Log } from '../types.js';
2
+ import { type ARIAVersion } from '@markuplint/ml-spec';
3
+ /**
4
+ * Convert a module path into a specifier that Node's ESM `import()` accepts.
5
+ *
6
+ * On Windows, raw drive-letter paths like `c:\foo` are rejected by the ESM
7
+ * loader (`ERR_UNSUPPORTED_ESM_URL_SCHEME: Received protocol 'c:'`) because
8
+ * the drive letter is parsed as a URL scheme. They must be converted to
9
+ * `file://` URLs first.
10
+ *
11
+ * Detection is OS-independent so that POSIX CI can exercise the Windows
12
+ * code path.
13
+ *
14
+ * Mirrors `packages/@markuplint/file-resolver/src/general-import.ts` —
15
+ * keep the two in sync when adjusting Windows-path handling. Note:
16
+ * file-resolver currently still relies on `pathToFileURL()` and carries
17
+ * the same Windows / POSIX-absolute mismatch; tracked separately as
18
+ * #3840 (dev / v5).
19
+ *
20
+ * @param modPath - A module specifier — typically the result of `Files.resolve`
21
+ * or `require.resolve`, which may be a bare module name (`markuplint`), a
22
+ * relative path (`./foo`), or an absolute path (Windows or POSIX).
23
+ * @returns A specifier safe to pass to `import()`. Absolute paths are converted
24
+ * to `file://` URLs with each segment percent-encoded; bare and relative
25
+ * specifiers are returned unchanged. UNC paths (`\\server\share\...`) are
26
+ * passed through unchanged as a known limitation.
27
+ * @see https://github.com/markuplint/markuplint/issues/3795
28
+ * @see https://github.com/markuplint/markuplint/issues/3836
29
+ * @see https://github.com/markuplint/markuplint/issues/3842
30
+ * @see https://nodejs.org/api/esm.html#urls
31
+ */
32
+ export declare function toImportSpecifier(modPath: string): string;
33
+ /**
34
+ * Resolve and load the markuplint module for a working directory.
35
+ *
36
+ * Attempts to load a markuplint installed upward from `workspace` first.
37
+ * If the local module fails to load (not installed, or import assertion incompatibility
38
+ * on Node.js 22+), falls back to the version installed with the language server.
39
+ *
40
+ * @param workspace - Absolute directory used as the resolution base
41
+ * @param log - Logger function for diagnostic output
42
+ * @param deps - Module system access; defaults to Node's own (injectable for tests)
43
+ * @returns The resolved module metadata including version, type, and optional fallback reason
44
+ */
45
+ export declare function loadModule(workspace: string, log: Log, deps?: LoadDeps): Promise<Module>;
46
+ type LoadDeps = {
47
+ readonly resolveEntry: (workspace: string, log: (message: string) => void) => string;
48
+ readonly importModule: (specifier: string) => Promise<any>;
49
+ readonly importBundledPackageJson: () => Promise<any>;
50
+ readonly readPackageJson: (packageJsonPath: string) => any;
51
+ };
52
+ /**
53
+ * Loads the markuplint module for a working directory.
54
+ */
55
+ export type ModuleResolver = (workspace: string) => Promise<Module>;
56
+ /**
57
+ * Create a resolver that loads the markuplint module per working directory and caches it.
58
+ *
59
+ * Resolution must follow the working directory rather than the workspace root: the modules a
60
+ * config refers to (`parser` / `specs` / `plugins`) are imported by `@markuplint/file-resolver`
61
+ * relative to the loaded markuplint installation, so a parser installed only in a sub-package
62
+ * is reachable only when markuplint itself is loaded from that sub-package. Falling back to the
63
+ * bundled copy there leaves the parser unreachable and the document silently undiagnosed.
64
+ *
65
+ * `onFirstResolve` fires once per distinct directory — the place for one-shot notices such as
66
+ * the bundled-fallback warning — whereas callers may report per-document status freely.
67
+ * A failed load is not cached so the next document in that directory retries.
68
+ *
69
+ * @param options.log - Logger function for diagnostic output
70
+ * @param options.onFirstResolve - Called once per directory when its module is first loaded
71
+ * @param options.load - Module loader (injectable for tests)
72
+ * @returns A resolver keyed by the normalized absolute directory
73
+ * @see https://github.com/markuplint/markuplint/issues/4048
74
+ */
75
+ export declare function createModuleResolver(options: {
76
+ readonly log: Log;
77
+ readonly onFirstResolve?: (mod: Module) => void;
78
+ readonly load?: (workspace: string, log: Log) => Promise<Module>;
79
+ }): ModuleResolver;
80
+ /**
81
+ * Metadata for the resolved markuplint module.
82
+ */
83
+ export type Module = {
84
+ /** Whether the module was loaded from a node_modules found upward from `workspace` */
85
+ isLocalModule: boolean;
86
+ /** The semver version string of the loaded module */
87
+ version: string;
88
+ /** The module system type declared in package.json */
89
+ moduleType: 'commonjs' | 'module';
90
+ /** The loaded markuplint module exports */
91
+ markuplint: any;
92
+ /** The ARIA specification version recommended by the loaded module */
93
+ ariaRecommendedVersion: ARIAVersion;
94
+ /**
95
+ * Reason the local module was skipped in favor of the bundled version.
96
+ * Set to `'import-assertion-compat'` when the local markuplint uses
97
+ * `assert { type: 'json' }` syntax that was removed in Node.js 22.
98
+ */
99
+ fallbackReason?: 'import-assertion-compat';
100
+ /** The normalized absolute working directory the module was resolved for */
101
+ readonly workspace: string;
102
+ };
103
+ type RequireDeps = {
104
+ readonly requireResolve: (name: string, options: {
105
+ readonly paths: readonly string[];
106
+ }) => string;
107
+ readonly readPackageJson: (packageJsonPath: string) => any;
108
+ };
109
+ /**
110
+ * Resolve the `markuplint` entry point with Node's CommonJS resolver, walking up from `dir`.
111
+ *
112
+ * Resolution stays in-process: `Files.resolve` from `vscode-languageserver` forks a Node process
113
+ * per call, which was acceptable for a single session-wide lookup but not for one lookup per
114
+ * working directory (every opened file's directory when `workingDirectories` is unset).
115
+ *
116
+ * `require.resolve` rejects a package whose `exports` map has no `require` condition (markuplint
117
+ * ships ESM only), so the entry point is then read from that `package.json` directly. The
118
+ * resolver functions are injectable because the server bundle is CommonJS while the test runner
119
+ * executes this file as ESM, where `require` is not defined.
120
+ *
121
+ * @param dir - Absolute directory to start the `node_modules` walk from
122
+ * @param log - Logger function for diagnostic output
123
+ * @param deps - `require.resolve` and `require` equivalents; defaults to Node's own
124
+ * @returns The absolute path of the markuplint entry point
125
+ */
126
+ export declare function resolveWithRequire(dir: string, log: (message: string) => void, deps?: RequireDeps): string;
127
+ export {};