@marko/language-tools 2.5.63 → 2.6.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.
@@ -0,0 +1,19 @@
1
+ import { type Range } from "../../parser";
2
+ import { type Extracted, Extractor } from "../../util/extractor";
3
+ export interface ExtractCSSModuleOptions {
4
+ code: string;
5
+ fileName: string;
6
+ }
7
+ /**
8
+ * Produce a virtual TypeScript module typing a CSS module's default export as
9
+ * an object of its class/id names. The type is exported directly (not via a
10
+ * named `const`) so no internal binding name leaks into hover/go-to-definition,
11
+ * and each name is mapped back to every selector it was defined at.
12
+ */
13
+ export declare function extractCSSModule(opts: ExtractCSSModuleOptions): Extracted;
14
+ /**
15
+ * Write an object type whose properties are the given CSS module class/id
16
+ * names, each source-mapped to the selector(s) it was defined at. Shared by the
17
+ * standalone `.module.css` extractor and the embedded `<style/var>` extractor.
18
+ */
19
+ export declare function writeStyleModuleType(extractor: Extractor, selectors: Map<string, Range[]>): void;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from "./extractors/css-module";
1
2
  export * from "./extractors/html";
2
3
  export * from "./extractors/script";
3
4
  export * from "./extractors/style";
package/dist/index.js CHANGED
@@ -35,6 +35,7 @@ __export(index_exports, {
35
35
  Project: () => project_exports,
36
36
  ScriptLang: () => ScriptLang,
37
37
  UNFINISHED: () => UNFINISHED,
38
+ extractCSSModule: () => extractCSSModule,
38
39
  extractHTML: () => extractHTML,
39
40
  extractScript: () => extractScript,
40
41
  extractStyle: () => extractStyle,
@@ -45,7 +46,8 @@ __export(index_exports, {
45
46
  isControlFlowTag: () => isControlFlowTag,
46
47
  isDefinitionFile: () => isDefinitionFile,
47
48
  normalizePath: () => normalizePath,
48
- parse: () => parse
49
+ parse: () => parse,
50
+ writeStyleModuleType: () => writeStyleModuleType
49
51
  });
50
52
  module.exports = __toCommonJS(index_exports);
51
53
 
@@ -1087,6 +1089,187 @@ function pushUniqueRange(ranges, start, end) {
1087
1089
  ranges.push({ start, end });
1088
1090
  }
1089
1091
 
1092
+ // src/util/find-style-selectors.ts
1093
+ function findStyleSelectors(code, offset = 0) {
1094
+ const result = /* @__PURE__ */ new Map();
1095
+ const pending = [];
1096
+ const braceStack = [];
1097
+ const parenStack = [];
1098
+ let bareGlobal = false;
1099
+ let pendingScope = null;
1100
+ const len = code.length;
1101
+ let i = 0;
1102
+ const blockGlobal = () => braceStack.length ? braceStack[braceStack.length - 1] : false;
1103
+ while (i < len) {
1104
+ const ch = code[i];
1105
+ const next = code[i + 1];
1106
+ if (ch === "/" && next === "*") {
1107
+ i += 2;
1108
+ while (i < len && !(code[i] === "*" && code[i + 1] === "/")) i++;
1109
+ i += 2;
1110
+ } else if (ch === "/" && next === "/") {
1111
+ i += 2;
1112
+ while (i < len && code[i] !== "\n") i++;
1113
+ } else if (ch === '"' || ch === "'") {
1114
+ i = skipString(code, i);
1115
+ } else if (isInterpolationStart(ch) && next === "{") {
1116
+ i = skipBalanced(code, i + 1);
1117
+ } else if (ch === ":") {
1118
+ let end = i + 1;
1119
+ while (end < len && isNameChar(code[end])) end++;
1120
+ const pseudo = code.slice(i + 1, end).toLowerCase();
1121
+ if (pseudo === "global" || pseudo === "local") {
1122
+ const isGlobal = pseudo === "global";
1123
+ if (code[end] === "(") {
1124
+ pendingScope = isGlobal;
1125
+ } else {
1126
+ let j = end;
1127
+ while (j < len && isWhitespace(code[j])) j++;
1128
+ if (code[j] === "{") {
1129
+ pendingScope = isGlobal;
1130
+ } else {
1131
+ bareGlobal = isGlobal;
1132
+ }
1133
+ }
1134
+ }
1135
+ i = end;
1136
+ } else if (ch === "(") {
1137
+ parenStack.push(
1138
+ pendingScope ?? (parenStack.length ? parenStack[parenStack.length - 1] : bareGlobal)
1139
+ );
1140
+ pendingScope = null;
1141
+ i++;
1142
+ } else if (ch === ")") {
1143
+ parenStack.pop();
1144
+ i++;
1145
+ } else if (ch === "." || ch === "#") {
1146
+ const start = i + 1;
1147
+ if (isNameStart(code[start])) {
1148
+ let end = start + 1;
1149
+ while (end < len && isNameChar(code[end])) end++;
1150
+ if (!(isInterpolationStart(code[end]) && code[end + 1] === "{")) {
1151
+ const global = parenStack.length ? parenStack[parenStack.length - 1] : bareGlobal;
1152
+ if (!global) pending.push({ start, end });
1153
+ }
1154
+ i = end;
1155
+ } else {
1156
+ i++;
1157
+ }
1158
+ } else if (ch === "{") {
1159
+ for (const range of pending) {
1160
+ const name = code.slice(range.start, range.end);
1161
+ const offsetRange = {
1162
+ start: range.start + offset,
1163
+ end: range.end + offset
1164
+ };
1165
+ const ranges = result.get(name);
1166
+ if (ranges) {
1167
+ ranges.push(offsetRange);
1168
+ } else {
1169
+ result.set(name, [offsetRange]);
1170
+ }
1171
+ }
1172
+ pending.length = 0;
1173
+ braceStack.push(pendingScope ?? blockGlobal());
1174
+ pendingScope = null;
1175
+ parenStack.length = 0;
1176
+ bareGlobal = blockGlobal();
1177
+ i++;
1178
+ } else if (ch === "}") {
1179
+ pending.length = 0;
1180
+ braceStack.pop();
1181
+ parenStack.length = 0;
1182
+ bareGlobal = blockGlobal();
1183
+ i++;
1184
+ } else if (ch === ";") {
1185
+ pending.length = 0;
1186
+ parenStack.length = 0;
1187
+ bareGlobal = blockGlobal();
1188
+ i++;
1189
+ } else if (ch === ",") {
1190
+ bareGlobal = blockGlobal();
1191
+ i++;
1192
+ } else {
1193
+ i++;
1194
+ }
1195
+ }
1196
+ return result;
1197
+ }
1198
+ function skipString(code, start) {
1199
+ const quote = code[start];
1200
+ const len = code.length;
1201
+ let i = start + 1;
1202
+ while (i < len) {
1203
+ const ch = code[i];
1204
+ if (ch === "\\") {
1205
+ i += 2;
1206
+ } else if (ch === quote) {
1207
+ return i + 1;
1208
+ } else {
1209
+ i++;
1210
+ }
1211
+ }
1212
+ return i;
1213
+ }
1214
+ function skipBalanced(code, openBrace) {
1215
+ const len = code.length;
1216
+ let depth = 0;
1217
+ let i = openBrace;
1218
+ while (i < len) {
1219
+ const ch = code[i];
1220
+ if (ch === "{") {
1221
+ depth++;
1222
+ } else if (ch === "}") {
1223
+ if (--depth === 0) return i + 1;
1224
+ }
1225
+ i++;
1226
+ }
1227
+ return i;
1228
+ }
1229
+ function isNameStart(ch) {
1230
+ if (ch === void 0) return false;
1231
+ const code = ch.charCodeAt(0);
1232
+ return code >= 97 && code <= 122 || // a-z
1233
+ code >= 65 && code <= 90 || // A-Z
1234
+ ch === "_" || ch === "-" || code >= 128;
1235
+ }
1236
+ function isNameChar(ch) {
1237
+ const code = ch.charCodeAt(0);
1238
+ return code >= 48 && code <= 57 || isNameStart(ch);
1239
+ }
1240
+ function isInterpolationStart(ch) {
1241
+ return ch === "#" || ch === "@" || ch === "$";
1242
+ }
1243
+ function isWhitespace(ch) {
1244
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f";
1245
+ }
1246
+
1247
+ // src/extractors/css-module/index.ts
1248
+ function extractCSSModule(opts) {
1249
+ const { code, fileName } = opts;
1250
+ const extractor = new Extractor(createSource(code, fileName));
1251
+ extractor.write("export default {} as unknown as ");
1252
+ writeStyleModuleType(extractor, findStyleSelectors(code));
1253
+ extractor.write(";\n");
1254
+ return extractor.end();
1255
+ }
1256
+ function writeStyleModuleType(extractor, selectors) {
1257
+ extractor.write("{");
1258
+ for (const ranges of selectors.values()) {
1259
+ extractor.write(' "').copy(ranges).write('": string;');
1260
+ }
1261
+ extractor.write(" }");
1262
+ }
1263
+ function createSource(code, fileName) {
1264
+ const lines = (0, import_htmljs_parser2.getLines)(code);
1265
+ return {
1266
+ code,
1267
+ filename: normalizePath(fileName),
1268
+ positionAt: (offset) => (0, import_htmljs_parser2.getPosition)(lines, offset),
1269
+ locationAt: (range) => (0, import_htmljs_parser2.getLocation)(lines, range.start, range.end)
1270
+ };
1271
+ }
1272
+
1090
1273
  // src/extractors/html/keywords.ts
1091
1274
  var builtinTagsRegex = (
1092
1275
  /* cspell:disable-next-line */
@@ -2950,7 +3133,11 @@ constructor(_) {}
2950
3133
  if (tag.var) {
2951
3134
  this.#closeBrackets[this.#closeBrackets.length - 1]++;
2952
3135
  this.#extractor.write("{const ");
2953
- if (isHTML) {
3136
+ if (tagName === "style") {
3137
+ this.#extractor.copy(tag.var.value).write(" = ");
3138
+ this.#writeStyleModuleVar(tag);
3139
+ this.#extractor.write(";\n");
3140
+ } else if (isHTML) {
2954
3141
  this.#extractor.copy(tag.var.value).write(` = ${varShared("el")}(${JSON.stringify(def.name)});
2955
3142
  `);
2956
3143
  } else {
@@ -2990,6 +3177,40 @@ constructor(_) {}
2990
3177
  }
2991
3178
  }
2992
3179
  }
3180
+ /** Types a `<style/foo>` CSS module var as an object of its class/id names. */
3181
+ #writeStyleModuleVar(tag) {
3182
+ const selectors = this.#getStyleModuleSelectors(tag);
3183
+ if (this.#scriptLang === "ts" /* ts */) {
3184
+ this.#extractor.write(`${varShared("any")} as `);
3185
+ writeStyleModuleType(this.#extractor, selectors);
3186
+ } else {
3187
+ this.#extractor.write("/** @type {");
3188
+ writeStyleModuleType(this.#extractor, selectors);
3189
+ this.#extractor.write(`} */(${varShared("any")})`);
3190
+ }
3191
+ }
3192
+ #getStyleModuleSelectors(tag) {
3193
+ const result = /* @__PURE__ */ new Map();
3194
+ if (tag.body) {
3195
+ for (const child of tag.body) {
3196
+ if (child.type === 17 /* Text */) {
3197
+ const found = findStyleSelectors(
3198
+ this.#code.slice(child.start, child.end),
3199
+ child.start
3200
+ );
3201
+ for (const [name, ranges] of found) {
3202
+ const existing = result.get(name);
3203
+ if (existing) {
3204
+ existing.push(...ranges);
3205
+ } else {
3206
+ result.set(name, ranges);
3207
+ }
3208
+ }
3209
+ }
3210
+ }
3211
+ }
3212
+ return result;
3213
+ }
2993
3214
  #endChildren() {
2994
3215
  const pendingBrackets = this.#closeBrackets.pop();
2995
3216
  if (pendingBrackets) {
@@ -3973,7 +4194,9 @@ var processors_exports = {};
3973
4194
  __export(processors_exports, {
3974
4195
  create: () => create,
3975
4196
  extensions: () => extensions,
3976
- has: () => has
4197
+ getProcessorExtension: () => getProcessorExtension,
4198
+ has: () => has,
4199
+ isCSSModule: () => isCSSModule
3977
4200
  });
3978
4201
 
3979
4202
  // src/util/get-ext.ts
@@ -3982,6 +4205,34 @@ function getExt(fileName) {
3982
4205
  if (extIndex !== -1) return fileName.slice(extIndex);
3983
4206
  }
3984
4207
 
4208
+ // src/processors/css-module.ts
4209
+ var css_module_default = {
4210
+ extensions: [".module.css", ".module.scss", ".module.less"],
4211
+ create({ ts }) {
4212
+ return {
4213
+ getScriptExtension() {
4214
+ return ts.Extension.Ts;
4215
+ },
4216
+ getScriptKind() {
4217
+ return ts.ScriptKind.TS;
4218
+ },
4219
+ extract(fileName, code) {
4220
+ return extractCSSModule({ code, fileName });
4221
+ },
4222
+ print({ extracted }) {
4223
+ return { code: extracted.toString() };
4224
+ },
4225
+ printTypes({ sourceFile, printer }) {
4226
+ let code = "";
4227
+ for (const statement of sourceFile.statements) {
4228
+ code += printer.printNode(ts.EmitHint.Unspecified, statement, sourceFile) + "\n";
4229
+ }
4230
+ return { code };
4231
+ }
4232
+ };
4233
+ }
4234
+ };
4235
+
3985
4236
  // src/processors/marko.ts
3986
4237
  var import_path4 = __toESM(require("path"));
3987
4238
 
@@ -4225,7 +4476,7 @@ function interopDefault(mod) {
4225
4476
  var isRemapExtensionReg = /\.ts$/;
4226
4477
  var skipRemapExtensionsReg = /\.(?:[cm]?jsx?|json|marko|css|less|sass|scss|styl|stylus|pcss|postcss|sss|a?png|jpe?g|jfif|pipeg|pjp|gif|svg|ico|web[pm]|avif|mp4|ogg|mp3|wav|flac|aac|opus|woff2?|eot|[ot]tf|webmanifest|pdf|txt)$/;
4227
4478
  var marko_default = {
4228
- extension: ".marko",
4479
+ extensions: [".marko"],
4229
4480
  create({ ts, host, configFile }) {
4230
4481
  const currentDirectory = host.getCurrentDirectory ? host.getCurrentDirectory() : ts.sys.getCurrentDirectory();
4231
4482
  const defaultScriptLang = configFile && /tsconfig/.test(configFile) ? "ts" /* ts */ : "js" /* js */;
@@ -4422,15 +4673,48 @@ function castType(type) {
4422
4673
  }
4423
4674
 
4424
4675
  // src/processors/index.ts
4425
- var extensions = [marko_default.extension];
4676
+ var configs = [marko_default, css_module_default];
4677
+ var simpleExtensions = /* @__PURE__ */ new Set();
4678
+ var compoundExtensions = [];
4679
+ for (const config of configs) {
4680
+ for (const extension of config.extensions) {
4681
+ if (extension.indexOf(".", 1) === -1) {
4682
+ simpleExtensions.add(extension);
4683
+ } else {
4684
+ compoundExtensions.push(extension);
4685
+ }
4686
+ }
4687
+ }
4688
+ var cssModuleExtensions = new Set(css_module_default.extensions);
4689
+ var extensions = configs.flatMap(
4690
+ (config) => config.extensions
4691
+ );
4426
4692
  function create(options) {
4427
- return {
4428
- [marko_default.extension]: marko_default.create(options)
4429
- };
4693
+ const result = {};
4694
+ for (const config of configs) {
4695
+ const processor = config.create(options);
4696
+ for (const extension of config.extensions) {
4697
+ result[extension] = processor;
4698
+ }
4699
+ }
4700
+ return result;
4430
4701
  }
4431
- function has(fileName) {
4702
+ function getProcessorExtension(fileName) {
4432
4703
  const ext = getExt(fileName);
4433
- return !!(ext && extensions.includes(ext));
4704
+ if (ext && simpleExtensions.has(ext)) return ext;
4705
+ if (compoundExtensions.length) {
4706
+ const lower = fileName.toLowerCase();
4707
+ for (const compound of compoundExtensions) {
4708
+ if (lower.endsWith(compound)) return compound;
4709
+ }
4710
+ }
4711
+ }
4712
+ function has(fileName) {
4713
+ return getProcessorExtension(fileName) !== void 0;
4714
+ }
4715
+ function isCSSModule(fileName) {
4716
+ const ext = getProcessorExtension(fileName);
4717
+ return ext !== void 0 && cssModuleExtensions.has(ext);
4434
4718
  }
4435
4719
 
4436
4720
  // src/util/is-definition-file.ts
@@ -4444,6 +4728,7 @@ function isDefinitionFile(fileName) {
4444
4728
  Project,
4445
4729
  ScriptLang,
4446
4730
  UNFINISHED,
4731
+ extractCSSModule,
4447
4732
  extractHTML,
4448
4733
  extractScript,
4449
4734
  extractStyle,
@@ -4454,5 +4739,6 @@ function isDefinitionFile(fileName) {
4454
4739
  isControlFlowTag,
4455
4740
  isDefinitionFile,
4456
4741
  normalizePath,
4457
- parse
4742
+ parse,
4743
+ writeStyleModuleType
4458
4744
  });
package/dist/index.mjs CHANGED
@@ -1046,6 +1046,187 @@ function pushUniqueRange(ranges, start, end) {
1046
1046
  ranges.push({ start, end });
1047
1047
  }
1048
1048
 
1049
+ // src/util/find-style-selectors.ts
1050
+ function findStyleSelectors(code, offset = 0) {
1051
+ const result = /* @__PURE__ */ new Map();
1052
+ const pending = [];
1053
+ const braceStack = [];
1054
+ const parenStack = [];
1055
+ let bareGlobal = false;
1056
+ let pendingScope = null;
1057
+ const len = code.length;
1058
+ let i = 0;
1059
+ const blockGlobal = () => braceStack.length ? braceStack[braceStack.length - 1] : false;
1060
+ while (i < len) {
1061
+ const ch = code[i];
1062
+ const next = code[i + 1];
1063
+ if (ch === "/" && next === "*") {
1064
+ i += 2;
1065
+ while (i < len && !(code[i] === "*" && code[i + 1] === "/")) i++;
1066
+ i += 2;
1067
+ } else if (ch === "/" && next === "/") {
1068
+ i += 2;
1069
+ while (i < len && code[i] !== "\n") i++;
1070
+ } else if (ch === '"' || ch === "'") {
1071
+ i = skipString(code, i);
1072
+ } else if (isInterpolationStart(ch) && next === "{") {
1073
+ i = skipBalanced(code, i + 1);
1074
+ } else if (ch === ":") {
1075
+ let end = i + 1;
1076
+ while (end < len && isNameChar(code[end])) end++;
1077
+ const pseudo = code.slice(i + 1, end).toLowerCase();
1078
+ if (pseudo === "global" || pseudo === "local") {
1079
+ const isGlobal = pseudo === "global";
1080
+ if (code[end] === "(") {
1081
+ pendingScope = isGlobal;
1082
+ } else {
1083
+ let j = end;
1084
+ while (j < len && isWhitespace(code[j])) j++;
1085
+ if (code[j] === "{") {
1086
+ pendingScope = isGlobal;
1087
+ } else {
1088
+ bareGlobal = isGlobal;
1089
+ }
1090
+ }
1091
+ }
1092
+ i = end;
1093
+ } else if (ch === "(") {
1094
+ parenStack.push(
1095
+ pendingScope ?? (parenStack.length ? parenStack[parenStack.length - 1] : bareGlobal)
1096
+ );
1097
+ pendingScope = null;
1098
+ i++;
1099
+ } else if (ch === ")") {
1100
+ parenStack.pop();
1101
+ i++;
1102
+ } else if (ch === "." || ch === "#") {
1103
+ const start = i + 1;
1104
+ if (isNameStart(code[start])) {
1105
+ let end = start + 1;
1106
+ while (end < len && isNameChar(code[end])) end++;
1107
+ if (!(isInterpolationStart(code[end]) && code[end + 1] === "{")) {
1108
+ const global = parenStack.length ? parenStack[parenStack.length - 1] : bareGlobal;
1109
+ if (!global) pending.push({ start, end });
1110
+ }
1111
+ i = end;
1112
+ } else {
1113
+ i++;
1114
+ }
1115
+ } else if (ch === "{") {
1116
+ for (const range of pending) {
1117
+ const name = code.slice(range.start, range.end);
1118
+ const offsetRange = {
1119
+ start: range.start + offset,
1120
+ end: range.end + offset
1121
+ };
1122
+ const ranges = result.get(name);
1123
+ if (ranges) {
1124
+ ranges.push(offsetRange);
1125
+ } else {
1126
+ result.set(name, [offsetRange]);
1127
+ }
1128
+ }
1129
+ pending.length = 0;
1130
+ braceStack.push(pendingScope ?? blockGlobal());
1131
+ pendingScope = null;
1132
+ parenStack.length = 0;
1133
+ bareGlobal = blockGlobal();
1134
+ i++;
1135
+ } else if (ch === "}") {
1136
+ pending.length = 0;
1137
+ braceStack.pop();
1138
+ parenStack.length = 0;
1139
+ bareGlobal = blockGlobal();
1140
+ i++;
1141
+ } else if (ch === ";") {
1142
+ pending.length = 0;
1143
+ parenStack.length = 0;
1144
+ bareGlobal = blockGlobal();
1145
+ i++;
1146
+ } else if (ch === ",") {
1147
+ bareGlobal = blockGlobal();
1148
+ i++;
1149
+ } else {
1150
+ i++;
1151
+ }
1152
+ }
1153
+ return result;
1154
+ }
1155
+ function skipString(code, start) {
1156
+ const quote = code[start];
1157
+ const len = code.length;
1158
+ let i = start + 1;
1159
+ while (i < len) {
1160
+ const ch = code[i];
1161
+ if (ch === "\\") {
1162
+ i += 2;
1163
+ } else if (ch === quote) {
1164
+ return i + 1;
1165
+ } else {
1166
+ i++;
1167
+ }
1168
+ }
1169
+ return i;
1170
+ }
1171
+ function skipBalanced(code, openBrace) {
1172
+ const len = code.length;
1173
+ let depth = 0;
1174
+ let i = openBrace;
1175
+ while (i < len) {
1176
+ const ch = code[i];
1177
+ if (ch === "{") {
1178
+ depth++;
1179
+ } else if (ch === "}") {
1180
+ if (--depth === 0) return i + 1;
1181
+ }
1182
+ i++;
1183
+ }
1184
+ return i;
1185
+ }
1186
+ function isNameStart(ch) {
1187
+ if (ch === void 0) return false;
1188
+ const code = ch.charCodeAt(0);
1189
+ return code >= 97 && code <= 122 || // a-z
1190
+ code >= 65 && code <= 90 || // A-Z
1191
+ ch === "_" || ch === "-" || code >= 128;
1192
+ }
1193
+ function isNameChar(ch) {
1194
+ const code = ch.charCodeAt(0);
1195
+ return code >= 48 && code <= 57 || isNameStart(ch);
1196
+ }
1197
+ function isInterpolationStart(ch) {
1198
+ return ch === "#" || ch === "@" || ch === "$";
1199
+ }
1200
+ function isWhitespace(ch) {
1201
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f";
1202
+ }
1203
+
1204
+ // src/extractors/css-module/index.ts
1205
+ function extractCSSModule(opts) {
1206
+ const { code, fileName } = opts;
1207
+ const extractor = new Extractor(createSource(code, fileName));
1208
+ extractor.write("export default {} as unknown as ");
1209
+ writeStyleModuleType(extractor, findStyleSelectors(code));
1210
+ extractor.write(";\n");
1211
+ return extractor.end();
1212
+ }
1213
+ function writeStyleModuleType(extractor, selectors) {
1214
+ extractor.write("{");
1215
+ for (const ranges of selectors.values()) {
1216
+ extractor.write(' "').copy(ranges).write('": string;');
1217
+ }
1218
+ extractor.write(" }");
1219
+ }
1220
+ function createSource(code, fileName) {
1221
+ const lines = getLines(code);
1222
+ return {
1223
+ code,
1224
+ filename: normalizePath(fileName),
1225
+ positionAt: (offset) => getPosition(lines, offset),
1226
+ locationAt: (range) => getLocation(lines, range.start, range.end)
1227
+ };
1228
+ }
1229
+
1049
1230
  // src/extractors/html/keywords.ts
1050
1231
  var builtinTagsRegex = (
1051
1232
  /* cspell:disable-next-line */
@@ -2912,7 +3093,11 @@ constructor(_) {}
2912
3093
  if (tag.var) {
2913
3094
  this.#closeBrackets[this.#closeBrackets.length - 1]++;
2914
3095
  this.#extractor.write("{const ");
2915
- if (isHTML) {
3096
+ if (tagName === "style") {
3097
+ this.#extractor.copy(tag.var.value).write(" = ");
3098
+ this.#writeStyleModuleVar(tag);
3099
+ this.#extractor.write(";\n");
3100
+ } else if (isHTML) {
2916
3101
  this.#extractor.copy(tag.var.value).write(` = ${varShared("el")}(${JSON.stringify(def.name)});
2917
3102
  `);
2918
3103
  } else {
@@ -2952,6 +3137,40 @@ constructor(_) {}
2952
3137
  }
2953
3138
  }
2954
3139
  }
3140
+ /** Types a `<style/foo>` CSS module var as an object of its class/id names. */
3141
+ #writeStyleModuleVar(tag) {
3142
+ const selectors = this.#getStyleModuleSelectors(tag);
3143
+ if (this.#scriptLang === "ts" /* ts */) {
3144
+ this.#extractor.write(`${varShared("any")} as `);
3145
+ writeStyleModuleType(this.#extractor, selectors);
3146
+ } else {
3147
+ this.#extractor.write("/** @type {");
3148
+ writeStyleModuleType(this.#extractor, selectors);
3149
+ this.#extractor.write(`} */(${varShared("any")})`);
3150
+ }
3151
+ }
3152
+ #getStyleModuleSelectors(tag) {
3153
+ const result = /* @__PURE__ */ new Map();
3154
+ if (tag.body) {
3155
+ for (const child of tag.body) {
3156
+ if (child.type === 17 /* Text */) {
3157
+ const found = findStyleSelectors(
3158
+ this.#code.slice(child.start, child.end),
3159
+ child.start
3160
+ );
3161
+ for (const [name, ranges] of found) {
3162
+ const existing = result.get(name);
3163
+ if (existing) {
3164
+ existing.push(...ranges);
3165
+ } else {
3166
+ result.set(name, ranges);
3167
+ }
3168
+ }
3169
+ }
3170
+ }
3171
+ }
3172
+ return result;
3173
+ }
2955
3174
  #endChildren() {
2956
3175
  const pendingBrackets = this.#closeBrackets.pop();
2957
3176
  if (pendingBrackets) {
@@ -3935,7 +4154,9 @@ var processors_exports = {};
3935
4154
  __export(processors_exports, {
3936
4155
  create: () => create,
3937
4156
  extensions: () => extensions,
3938
- has: () => has
4157
+ getProcessorExtension: () => getProcessorExtension,
4158
+ has: () => has,
4159
+ isCSSModule: () => isCSSModule
3939
4160
  });
3940
4161
 
3941
4162
  // src/util/get-ext.ts
@@ -3944,6 +4165,34 @@ function getExt(fileName) {
3944
4165
  if (extIndex !== -1) return fileName.slice(extIndex);
3945
4166
  }
3946
4167
 
4168
+ // src/processors/css-module.ts
4169
+ var css_module_default = {
4170
+ extensions: [".module.css", ".module.scss", ".module.less"],
4171
+ create({ ts }) {
4172
+ return {
4173
+ getScriptExtension() {
4174
+ return ts.Extension.Ts;
4175
+ },
4176
+ getScriptKind() {
4177
+ return ts.ScriptKind.TS;
4178
+ },
4179
+ extract(fileName, code) {
4180
+ return extractCSSModule({ code, fileName });
4181
+ },
4182
+ print({ extracted }) {
4183
+ return { code: extracted.toString() };
4184
+ },
4185
+ printTypes({ sourceFile, printer }) {
4186
+ let code = "";
4187
+ for (const statement of sourceFile.statements) {
4188
+ code += printer.printNode(ts.EmitHint.Unspecified, statement, sourceFile) + "\n";
4189
+ }
4190
+ return { code };
4191
+ }
4192
+ };
4193
+ }
4194
+ };
4195
+
3947
4196
  // src/processors/marko.ts
3948
4197
  import path4 from "path";
3949
4198
 
@@ -4187,7 +4436,7 @@ function interopDefault(mod) {
4187
4436
  var isRemapExtensionReg = /\.ts$/;
4188
4437
  var skipRemapExtensionsReg = /\.(?:[cm]?jsx?|json|marko|css|less|sass|scss|styl|stylus|pcss|postcss|sss|a?png|jpe?g|jfif|pipeg|pjp|gif|svg|ico|web[pm]|avif|mp4|ogg|mp3|wav|flac|aac|opus|woff2?|eot|[ot]tf|webmanifest|pdf|txt)$/;
4189
4438
  var marko_default = {
4190
- extension: ".marko",
4439
+ extensions: [".marko"],
4191
4440
  create({ ts, host, configFile }) {
4192
4441
  const currentDirectory = host.getCurrentDirectory ? host.getCurrentDirectory() : ts.sys.getCurrentDirectory();
4193
4442
  const defaultScriptLang = configFile && /tsconfig/.test(configFile) ? "ts" /* ts */ : "js" /* js */;
@@ -4384,15 +4633,48 @@ function castType(type) {
4384
4633
  }
4385
4634
 
4386
4635
  // src/processors/index.ts
4387
- var extensions = [marko_default.extension];
4636
+ var configs = [marko_default, css_module_default];
4637
+ var simpleExtensions = /* @__PURE__ */ new Set();
4638
+ var compoundExtensions = [];
4639
+ for (const config of configs) {
4640
+ for (const extension of config.extensions) {
4641
+ if (extension.indexOf(".", 1) === -1) {
4642
+ simpleExtensions.add(extension);
4643
+ } else {
4644
+ compoundExtensions.push(extension);
4645
+ }
4646
+ }
4647
+ }
4648
+ var cssModuleExtensions = new Set(css_module_default.extensions);
4649
+ var extensions = configs.flatMap(
4650
+ (config) => config.extensions
4651
+ );
4388
4652
  function create(options) {
4389
- return {
4390
- [marko_default.extension]: marko_default.create(options)
4391
- };
4653
+ const result = {};
4654
+ for (const config of configs) {
4655
+ const processor = config.create(options);
4656
+ for (const extension of config.extensions) {
4657
+ result[extension] = processor;
4658
+ }
4659
+ }
4660
+ return result;
4392
4661
  }
4393
- function has(fileName) {
4662
+ function getProcessorExtension(fileName) {
4394
4663
  const ext = getExt(fileName);
4395
- return !!(ext && extensions.includes(ext));
4664
+ if (ext && simpleExtensions.has(ext)) return ext;
4665
+ if (compoundExtensions.length) {
4666
+ const lower = fileName.toLowerCase();
4667
+ for (const compound of compoundExtensions) {
4668
+ if (lower.endsWith(compound)) return compound;
4669
+ }
4670
+ }
4671
+ }
4672
+ function has(fileName) {
4673
+ return getProcessorExtension(fileName) !== void 0;
4674
+ }
4675
+ function isCSSModule(fileName) {
4676
+ const ext = getProcessorExtension(fileName);
4677
+ return ext !== void 0 && cssModuleExtensions.has(ext);
4396
4678
  }
4397
4679
 
4398
4680
  // src/util/is-definition-file.ts
@@ -4405,6 +4687,7 @@ export {
4405
4687
  project_exports as Project,
4406
4688
  ScriptLang,
4407
4689
  UNFINISHED,
4690
+ extractCSSModule,
4408
4691
  extractHTML,
4409
4692
  extractScript,
4410
4693
  extractStyle,
@@ -4415,5 +4698,6 @@ export {
4415
4698
  isControlFlowTag,
4416
4699
  isDefinitionFile,
4417
4700
  normalizePath,
4418
- parse
4701
+ parse,
4702
+ writeStyleModuleType
4419
4703
  };
package/dist/parser.d.ts CHANGED
@@ -43,7 +43,7 @@ export declare namespace Node {
43
43
  type AttrNode = AttrNamed | AttrSpread;
44
44
  type ControlFlowTag = Tag & {
45
45
  nameText: "if" | "else" | "else-if" | "for" | "while";
46
- bodyType: TagType.html;
46
+ bodyType: typeof TagType.html;
47
47
  };
48
48
  type ChildNode = Tag | AttrTag | Text | Doctype | Declaration | CDATA | Placeholder | Scriptlet;
49
49
  interface Commentable {
@@ -87,7 +87,7 @@ export declare namespace Node {
87
87
  open: Range;
88
88
  close: Range | undefined;
89
89
  nameText: string;
90
- bodyType: TagType.html;
90
+ bodyType: typeof TagType.html;
91
91
  name: OpenTagName;
92
92
  var: TagVar | undefined;
93
93
  args: TagArgs | undefined;
@@ -0,0 +1,15 @@
1
+ declare const _default: {
2
+ extensions: (".module.css" | ".module.scss" | ".module.less")[];
3
+ create({ ts }: import(".").CreateProcessorOptions): {
4
+ getScriptExtension(): import("typescript").Extension.Ts;
5
+ getScriptKind(): import("typescript").ScriptKind.TS;
6
+ extract(fileName: string, code: string): import("..").Extracted;
7
+ print({ extracted }: import(".").PrintContext): {
8
+ code: string;
9
+ };
10
+ printTypes({ sourceFile, printer }: import(".").PrintContext): {
11
+ code: string;
12
+ };
13
+ };
14
+ };
15
+ export default _default;
@@ -2,7 +2,7 @@ import type ts from "typescript/lib/tsserverlibrary";
2
2
  import { Extracted } from "../util/extractor";
3
3
  export type ProcessorExtension = `.${string}`;
4
4
  export interface ProcessorConfig {
5
- extension: ProcessorExtension;
5
+ extensions: ProcessorExtension[];
6
6
  create(options: CreateProcessorOptions): Processor;
7
7
  }
8
8
  export interface CreateProcessorOptions {
@@ -32,5 +32,9 @@ export interface PrintContext {
32
32
  formatSettings: Required<ts.FormatCodeSettings>;
33
33
  }
34
34
  export declare const extensions: ProcessorExtension[];
35
- export declare function create(options: CreateProcessorOptions): Record<ProcessorExtension, Processor>;
35
+ export declare function create(options: CreateProcessorOptions): Record<`.${string}`, Processor>;
36
+ /** Resolve a file's processor extension, including compound `.module.css`. */
37
+ export declare function getProcessorExtension(fileName: string): ProcessorExtension | undefined;
36
38
  export declare function has(fileName: string): boolean;
39
+ /** Whether a file is a CSS module (`.module.css`, `.module.scss`, `.module.less`). */
40
+ export declare function isCSSModule(fileName: string): boolean;
@@ -1,6 +1,6 @@
1
1
  import type ts from "typescript/lib/tsserverlibrary";
2
2
  declare const _default: {
3
- extension: ".marko";
3
+ extensions: ".marko"[];
4
4
  create({ ts, host, configFile }: import(".").CreateProcessorOptions): {
5
5
  getRootNames(): string[];
6
6
  getScriptExtension(fileName: string): ts.Extension.Ts | ts.Extension.Js;
@@ -1,4 +1,15 @@
1
- import { type Location, type Parsed, type Position, type Range } from "../parser";
1
+ import { type Location, type Position, type Range } from "../parser";
2
+ /**
3
+ * The minimal source an {@link Extractor} needs to maintain a source mapping.
4
+ * A Marko `Parsed` satisfies it, as does any other parsed source (eg a CSS
5
+ * module).
6
+ */
7
+ export interface ExtractorSource {
8
+ code: string;
9
+ filename: string;
10
+ positionAt(offset: number): Position;
11
+ locationAt(range: Range): Location;
12
+ }
2
13
  declare const enum Mapping {
3
14
  full = 0,
4
15
  alias = 1,
@@ -17,7 +28,7 @@ interface Token {
17
28
  */
18
29
  export declare class Extractor {
19
30
  #private;
20
- constructor(parsed: Parsed);
31
+ constructor(parsed: ExtractorSource);
21
32
  write(str: string): this;
22
33
  copy(range: Range | Range[] | string | false | void | undefined | null): this;
23
34
  anchor(range: Range | false | void | undefined | null): this;
@@ -25,8 +36,8 @@ export declare class Extractor {
25
36
  }
26
37
  export declare class Extracted {
27
38
  #private;
28
- parsed: Parsed;
29
- constructor(parsed: Parsed, generated: string, tokens: Token[]);
39
+ parsed: ExtractorSource;
40
+ constructor(parsed: ExtractorSource, generated: string, tokens: Token[]);
30
41
  sourceOffsetAt(generatedOffset: number): number | undefined;
31
42
  sourcePositionAt(generatedOffset: number): Position | undefined;
32
43
  sourceRangeAt(generatedStart: number, generatedEnd: number): {
@@ -0,0 +1,14 @@
1
+ import type { Range } from "../parser";
2
+ /**
3
+ * Collect class (`.name`) and id (`#name`) selectors, mapping each unique name
4
+ * to every source {@link Range} it was defined at. A name only counts when it
5
+ * appears in a prelude that ends with `{`; names in declaration values or
6
+ * at-rules (ending with `;`/`}`) are discarded, which keeps hex colors and
7
+ * numbers out. Names in `:global` scope -- `:global(.x)`, a bare `:global .x`,
8
+ * or a `:global { ... }` block (and their `:local` inverses) -- are excluded,
9
+ * matching what a CSS module actually exports. Comments, strings and
10
+ * interpolations are skipped. `offset` is added to every range so callers can
11
+ * scan a slice of a larger document (eg an embedded `<style>` block) in the
12
+ * original document's coordinate space.
13
+ */
14
+ export declare function findStyleSelectors(code: string, offset?: number): Map<string, Range[]>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marko/language-tools",
3
- "version": "2.5.63",
3
+ "version": "2.6.0",
4
4
  "description": "Marko Language Tools",
5
5
  "keywords": [
6
6
  "analysis",
@@ -37,15 +37,15 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@luxass/strip-json-comments": "^1.4.0",
40
- "@marko/compiler": "^5.39.63",
41
- "htmljs-parser": "^5.10.2",
40
+ "@marko/compiler": "^5.39.66",
41
+ "htmljs-parser": "^5.11.0",
42
42
  "relative-import-path": "^1.0.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/babel__code-frame": "^7.27.0",
46
46
  "@typescript/vfs": "^1.6.4",
47
- "marko": "^5.38.36",
47
+ "marko": "^5.39.11",
48
48
  "mitata": "^1.0.34",
49
- "tsx": "^4.21.0"
49
+ "tsx": "^4.22.4"
50
50
  }
51
51
  }