Extract textmate generator to helper library (#380)

This commit is contained in:
Nick Guerrera 2021-03-24 13:20:54 -07:00 коммит произвёл GitHub
Родитель 7034d33f78
Коммит 672cb699fa
11 изменённых файлов: 316 добавлений и 172 удалений

Просмотреть файл

@ -1,6 +1,23 @@
{
"name": "adl-vscode",
"entries": [
{
"version": "0.4.2",
"tag": "adl-vscode_v0.4.2",
"date": "Wed, 24 Mar 2021 18:40:21 GMT",
"comments": {
"patch": [
{
"comment": "Extract textmate generator to helper library"
}
],
"dependency": [
{
"comment": "Updating dependency \"@azure-tools/tmlanguage-generator\" from `0.1.0` to `0.1.1`"
}
]
}
},
{
"version": "0.4.1",
"tag": "adl-vscode_v0.4.1",

Просмотреть файл

@ -1,6 +1,13 @@
# Change Log - adl-vscode
This log was last generated on Tue, 23 Mar 2021 01:06:29 GMT and should not be manually modified.
This log was last generated on Wed, 24 Mar 2021 18:40:21 GMT and should not be manually modified.
## 0.4.2
Wed, 24 Mar 2021 18:40:21 GMT
### Patches
- Extract textmate generator to helper library
## 0.4.1
Tue, 23 Mar 2021 01:06:29 GMT

Просмотреть файл

@ -1,25 +1,15 @@
// TextMate-based syntax highlighting is implemented in this file.
// adl.tmLanguage.json is generated by running this script.
import fs from "fs";
import { promisify } from "util";
import { loadWASM, OnigRegExp } from "onigasm";
import * as tm from "@azure-tools/tmlanguage-generator";
import fs from "fs/promises";
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
type IncludeRule = tm.IncludeRule<ADLScope>;
type BeginEndRule = tm.BeginEndRule<ADLScope>;
type MatchRule = tm.MatchRule<ADLScope>;
type Grammar = tm.Grammar<ADLScope>;
const schema = "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json";
// Special scope that indicates a larger construct that doesn't get a single color.
// Expanded to meta.<key>.adl when we emit.
const meta = "<meta>";
/**
* The TextMate scope that gets assigned to a match and colored by a theme.
* See https://macromates.com/manual/en/language_grammars#naming_conventions
*/
type Scope =
| typeof meta
type ADLScope =
| "comment.block.adl"
| "comment.line.double-slash.adl"
| "constant.character.escape.adl"
@ -31,43 +21,7 @@ type Scope =
| "string.quoted.double.adl"
| "variable.name.adl";
interface RuleKey {
/** Rule's unique key through which identifies the rule in the repository. */
key: string;
}
interface RuleScope {
scope: Scope;
}
interface RulePatterns {
patterns: Rule[];
}
type Captures = Record<string, RuleScope | RulePatterns>;
type Rule = MatchRule | BeginEndRule | IncludeRule;
interface MatchRule extends RuleScope, RuleKey {
match: string;
captures?: Captures;
}
interface BeginEndRule extends RuleKey, RuleScope, Partial<RulePatterns> {
begin: string;
end: string;
beginCaptures?: Captures;
endCaptures?: Captures;
}
interface IncludeRule extends RuleKey, RulePatterns {}
interface Grammar extends RulePatterns {
$schema: typeof schema;
name: string;
scopeName: string;
fileTypes: string[];
}
const meta: typeof tm.meta = tm.meta;
const identifierStart = "[_$[:alpha:]]";
const identifierContinue = "[_$[:alnum:]]";
const beforeIdentifier = `(?=${identifierStart})`;
@ -309,130 +263,23 @@ const operationStatement: BeginEndRule = {
// expressions color acceptably as unclassified punctuation around those we do
// handle here.
expression.patterns = [token, parenthesizedExpression, modelExpression, identifierExpression];
statement.patterns = [token, decorator, modelStatement, namespaceStatement, operationStatement];
const grammar: Grammar = {
$schema: schema,
$schema: tm.schema,
name: "ADL",
scopeName: "source.adl",
fileTypes: [".adl"],
patterns: [statement],
};
/** Entry point, write the grammar to disk. */
async function main() {
const onigasm = await readFile("node_modules/onigasm/lib/onigasm.wasm");
await loadWASM(onigasm.buffer);
const filePath = "./dist/adl.tmlanguage.json";
const json = emit(grammar);
await writeFile(filePath, json);
const json = await tm.emitJSON(grammar);
await fs.writeFile(filePath, json);
}
/** Emit the grammar to a JSON string matching tmlanguage.json schema. */
function emit(grammar: Grammar): string {
const indent = 2;
const processed = processGrammar(grammar);
return JSON.stringify(processed, undefined, indent);
}
/**
* Convert the grammar from our more convenient representation to the
* tmlanguage.json schema. Perform some validation in the process.
*/
function processGrammar(grammar: Grammar): any {
// key is rule.key, value is [unprocessed rule, processed rule]. unprocessed
// rule is used for its identity to check for duplicates and deal with cycles.
const repository = new Map<string, [Rule, any]>();
const output = processNode(grammar);
output.repository = processRepository();
return output;
function processNode(node: any): any {
if (typeof node !== "object") {
return node;
}
if (Array.isArray(node)) {
return node.map(processNode);
}
const output: any = {};
for (const key in node) {
const value = node[key];
switch (key) {
case "key":
// Drop it. It was used to place the node in the repository, and does
// not need to be retained on the node in the final structure.
break;
case "scope":
// tmlanguage uses "name" confusingly for scope. We avoid "name" which
// can be confused with the repository key.
output.name = value === meta ? `meta.${node.key}.adl` : value;
break;
case "begin":
case "end":
case "match":
validateRegexp(value, node, key);
output[key] = value;
break;
case "patterns":
output[key] = processPatterns(value);
break;
default:
output[key] = processNode(value);
break;
}
}
return output;
}
function processPatterns(rules: Rule[]) {
for (const rule of rules) {
if (!repository.has(rule.key)) {
// put placeholder first to prevent cycles
const entry: [Rule, any] = [rule, undefined];
repository.set(rule.key, entry);
// fill placeholder with processed node.
entry[1] = processNode(rule);
} else if (repository.get(rule.key)![0] !== rule) {
throw new Error("Duplicate key: " + rule.key);
}
}
return rules.map((r) => ({ include: `#${r.key}` }));
}
function processRepository() {
const output: any = {};
for (const key of [...repository.keys()].sort()) {
output[key] = repository.get(key)![1];
}
return output;
}
function validateRegexp(regexp: string, node: any, prop: string) {
try {
new OnigRegExp(regexp).testSync("");
} catch (err) {
let message: string = err.message;
if (/^[0-9,]+/.test(message)) {
// Work around for https://github.com/NeekSandhu/onigasm/issues/26
const array = new Uint8Array(message.split(",").map((s: string) => Number(s)));
const buffer = Buffer.from(array);
message = buffer.toString("utf-8");
}
console.error(`Error: Bad regex: ${JSON.stringify({ [prop]: regexp })}`);
console.error(`Error: ${message}`);
console.error();
console.error("Context:");
console.dir(node);
console.error();
throw err;
}
}
}
main()
.then()
.catch((err) => {
console.log(err.stack);
});
main().catch((err) => {
console.log(err.stack);
process.exit(1);
});

Просмотреть файл

@ -8,7 +8,7 @@
"url": "git+https://github.com/azure/adl"
},
"publisher": "Microsoft",
"version": "0.4.1",
"version": "0.4.2",
"engines": {
"vscode": "^1.53.0"
},
@ -24,7 +24,7 @@
"ThirdPartyNotices.txt"
],
"scripts": {
"build": "npm run compile && npm run generate-tmlanguage && npm run rollup && npm run generate-third-party-notices && npm run package-vsix",
"build": "npm run compile && npm run rollup && npm run generate-tmlanguage && npm run generate-third-party-notices && npm run package-vsix",
"prepare": "npm run build",
"compile": "tsc -p .",
"watch": "tsc -p . --watch",
@ -42,7 +42,7 @@
"@rollup/plugin-node-resolve": "^11.2.0",
"@types/node": "14.0.27",
"@types/vscode": "^1.53.0",
"onigasm": "~2.2.5",
"@azure-tools/tmlanguage-generator": "0.1.1",
"rollup": "^2.41.4",
"typescript": "~4.1.5",
"vsce": "^1.85.1",

Просмотреть файл

@ -0,0 +1,17 @@
{
"name": "@azure-tools/tmlanguage-generator",
"entries": [
{
"version": "0.1.1",
"tag": "@azure-tools/tmlanguage-generator_v0.1.1",
"date": "Wed, 24 Mar 2021 18:40:21 GMT",
"comments": {
"patch": [
{
"comment": "Initial release"
}
]
}
}
]
}

Просмотреть файл

@ -0,0 +1,11 @@
# Change Log - @azure-tools/textmate
This log was last generated on Wed, 24 Mar 2021 18:40:21 GMT and should not be manually modified.
## 0.1.1
Wed, 24 Mar 2021 18:40:21 GMT
### Patches
- Initial release

Просмотреть файл

@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE

Просмотреть файл

@ -0,0 +1 @@
# Helper library for generating TextMate syntax highlighting files

Просмотреть файл

@ -0,0 +1,173 @@
import { loadWASM, OnigRegExp } from "onigasm";
import { readFile } from "fs/promises";
import { resolve } from "path";
export const schema =
"https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json";
/**
* Special scope that indicates a larger construct that doesn't get a single color.
* Expanded to meta.<key>.<grammar name> during emit.
*/
export const meta = Symbol("meta");
export interface RuleKey {
/** Rule's unique key through which identifies the rule in the repository. */
key: string;
}
export interface RuleScope<Scope extends string = string> {
/**
* The TextMate scope that gets assigned to a match and colored by a theme.
* See https://macromates.com/manual/en/language_grammars#naming_conventions
*/
scope: Scope | typeof meta;
}
export interface RulePatterns<Scope extends string = string> {
patterns: Rule<Scope>[];
}
export type Captures<Scope extends string = string> = Record<
string,
RuleScope<Scope> | RulePatterns<Scope>
>;
export type Rule<Scope extends string = string> =
| MatchRule<Scope>
| BeginEndRule<Scope>
| IncludeRule<Scope>;
export interface MatchRule<Scope extends string = string> extends RuleScope<Scope>, RuleKey {
match: string;
captures?: Captures<Scope>;
}
export interface BeginEndRule<Scope extends string = string>
extends RuleKey,
RuleScope<Scope>,
Partial<RulePatterns<Scope>> {
begin: string;
end: string;
beginCaptures?: Captures<Scope>;
endCaptures?: Captures<Scope>;
}
export interface IncludeRule<Scope extends string = string> extends RuleKey, RulePatterns<Scope> {}
export interface Grammar<Scope extends string = string> extends RulePatterns<Scope> {
$schema: typeof schema;
name: string;
scopeName: string;
fileTypes: string[];
}
let initialized = false;
async function initialize() {
if (!initialized) {
const path = resolve(__dirname, "../node_modules/onigasm/lib/onigasm.wasm");
const wasm = await readFile(path);
await loadWASM(wasm.buffer);
initialized = true;
}
}
/**
* Emit the given grammar to JSON.
*/
export async function emitJSON(grammar: Grammar): Promise<string> {
await initialize();
const indent = 2;
const processed = processGrammar(grammar);
return JSON.stringify(processed, undefined, indent);
}
/**
* Convert the grammar from our more convenient representation to the
* tmlanguage.json schema. Perform some validation in the process.
*/
function processGrammar(grammar: Grammar): any {
// key is rule.key, value is [unprocessed rule, processed rule]. unprocessed
// rule is used for its identity to check for duplicates and deal with cycles.
const repository = new Map<string, [Rule, any]>();
const output = processNode(grammar);
output.repository = processRepository();
return output;
function processNode(node: any): any {
if (typeof node !== "object") {
return node;
}
if (Array.isArray(node)) {
return node.map(processNode);
}
const output: any = {};
for (const key in node) {
const value = node[key];
switch (key) {
case "key":
// Drop it. It was used to place the node in the repository, and does
// not need to be retained on the node in the final structure.
break;
case "scope":
// tmlanguage uses "name" confusingly for scope. We avoid "name" which
// can be confused with the repository key.
output.name = value === meta ? `meta.${node.key}.adl` : value;
break;
case "begin":
case "end":
case "match":
validateRegexp(value, node, key);
output[key] = value;
break;
case "patterns":
output[key] = processPatterns(value);
break;
default:
output[key] = processNode(value);
break;
}
}
return output;
}
function processPatterns(rules: Rule[]) {
for (const rule of rules) {
if (!repository.has(rule.key)) {
// put placeholder first to prevent cycles
const entry: [Rule, any] = [rule, undefined];
repository.set(rule.key, entry);
// fill placeholder with processed node.
entry[1] = processNode(rule);
} else if (repository.get(rule.key)![0] !== rule) {
throw new Error("Duplicate key: " + rule.key);
}
}
return rules.map((r) => ({ include: `#${r.key}` }));
}
function processRepository() {
const output: any = {};
for (const key of [...repository.keys()].sort()) {
output[key] = repository.get(key)![1];
}
return output;
}
function validateRegexp(regexp: string, node: any, prop: string) {
try {
new OnigRegExp(regexp).testSync("");
} catch (err) {
if (/^[0-9,]+/.test(err.message)) {
// Work around for https://github.com/NeekSandhu/onigasm/issues/26
const array = new Uint8Array(err.message.split(",").map((s: string) => Number(s)));
const buffer = Buffer.from(array);
err = new Error(buffer.toString("utf-8"));
}
console.error(`Error: Bad regex: ${JSON.stringify({ [prop]: regexp })} in:`);
console.error(node);
throw err;
}
}
}

Просмотреть файл

@ -0,0 +1,40 @@
{
"name": "@azure-tools/tmlanguage-generator",
"version": "0.1.1",
"author": "Microsoft Corporation",
"description": "Helper library to generate TextMate syntax highlighting tmlanguage files.",
"homepage": "https://github.com/Azure/adl/tree/master/packages/textmate",
"readme": "https://github.com/Azure/adl/blob/master/packages/textmate/REAMDE.md",
"keywords": [
"textmate",
"tmlanguage"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/Azure/adl.git"
},
"bugs": {
"url": "https://github.com/Azure/adl/issues"
},
"main": "dist/index.js",
"engines": {
"node": ">=14.0.0"
},
"scripts": {
"build": "tsc -p .",
"prepare": "npm run build",
"watch": "tsc -p . --watch",
"check-format": "prettier --list-different --config ../../.prettierrc.json --ignore-path ../../.prettierignore \"**/*.{ts,js,json}\"",
"format": "prettier --write --config ../../.prettierrc.json --ignore-path ../../.prettierignore \"**/*.{ts,js,json}\""
},
"files": [
"dist/**"
],
"dependencies": {
"onigasm": "~2.2.5"
},
"devDependencies": {
"@types/node": "14.0.27"
}
}

Просмотреть файл

@ -0,0 +1,10 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": ".",
"types": ["node"]
},
"include": ["./**/*.ts"],
"exclude": ["dist", "node_modules", "**/*.d.ts"]
}