@staticbolt/lsp 1.0.0-beta.25 → 1.0.0-beta.27

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.
@@ -0,0 +1,253 @@
1
+ import type {
2
+ HTMLDataV1,
3
+ IAttributeData,
4
+ IHTMLDataProvider,
5
+ IReference,
6
+ ITagData,
7
+ IValueData,
8
+ MarkupContent,
9
+ } from "vscode-html-languageservice";
10
+
11
+ type Description = string | MarkupContent | undefined;
12
+ type DescriptionKind = MarkupContent["kind"];
13
+ type ValueSets = Map<string, IValueData[]>;
14
+
15
+ /** Separates the documentation of two contributors. Rendered as a horizontal rule, unless the description is plain text. */
16
+ const MARKDOWN_SEPARATOR = "\n\n---\n\n";
17
+ const PLAINTEXT_SEPARATOR = "\n\n";
18
+
19
+ /** Groups entries by key, preserving the order of first appearance. */
20
+ function groupBy<T>(items: T[], toKey: (item: T) => string): T[][] {
21
+ const groups = new Map<string, T[]>();
22
+ const ordered: T[][] = [];
23
+
24
+ for (const item of items) {
25
+ const key = toKey(item);
26
+
27
+ let group = groups.get(key);
28
+ if (!group) {
29
+ group = [];
30
+ groups.set(key, group);
31
+ ordered.push(group);
32
+ }
33
+
34
+ group.push(item);
35
+ }
36
+
37
+ return ordered;
38
+ }
39
+
40
+ /** Markdown wins over plain text, so a contributor asking for rich text still gets it. Plain strings imply no preference. */
41
+ function mergeDescriptionKind(descriptions: Exclude<Description, undefined>[]): DescriptionKind | undefined {
42
+ let kind: DescriptionKind | undefined;
43
+
44
+ for (const description of descriptions) {
45
+ if (typeof description === "string" || kind === "markdown") continue;
46
+ kind = description.kind;
47
+ }
48
+
49
+ return kind;
50
+ }
51
+
52
+ /** Concatenates every distinct description, so no contributor's documentation gets lost. */
53
+ function mergeDescriptions(descriptions: Description[]): Description {
54
+ const defined = descriptions.filter(description => description !== undefined);
55
+
56
+ // nothing to merge, keep the original as-is
57
+ if (defined.length <= 1) return defined[0];
58
+
59
+ const parts: string[] = [];
60
+ const seen = new Set<string>();
61
+
62
+ for (const description of defined) {
63
+ const text = (typeof description === "string" ? description : description.value).trim();
64
+ if (!text || seen.has(text)) continue;
65
+
66
+ seen.add(text);
67
+ parts.push(text);
68
+ }
69
+
70
+ if (parts.length === 0) return undefined;
71
+
72
+ const kind = mergeDescriptionKind(defined);
73
+ const value = parts.join(kind === "plaintext" ? PLAINTEXT_SEPARATOR : MARKDOWN_SEPARATOR);
74
+
75
+ return kind ? { kind, value } : value;
76
+ }
77
+
78
+ function mergeReferences(references: (IReference[] | undefined)[]): IReference[] | undefined {
79
+ const merged: IReference[] = [];
80
+ const seen = new Set<string>();
81
+
82
+ for (const list of references) {
83
+ const entries = list ?? [];
84
+
85
+ for (const reference of entries) {
86
+ const key = `${reference.name} ${reference.url}`;
87
+ if (seen.has(key)) continue;
88
+
89
+ seen.add(key);
90
+ merged.push(reference);
91
+ }
92
+ }
93
+
94
+ return merged.length > 0 ? merged : undefined;
95
+ }
96
+
97
+ function mergeBrowsers(browsers: (string[] | undefined)[]): string[] | undefined {
98
+ const merged = new Set<string>();
99
+
100
+ for (const list of browsers) {
101
+ const entries = list ?? [];
102
+
103
+ for (const browser of entries) {
104
+ merged.add(browser);
105
+ }
106
+ }
107
+
108
+ return merged.size > 0 ? [...merged] : undefined;
109
+ }
110
+
111
+ /** The values an attribute contributes, with its `valueSet` reference expanded. */
112
+ function resolveValues(attribute: IAttributeData, valueSets: ValueSets): IValueData[] {
113
+ const values = attribute.values ?? [];
114
+ if (!attribute.valueSet) return values;
115
+
116
+ return [...(valueSets.get(attribute.valueSet) ?? []), ...values];
117
+ }
118
+
119
+ /** Merges entries sharing the same value name into one. Unlike tags and attributes, value names are case-sensitive. */
120
+ function mergeValues(values: IValueData[]): IValueData[] {
121
+ return groupBy(values, value => value.name).map(group => {
122
+ if (group.length === 1) return group[0];
123
+
124
+ return {
125
+ name: group[0].name,
126
+ description: mergeDescriptions(group.map(value => value.description)),
127
+ references: mergeReferences(group.map(value => value.references)),
128
+ browsers: mergeBrowsers(group.map(value => value.browsers)),
129
+ status: group.find(value => value.status)?.status,
130
+ };
131
+ });
132
+ }
133
+
134
+ /**
135
+ * Merges entries sharing the same attribute name into one, combining their descriptions, values, references and browser support.
136
+ *
137
+ * `valueSet` references are expanded into the merged `values`, so attributes contributed by different plugins can each bring
138
+ * their own value set and still end up with a single, complete value list.
139
+ */
140
+ function mergeAttributes(attributes: IAttributeData[], valueSets: ValueSets): IAttributeData[] {
141
+ return groupBy(attributes, attribute => attribute.name.toLowerCase()).map(group => {
142
+ // nothing to merge and no value set to expand, keep the original as-is
143
+ if (group.length === 1 && !group[0].valueSet) return group[0];
144
+
145
+ const values = mergeValues(group.flatMap(attribute => resolveValues(attribute, valueSets)));
146
+
147
+ return {
148
+ name: group[0].name,
149
+ description: mergeDescriptions(group.map(attribute => attribute.description)),
150
+ // `valueSet` is dropped on purpose: every referenced set is now expanded into `values`
151
+ values: values.length > 0 ? values : undefined,
152
+ references: mergeReferences(group.map(attribute => attribute.references)),
153
+ browsers: mergeBrowsers(group.map(attribute => attribute.browsers)),
154
+ status: group.find(attribute => attribute.status)?.status,
155
+ };
156
+ });
157
+ }
158
+
159
+ /** Merges entries sharing the same tag name into one, including their attributes. */
160
+ function mergeTags(tags: ITagData[], valueSets: ValueSets): ITagData[] {
161
+ return groupBy(tags, tag => tag.name.toLowerCase()).map(group => {
162
+ const attributes = mergeAttributes(
163
+ group.flatMap(tag => tag.attributes ?? []),
164
+ valueSets
165
+ );
166
+
167
+ if (group.length === 1) return { ...group[0], attributes };
168
+
169
+ return {
170
+ name: group[0].name,
171
+ description: mergeDescriptions(group.map(tag => tag.description)),
172
+ attributes,
173
+ references: mergeReferences(group.map(tag => tag.references)),
174
+ browsers: mergeBrowsers(group.map(tag => tag.browsers)),
175
+ status: group.find(tag => tag.status)?.status,
176
+ void: group.some(tag => tag.void),
177
+ };
178
+ });
179
+ }
180
+
181
+ /** Value sets sharing a name are merged, so an attribute referencing one gets the values of every contributor. */
182
+ function collectValueSets(htmlData: HTMLDataV1[]): ValueSets {
183
+ const valueSets: ValueSets = new Map();
184
+ const collected = htmlData.flatMap(data => data.valueSets ?? []);
185
+
186
+ for (const valueSet of collected) {
187
+ const existing = valueSets.get(valueSet.name);
188
+ valueSets.set(valueSet.name, existing ? mergeValues([...existing, ...valueSet.values]) : valueSet.values);
189
+ }
190
+
191
+ return valueSets;
192
+ }
193
+
194
+ /**
195
+ * Builds a single data provider out of every collected `HTMLDataV1`, merging tags, attributes and values that share the same name
196
+ * instead of reporting them once per contributor.
197
+ */
198
+ export function createMergedHtmlDataProvider(id: string, htmlData: HTMLDataV1[]): IHTMLDataProvider {
199
+ const valueSets = collectValueSets(htmlData);
200
+
201
+ // the language service asks for tags on every parse, and for attributes on every completion/hover, so merging happens once here
202
+ const tags = mergeTags(
203
+ htmlData.flatMap(data => data.tags ?? []),
204
+ valueSets
205
+ );
206
+
207
+ const globalAttributes = mergeAttributes(
208
+ htmlData.flatMap(data => data.globalAttributes ?? []),
209
+ valueSets
210
+ );
211
+
212
+ const tagsByName = new Map(tags.map(tag => [tag.name.toLowerCase(), tag]));
213
+ const attributesByTag = new Map<string, IAttributeData[]>();
214
+
215
+ function provideAttributes(tag: string): IAttributeData[] {
216
+ const key = tag.toLowerCase();
217
+
218
+ const cached = attributesByTag.get(key);
219
+ if (cached) return cached;
220
+
221
+ // an unknown tag only sees the global attributes, which are merged already
222
+ const tagAttributes = tagsByName.get(key)?.attributes;
223
+ if (!tagAttributes || tagAttributes.length === 0) return globalAttributes;
224
+
225
+ const attributes = mergeAttributes([...globalAttributes, ...tagAttributes], valueSets);
226
+ attributesByTag.set(key, attributes);
227
+
228
+ return attributes;
229
+ }
230
+
231
+ return {
232
+ getId() {
233
+ return id;
234
+ },
235
+
236
+ isApplicable(languageId) {
237
+ return languageId === "html";
238
+ },
239
+
240
+ provideTags() {
241
+ return tags;
242
+ },
243
+
244
+ provideAttributes,
245
+
246
+ provideValues(tag, attribute) {
247
+ const name = attribute.toLowerCase();
248
+
249
+ // merged attributes already carry their expanded value sets
250
+ return provideAttributes(tag).find(a => a.name.toLowerCase() === name)?.values ?? [];
251
+ },
252
+ };
253
+ }
@@ -4,14 +4,14 @@ import * as vscodeUri from "vscode-uri";
4
4
 
