@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.
- package/README.md +74 -2
- package/bin/markuplint-language-server.mjs +3 -0
- package/lib/const.d.ts +41 -0
- package/lib/const.js +44 -0
- package/lib/i18n.d.ts +19 -0
- package/lib/i18n.js +63 -0
- package/lib/index.d.ts +41 -0
- package/lib/index.js +40 -0
- package/lib/lsp.d.ts +8 -0
- package/lib/lsp.js +8 -0
- package/lib/protocol.d.ts +10 -0
- package/lib/protocol.js +9 -0
- package/lib/server/client-channel.d.ts +38 -0
- package/lib/server/client-channel.js +89 -0
- package/lib/server/code-actions.d.ts +36 -0
- package/lib/server/code-actions.js +101 -0
- package/lib/server/convert-diagnostics.d.ts +28 -0
- package/lib/server/convert-diagnostics.js +82 -0
- package/lib/server/debug.d.ts +3 -0
- package/lib/server/debug.js +7 -0
- package/lib/server/document-events.d.ts +78 -0
- package/lib/server/document-events.js +299 -0
- package/lib/server/get-accessibility-by-location.d.ts +6 -0
- package/lib/server/get-accessibility-by-location.js +19 -0
- package/lib/server/get-module.d.ts +127 -0
- package/lib/server/get-module.js +257 -0
- package/lib/server/index.d.ts +1 -0
- package/lib/server/index.js +5 -0
- package/lib/server/initialize-params.d.ts +32 -0
- package/lib/server/initialize-params.js +28 -0
- package/lib/server/module-notices.d.ts +28 -0
- package/lib/server/module-notices.js +41 -0
- package/lib/server/parser-load-failure.d.ts +36 -0
- package/lib/server/parser-load-failure.js +85 -0
- package/lib/server/server.d.ts +20 -0
- package/lib/server/server.js +96 -0
- package/lib/server/suppression-support.d.ts +57 -0
- package/lib/server/suppression-support.js +233 -0
- package/lib/server/v2.d.ts +12 -0
- package/lib/server/v2.js +109 -0
- package/lib/server/v3.d.ts +19 -0
- package/lib/server/v3.js +208 -0
- package/lib/server/v4.d.ts +85 -0
- package/lib/server/v4.js +292 -0
- package/lib/server/v5.d.ts +89 -0
- package/lib/server/v5.js +339 -0
- package/lib/types.d.ts +88 -0
- package/lib/types.js +1 -0
- package/lib/utils/get-file-path.d.ts +15 -0
- package/lib/utils/get-file-path.js +45 -0
- package/lib/utils/resolve-working-directory.d.ts +44 -0
- package/lib/utils/resolve-working-directory.js +266 -0
- package/locales/en.json +7 -0
- package/locales/ja.json +21 -0
- 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,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 {};
|