Add types to TextMate gulp step

This commit is contained in:
Koen Vlaswinkel
2024-02-14 14:26:40 +01:00
parent 9e2b16afe3
commit 3cb92233ac
3 changed files with 158 additions and 41 deletions

View File

@@ -121,15 +121,6 @@ module.exports = {
}, },
}, },
}, },
{
files: ["test/**/*"],
parserOptions: {
project: resolve(__dirname, "test/tsconfig.json"),
},
env: {
jest: true,
},
},
{ {
files: ["test/vscode-tests/**/*"], files: ["test/vscode-tests/**/*"],
parserOptions: { parserOptions: {
@@ -156,6 +147,18 @@ module.exports = {
], ],
}, },
}, },
{
files: ["test/**/*"],
parserOptions: {
project: resolve(__dirname, "test/tsconfig.json"),
},
env: {
jest: true,
},
rules: {
"@typescript-eslint/no-explicit-any": "off",
},
},
{ {
files: [ files: [
".eslintrc.js", ".eslintrc.js",
@@ -188,11 +191,5 @@ module.exports = {
"import/no-namespace": ["error", { ignore: ["react"] }], "import/no-namespace": ["error", { ignore: ["react"] }],
}, },
}, },
{
files: ["test/**/*", "gulpfile.ts/**/*"],
rules: {
"@typescript-eslint/no-explicit-any": "off",
},
},
], ],
}; };

View File

@@ -0,0 +1,91 @@
/**
* A subset of the standard TextMate grammar that is used by our transformation
* step. For a full JSON schema, see:
* https://github.com/martinring/tmlanguage/blob/478ad124a21933cd4b0b65f1ee7ee18ee1f87473/tmlanguage.json
*/
export interface TextmateGrammar {
patterns: Pattern[];
repository?: Record<string, Pattern>;
}
/**
* The extended TextMate grammar as used by our transformation step. This is a superset of the
* standard TextMate grammar, and includes additional fields that are used by our transformation
* step.
*
* Any comment of the form `(?#ref-id)` in a `match`, `begin`, or `end` property will be replaced
* with the match text of the rule named "ref-id". If the rule named "ref-id" consists of just a
* `patterns` property with a list of `include` directives, the replacement pattern is the
* disjunction of the match patterns of all of the included rules.
*/
export interface ExtendedTextmateGrammar<MatchType = string> {
/**
* This represents the set of regular expression options to apply to all regular
* expressions throughout the file.
*/
regexOptions?: string;
/**
* This element defines a map of macro names to replacement text. When a `match`, `begin`, or
* `end` property has a value that is a single-key map, the value is replaced with the value of the
* macro named by the key, with any use of `(?#)` in the macro text replaced with the text of the
* value of the key, surrounded by a non-capturing group (`(?:)`). For example:
*
* The `beginPattern` and `endPattern` Properties
* A rule can have a `beginPattern` or `endPattern` property whose value is a reference to another
* rule (e.g. `#other-rule`). The `beginPattern` property is replaced as follows:
*
* my-rule:
* beginPattern: '#other-rule'
*
* would be transformed to
*
* my-rule:
* begin: '(?#other-rule)'
* beginCaptures:
* '0':
* patterns:
* - include: '#other-rule'
*
* An `endPattern` property is transformed similary.
*
* macros:
* repeat: '(?#)*'
* repository:
* multi-letter:
* match:
* repeat: '[A-Za-z]'
* name: scope.multi-letter
*
* would be transformed to
*
* repository:
* multi-letter:
* match: '(?:[A-Za-z])*'
* name: scope.multi-letter
*/
macros?: Record<string, string>;
patterns: Array<Pattern<MatchType>>;
repository?: Record<string, Pattern<MatchType>>;
}
export interface Pattern<MatchType = string> {
include?: string;
match?: MatchType;
begin?: MatchType;
end?: MatchType;
while?: MatchType;
captures?: Record<string, PatternCapture>;
beginCaptures?: Record<string, PatternCapture>;
endCaptures?: Record<string, PatternCapture>;
patterns?: Array<Pattern<MatchType>>;
beginPattern?: string;
endPattern?: string;
}
export interface PatternCapture {
name?: string;
patterns?: Pattern[];
}
export type ExtendedMatchType = string | Record<string, string>;

View File