5
5
  import { ConfigManager } from "./helpers/config-loader.ts";
6
6
  import { findStaticboltProjects } from "./helpers/find-projects.ts";
7
- import { getLanguageModes } from "./modes/language-modes.ts";
7
+ import { getLanguageModes, isCompletionItemData } from "./modes/language-modes.ts";
8
8
  import { getFileSystemProvider } from "./requests.ts";
9
9
  import { pushAll } from "./utils/arrays.ts";
10
10
  import { getDocumentContext } from "./utils/document-context.ts";
11
11
  import { findProjectRoot } from "./utils/find-project-root.ts";
12
12
  import { runSafe } from "./utils/runner.ts";
13
13
 
14
- import type { CompletionItemData, LanguageModes, Settings } from "./modes/language-modes.ts";
14
+ import type { LanguageModes } from "./modes/language-modes.ts";
15
15
  import type { FileSystemProvider } from "./requests.ts";
16
16
  import type { Connection, Disposable, InitializeParams, WorkspaceFolder } from "vscode-languageserver";
17
17
 
@@ -43,13 +43,6 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
43
43
 
44
44
  let languageModes: LanguageModes;
45
45
 
46
- const documentSettings: { [key: string]: Thenable<Settings> } = {};
47
-
48
- // remove document settings on close
49
- documents.onDidClose(document => {
50
- delete documentSettings[document.document.uri];
51
- });
52
-
53
46
  connection.onInitialize((parameters: InitializeParams) => {
54
47
  if (Array.isArray(parameters.workspaceFolders)) {
55
48
  lspSearchRoots = parameters.workspaceFolders;
@@ -110,8 +103,9 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
110
103
  const config = await configManager.get(projectRoot);
111
104
  if (!config) return { isIncomplete: true, items: [] };
112
105
 
106
+ const htmlDocument = languageModes.getHtmlDocument(document);
113
107
  const documentContext = getDocumentContext(document.uri, workspaceFolders);
114
- return mode.doComplete(document, textDocumentPosition.position, documentContext, configManager.lspHtmlData);
108
+ return mode.doComplete(htmlDocument, textDocumentPosition.position, documentContext, configManager.lspHtmlData);
115
109
  },
116
110
  null,
117
111
  `Error while computing completions for ${textDocumentPosition.textDocument.uri}`,
@@ -132,7 +126,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
132
126
  const mode = languageModes.getMode(data.languageId);
133
127
  if (!mode?.doResolve) return item;
134
128
 
135
- return mode.doResolve(document, item);
129
+ return mode.doResolve(languageModes.getHtmlDocument(document), item);
136
130
  },
137
131
  item,
138
132
  `Error while resolving completion proposal`,
@@ -156,7 +150,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
156
150
  const config = await configManager.get(projectRoot);
157
151
  if (!config) return null;
158
152
 
159
- return mode.doHover(document, textDocumentPosition.position, configManager.lspHtmlData);
153
+ return mode.doHover(languageModes.getHtmlDocument(document), textDocumentPosition.position, configManager.lspHtmlData);
160
154
  },
161
155
  null,
162
156
  `Error while computing hover for ${textDocumentPosition.textDocument.uri}`,
@@ -176,10 +170,11 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
176
170
 
177
171
  const links: vscode.DocumentLink[] = [];
178
172
 
173
+ const htmlDocument = languageModes.getHtmlDocument(document);
179
174
  const documentContext = getDocumentContext(document.uri, workspaceFolders);
180
175
  for (const mode of languageModes.getAllModesInDocument(document)) {
181
176
  if (mode.findDocumentLinks) {
182
- pushAll(links, await mode.findDocumentLinks(document, documentContext, projectRoot));
177
+ pushAll(links, await mode.findDocumentLinks(htmlDocument, documentContext, projectRoot));
183
178
  }
184
179
  }
185
180
 
@@ -194,7 +189,3 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
194
189
  // Listen on the connection
195
190
  connection.listen();
196
191
  }
197
-
198
- export function isCompletionItemData(value: Record<string, unknown>): value is CompletionItemData {
199
- return value && typeof value.languageId === "string" && typeof value.uri === "string" && typeof value.offset === "number";
200
- }
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { format, stripVTControlCharacters } from "node:util";
1
2
  import * as vs from "vscode-languageserver/node";
2
3
 
3
4
  import { startServer } from "./html-server.ts";