@@ -3,6 +3,12 @@ import { load } from "js-yaml";
import { obj } from "through2"; import { obj } from "through2";
import PluginError from "plugin-error"; import PluginError from "plugin-error";
import type Vinyl from "vinyl"; import type Vinyl from "vinyl";
import type {
ExtendedMatchType,
ExtendedTextmateGrammar,
Pattern,
TextmateGrammar,
} from "./textmate-grammar";
/** /**
* Replaces all rule references with the match pattern of the referenced rule. * Replaces all rule references with the match pattern of the referenced rule.
@@ -34,7 +40,9 @@ function replaceReferencesWithStrings(
* @param yaml The root of the YAML document. * @param yaml The root of the YAML document.
* @returns A map from macro name to replacement text. * @returns A map from macro name to replacement text.
*/ */
function gatherMacros(yaml: any): Map<string, string> { function gatherMacros<T>(
yaml: ExtendedTextmateGrammar<T>,
): Map<string, string> {
const macros = new Map<string, string>(); const macros = new Map<string, string>();
for (const key in yaml.macros) { for (const key in yaml.macros) {
macros.set(key, yaml.macros[key]); macros.set(key, yaml.macros[key]);
@@ -51,7 +59,7 @@ function gatherMacros(yaml: any): Map<string, string> {
* @returns The match text for the rule. This is either the value of the rule's `match` property, * @returns The match text for the rule. This is either the value of the rule's `match` property,
* or the disjunction of the match text of all of the other rules `include`d by this rule. * or the disjunction of the match text of all of the other rules `include`d by this rule.
*/ */
function getNodeMatchText(rule: any): string { function getNodeMatchText(rule: Pattern): string {
if (rule.match !== undefined) { if (rule.match !== undefined) {
// For a match string, just use that string as the replacement. // For a match string, just use that string as the replacement.
return rule.match; return rule.match;
@@ -78,7 +86,7 @@ function getNodeMatchText(rule: any): string {
* @returns A map whose keys are the names of rules, and whose values are the corresponding match * @returns A map whose keys are the names of rules, and whose values are the corresponding match
* text of each rule. * text of each rule.
*/ */
function gatherMatchTextForRules(yaml: any): Map<string, string> { function gatherMatchTextForRules(yaml: TextmateGrammar): Map<string, string> {
const replacements = new Map<string, string>(); const replacements = new Map<string, string>();
for (const key in yaml.repository) { for (const key in yaml.repository) {
const node = yaml.repository[key]; const node = yaml.repository[key];
@@ -94,9 +102,14 @@ function gatherMatchTextForRules(yaml: any): Map<string, string> {
* @param yaml The root of the YAML document. * @param yaml The root of the YAML document.
* @param action Callback to invoke on each rule. * @param action Callback to invoke on each rule.
*/ */
function visitAllRulesInFile(yaml: any, action: (rule: any) => void) { function visitAllRulesInFile<T>(
yaml: ExtendedTextmateGrammar<T>,
action: (rule: Pattern<T>) => void,
) {
visitAllRulesInRuleMap(yaml.patterns, action); visitAllRulesInRuleMap(yaml.patterns, action);
visitAllRulesInRuleMap(yaml.repository, action); if (yaml.repository) {
visitAllRulesInRuleMap(Object.values(yaml.repository), action);
}
} }
/** /**
@@ -107,9 +120,11 @@ function visitAllRulesInFile(yaml: any, action: (rule: any) => void) {
* @param ruleMap The map or array of rules to visit. * @param ruleMap The map or array of rules to visit.
* @param action Callback to invoke on each rule. * @param action Callback to invoke on each rule.
*/ */
function visitAllRulesInRuleMap(ruleMap: any, action: (rule: any) => void) { function visitAllRulesInRuleMap<T>(
for (const key in ruleMap) { ruleMap: Array<Pattern<T>>,
const rule = ruleMap[key]; action: (rule: Pattern<T>) => void,
) {
for (const rule of ruleMap) {
if (typeof rule === "object") { if (typeof rule === "object") {
action(rule); action(rule);
if (rule.patterns !== undefined) { if (rule.patterns !== undefined) {
@@ -125,16 +140,22 @@ function visitAllRulesInRuleMap(ruleMap: any, action: (rule: any) => void) {
* @param rule The rule whose matches are to be transformed. * @param rule The rule whose matches are to be transformed.
* @param action The transformation to make on each match pattern. * @param action The transformation to make on each match pattern.
*/ */
function visitAllMatchesInRule(rule: any, action: (match: any) => any) { function visitAllMatchesInRule<T>(rule: Pattern<T>, action: (match: T) => T) {
for (const key in rule) { for (const key in rule) {
switch (key) { switch (key) {
case "begin": case "begin":
case "end": case "end":
case "match": case "match":
case "while": case "while": {
rule[key] = action(rule[key]); const ruleElement = rule[key];
break;
if (!ruleElement) {
continue;
}
rule[key] = action(ruleElement);
break;
}
default: default:
break; break;
} }
@@ -148,14 +169,17 @@ function visitAllMatchesInRule(rule: any, action: (match: any) => any) {
* @param rule Rule to be transformed. * @param rule Rule to be transformed.
* @param key Base key of the property to be transformed. * @param key Base key of the property to be transformed.
*/ */
function expandPatternMatchProperties(rule: any, key: "begin" | "end") { function expandPatternMatchProperties<T>(
const patternKey = `${key}Pattern`; rule: Pattern<T>,
const capturesKey = `${key}Captures`; key: "begin" | "end",
) {
const patternKey = `${key}Pattern` as const;
const capturesKey = `${key}Captures` as const;
const pattern = rule[patternKey]; const pattern = rule[patternKey];
if (pattern !== undefined) { if (pattern !== undefined) {
const patterns: string[] = Array.isArray(pattern) ? pattern : [pattern]; const patterns: string[] = Array.isArray(pattern) ? pattern : [pattern];
rule[key] = patterns.map((p) => `((?${p}))`).join("|"); rule[key] = patterns.map((p) => `((?${p}))`).join("|") as T;
const captures: { [index: string]: any } = {}; const captures: Pattern["captures"] = {};
for (const patternIndex in patterns) { for (const patternIndex in patterns) {
captures[(Number(patternIndex) + 1).toString()] = { captures[(Number(patternIndex) + 1).toString()] = {
patterns: [ patterns: [
@@ -175,7 +199,7 @@ function expandPatternMatchProperties(rule: any, key: "begin" | "end") {
* *
* @param yaml The root of the YAML document. * @param yaml The root of the YAML document.
*/ */
function transformFile(yaml: any) { function transformFile(yaml: ExtendedTextmateGrammar<ExtendedMatchType>) {
const macros = gatherMacros(yaml); const macros = gatherMacros(yaml);
visitAllRulesInFile(yaml, (rule) => { visitAllRulesInFile(yaml, (rule) => {
expandPatternMatchProperties(rule, "begin"); expandPatternMatchProperties(rule, "begin");
@@ -198,24 +222,29 @@ function transformFile(yaml: any) {
yaml.macros = undefined; yaml.macros = undefined;
const replacements = gatherMatchTextForRules(yaml); // We have removed all object match properties, so we don't have an extended match type anymore.
const macrolessYaml = yaml as ExtendedTextmateGrammar;
const replacements = gatherMatchTextForRules(macrolessYaml);
// Expand references in matches. // Expand references in matches.
visitAllRulesInFile(yaml, (rule) => { visitAllRulesInFile(macrolessYaml, (rule) => {
visitAllMatchesInRule(rule, (match) => { visitAllMatchesInRule(rule, (match) => {
return replaceReferencesWithStrings(match, replacements); return replaceReferencesWithStrings(match, replacements);
}); });
}); });
if (yaml.regexOptions !== undefined) { if (macrolessYaml.regexOptions !== undefined) {
const regexOptions = `(?${yaml.regexOptions})`; const regexOptions = `(?${macrolessYaml.regexOptions})`;
visitAllRulesInFile(yaml, (rule) => { visitAllRulesInFile(macrolessYaml, (rule) => {
visitAllMatchesInRule(rule, (match) => { visitAllMatchesInRule(rule, (match) => {
return regexOptions + match; return regexOptions + match;
}); });
}); });
yaml.regexOptions = undefined; macrolessYaml.regexOptions = undefined;
} }
return macrolessYaml;
} }
export function transpileTextMateGrammar() { export function transpileTextMateGrammar() {
@@ -230,8 +259,8 @@ export function transpileTextMateGrammar() {
} else if (file.isBuffer()) { } else if (file.isBuffer()) {
const buf: Buffer = file.contents; const buf: Buffer = file.contents;
const yamlText: string = buf.toString("utf8"); const yamlText: string = buf.toString("utf8");
const jsonData: any = load(yamlText); const yamlData = load(yamlText) as TextmateGrammar;
transformFile(jsonData); const jsonData = transformFile(yamlData);
file.contents = Buffer.from(JSON.stringify(jsonData, null, 2), "utf8"); file.contents = Buffer.from(JSON.stringify(jsonData, null, 2), "utf8");
file.extname = ".json"; file.extname = ".json";