@@ -10,8 +11,19 @@ import type { Connection, Disposable } from "vscode-languageserver/node";
10
11
  // Create a connection for the server.
11
12
  const connection: Connection = vs.createConnection();
12
13
 
13
- console.log = connection.console.log.bind(connection.console);
14
- console.error = connection.console.error.bind(connection.console);
14
+ /**
15
+ * `RemoteConsole` takes a single string, but the shared logger calls `console` with several arguments and colours them with
16
+ * chalk. Passing the methods straight through drops everything after the first argument, and the output panel renders no ANSI —
17
+ * so format the arguments the way `console` would, then strip the escapes.
18
+ */
19
+ function forward(write: (message: string) => void) {
20
+ return (...messages: unknown[]) => write(stripVTControlCharacters(format(...messages)));
21
+ }
22
+
23
+ console.log = forward(connection.console.log.bind(connection.console));
24
+ console.info = forward(connection.console.info.bind(connection.console));
25
+ console.warn = forward(connection.console.warn.bind(connection.console));
26
+ console.error = forward(connection.console.error.bind(connection.console));
15
27
 
16
28
  process.on("unhandledRejection", (error: unknown) => {
17
29
  connection.console.error(formatError(`Unhandled exception`, error));
@@ -1,40 +1,41 @@
1
1
  import vscodeHtml from "vscode-html-languageservice";
2
2
  import { TextDocument } from "vscode-languageserver-textdocument";
3
3
 
4
- import type { LanguageService, Range } from "vscode-html-languageservice";
4
+ import { blankRegions, findMarkdownNonHtmlRegions } from "./markdown-regions.ts";
5
5
 
6
- export interface LanguageRange extends Range {
7
- languageId: string | undefined;
8
- attributeValue?: boolean;
9
- }
6
+ import type { LanguageService } from "vscode-html-languageservice";
10
7
 
11
8
  export interface HTMLDocumentRegions {
12
- getEmbeddedDocument(languageId: string, shouldIgnoreAttributeValues?: boolean): TextDocument;
13
- getLanguageRanges(range: Range): LanguageRange[];
9
+ /** The document to serve HTML features from: itself for an HTML file, a copy declared as HTML for a markdown one. */
10
+ getHtmlDocument(): TextDocument;
14
11
  getLanguageAtPosition(position: Position): string | undefined;
15
12
  getLanguagesInDocument(): string[];
16
- getImportedScripts(): string[];
17
13
  }
18
14
 
19
- const CSS_STYLE_RULE = "__";
20
15
  const TokenType = vscodeHtml.TokenType;
21
- const Position = vscodeHtml.Position;
22
16
  type Position = vscodeHtml.Position;
23
17
 
24
18
  interface EmbeddedRegion {
25
19
  languageId: string | undefined;
26
20
  start: number;
27
21
  end: number;
28
- attributeValue?: boolean;
29
22
  }
30
23
 
31
24
  export function getDocumentRegions(languageService: LanguageService, document: TextDocument): HTMLDocumentRegions {
25
+ const source = document.getText();
26
+ const isMarkdown = document.languageId === "markdown";
27
+
28
+ const markdownRegions: EmbeddedRegion[] = isMarkdown
29
+ ? findMarkdownNonHtmlRegions(source).map(region => ({ ...region, languageId: undefined }))
30
+ : [];
31
+
32
+ const htmlText = isMarkdown ? blankRegions(source, markdownRegions) : source;
33
+
32
34
  const regions: EmbeddedRegion[] = [];
33
- const scanner = languageService.createScanner(document.getText());
35
+ const scanner = languageService.createScanner(htmlText);
34
36
  let lastTagName: string = "";
35
37
  let lastAttributeName: string | null = null;
36
38
  let languageIdFromType: string | undefined;
37
- const importedScripts: string[] = [];
38
39
 
39
40
  let token = scanner.scan();
40
41
 
@@ -59,13 +60,7 @@ export function getDocumentRegions(languageService: LanguageService, document: T
59
60
  break;
60
61
  }
61
62
  case TokenType.AttributeValue: {
62
- if (lastAttributeName === "src" && lastTagName.toLowerCase() === "script") {
63
- let value = scanner.getTokenText();
64
- if (value[0] === "'" || value[0] === '"') {
65
- value = value.slice(1, -1);
66
- }
67
- importedScripts.push(value);
68
- } else if (lastAttributeName === "type" && lastTagName.toLowerCase() === "script") {
63
+ if (lastAttributeName === "type" && lastTagName.toLowerCase() === "script") {
69
64
  const token = scanner.getTokenText();
70
65
  if (/["'](module|(text|application)\/(java|ecma)script|text\/babel)["']/.test(token) || token === "module") {
71
66
  languageIdFromType = "javascript";
@@ -79,12 +74,12 @@ export function getDocumentRegions(languageService: LanguageService, document: T
79
74
  if (attributeLanguageId) {
80
75
  let start = scanner.getTokenOffset();
81
76
  let end = scanner.getTokenEnd();
82
- const firstChar = document.getText()[start];
77
+ const firstChar = htmlText[start];
83
78
  if (firstChar === "'" || firstChar === '"') {
84
79
  start++;
85
80
  end--;
86
81
  }
87
- regions.push({ languageId: attributeLanguageId, start, end, attributeValue: true });
82
+ regions.push({ languageId: attributeLanguageId, start, end });
88
83
  }
89
84
  }
90
85
  lastAttributeName = null;
@@ -93,76 +88,43 @@ export function getDocumentRegions(languageService: LanguageService, document: T
93
88
  }
94
89
  token = scanner.scan();
95
90
  }
91
+
92
+ const allRegions = mergeRegions(regions, markdownRegions);
93
+ const htmlDocument = isMarkdown ? TextDocument.create(document.uri, "html", document.version, htmlText) : document;
94
+
96
95
  return {
97
- getLanguageRanges: (range: Range) => getLanguageRanges(document, regions, range),
98
- getEmbeddedDocument: (languageId: string, shouldIgnoreAttributeValues: boolean) =>
99
- getEmbeddedDocument(document, regions, languageId, shouldIgnoreAttributeValues),
100
- getLanguageAtPosition: (position: Position) => getLanguageAtPosition(document, regions, position),
101
- getLanguagesInDocument: () => getLanguagesInDocument(document, regions),
102
- getImportedScripts: () => importedScripts,
96
+ getLanguageAtPosition: (position: Position) => getLanguageAtPosition(document, allRegions, position),
97
+ getLanguagesInDocument: () => getLanguagesInDocument(allRegions),
98
+ getHtmlDocument: () => htmlDocument,
103
99
  };
104
100
  }
105
101
 
106
- function getLanguageRanges(document: TextDocument, regions: EmbeddedRegion[], range: Range): LanguageRange[] {
107
- const result: LanguageRange[] = [];
108
- let currentPos = range ? range.start : Position.create(0, 0);
109
- let currentOffset = range ? document.offsetAt(range.start) : 0;
110
- const endOffset = range ? document.offsetAt(range.end) : document.getText().length;
111
- for (const region of regions) {
112
- if (!(region.end > currentOffset && region.start < endOffset)) {
113
- continue;
114
- }
102
+ /** The lookups below read the regions in order, so an overlap is dropped: a scanned region always wins over a markdown one. */
103
+ function mergeRegions(scanned: EmbeddedRegion[], markdown: EmbeddedRegion[]): EmbeddedRegion[] {
104
+ if (markdown.length === 0) return scanned;
115
105
 
116
- const start = Math.max(region.start, currentOffset);
117
- const startPos = document.positionAt(start);
118
- if (currentOffset < region.start) {
119
- result.push({
120
- start: currentPos,
121
- end: startPos,
122
- languageId: "html",
123
- });
124
- }
125
- const end = Math.min(region.end, endOffset);
126
- const endPos = document.positionAt(end);
127
- if (end > region.start) {
128
- result.push({
129
- start: startPos,
130
- end: endPos,
131
- languageId: region.languageId,
132
- attributeValue: region.attributeValue,
133
- });
134
- }
135
- currentOffset = end;
136
- currentPos = endPos;
137
- }
138
- if (currentOffset < endOffset) {
139
- const endPos = range ? range.end : document.positionAt(endOffset);
140
- result.push({
141
- start: currentPos,
142
- end: endPos,
143
- languageId: "html",
144
- });
106
+ const merged = [...scanned, ...markdown].toSorted((a, b) => a.start - b.start || b.end - a.end);
107
+ const result: EmbeddedRegion[] = [];
108
+
109
+ for (const region of merged) {
110
+ const previous = result.at(-1);
111
+ if (previous && region.start < previous.end) continue;
112
+ result.push(region);
145
113
  }
114
+
146
115
  return result;
147
116
  }
148
117
 
149
- function getLanguagesInDocument(_document: TextDocument, regions: EmbeddedRegion[]): string[] {
150
- const result: string[] = [];
118
+ function getLanguagesInDocument(regions: EmbeddedRegion[]): string[] {
119
+ const languages = new Set(["html"]);
151
120
 
152
121
  for (const region of regions) {
153
- if (!(region.languageId && !result.includes(region.languageId))) {
154
- continue;
155
- }
156
-
157
- result.push(region.languageId);
158
- if (result.length === 3) {
159
- return result;
122
+ if (region.languageId) {
123
+ languages.add(region.languageId);
160
124
  }
161
125
  }
162
126
 
163
- result.push("html");
164
-
165
- return result;
127
+ return [...languages];
166
128
  }
167
129
 
168
130
  function getLanguageAtPosition(document: TextDocument, regions: EmbeddedRegion[], position: Position): string | undefined {
@@ -179,100 +141,6 @@ function getLanguageAtPosition(document: TextDocument, regions: EmbeddedRegion[]
179
141
  return "html";
180
142
  }
181
143
 
182
- function getEmbeddedDocument(
183
- document: TextDocument,
184
- contents: EmbeddedRegion[],
185
- languageId: string,
186
- shouldIgnoreAttributeValues: boolean
187
- ): TextDocument {
188
- let currentPos = 0;
189
- const oldContent = document.getText();
190
- let result = "";
191
- let lastSuffix = "";
192
- for (const c of contents) {
193
- if (!(c.languageId === languageId && (!shouldIgnoreAttributeValues || !c.attributeValue))) {
194
- continue;
195
- }
196
-
197
- result = substituteWithWhitespace(result, currentPos, c.start, oldContent, lastSuffix, getPrefix(c));
198
- result += updateContent(c, oldContent.slice(c.start, c.end));
199
- currentPos = c.end;
200
- lastSuffix = getSuffix(c);
201
- }
202
- result = substituteWithWhitespace(result, currentPos, oldContent.length, oldContent, lastSuffix, "");
203
- return TextDocument.create(document.uri, languageId, document.version, result);
204
- }
205
-
206
- function getPrefix(c: EmbeddedRegion) {
207
- if (c.attributeValue) {
208
- switch (c.languageId) {
209
- case "css": {
210
- return CSS_STYLE_RULE + "{";
211
- }
212
- }
213
- }
214
- return "";
215
- }
216
- function getSuffix(c: EmbeddedRegion) {
217
- if (c.attributeValue) {
218
- switch (c.languageId) {
219
- case "css": {
220
- return "}";
221
- }
222
- case "javascript": {
223
- return ";";
224
- }
225
- }
226
- }
227
- return "";
228
- }
229
- function updateContent(c: EmbeddedRegion, content: string): string {
230
- if (!c.attributeValue && c.languageId === "javascript") {
231
- return content.replace(`<!--`, `/* `).replace(`-->`, ` */`);
232
- }
233
- if (c.languageId === "css") {
234
- const quoteEscape = /(&quot;|&#34;)/g;
235
- return content.replace(quoteEscape, (match, _, offset: number) => {
236
- const spaces = " ".repeat(match.length - 1);
237
- const afterChar = content[offset + match.length];
238
- if (!afterChar || afterChar.includes(" ")) {
239
- return `${spaces}"`;
240
- }
241
- return `"${spaces}`;
242
- });
243
- }
244
- return content;
245
- }
246
-
247
- function substituteWithWhitespace(result: string, start: number, end: number, oldContent: string, before: string, after: string) {
248
- result += before;
249
- let accumulatedWS = -before.length; // start with a negative value to account for the before string
250
- for (let index = start; index < end; index++) {
251
- const ch = oldContent[index];
252
- if (ch === "\n" || ch === "\r") {
253
- // only write new lines, skip the whitespace
254
- accumulatedWS = 0;
255
- result += ch;
256
- } else {
257
- accumulatedWS++;
258
- }
259
- }
260
- result = append(result, " ", accumulatedWS - after.length);
261
- result += after;
262
- return result;
263
- }
264
-
265
- function append(result: string, string_: string, n: number): string {
266
- while (n > 0) {
267
- if (n & 1) {
268
- result += string_;
269
- }
270
- n >>= 1;
271
- string_ += string_;
272
- }
273
- return result;
274
- }
275
-
276
144
  function getAttributeLanguage(attributeName: string): string | null {
277
145
  const match = attributeName.match(/^(style)$|^(on\w+)$/i);
278
146
  if (!match) {