first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# @n8n/codemirror-lang
|
||||
|
||||
Language support package for CodeMirror 6 in n8n
|
||||
|
||||
[n8n Expression Language support](./src/expressions/README.md)
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
|
||||
export default defineConfig(baseConfig, globalIgnores(['src/expressions/grammar*.ts']), {
|
||||
rules: {
|
||||
// TODO: Remove this
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
'no-useless-escape': 'warn',
|
||||
'@typescript-eslint/unbound-method': 'warn',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "@n8n/codemirror-lang",
|
||||
"version": "0.5.0",
|
||||
"description": "Language support package for CodeMirror 6 in n8n",
|
||||
"sideEffects": false,
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Iván Ovejero",
|
||||
"email": "ivov.src@gmail.com",
|
||||
"url": "https://ivov.dev"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/n8n-io/n8n",
|
||||
"directory": "packages/@n8n/codemirror-lang"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"module": "src/index.ts",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"require": "./dist/index.js",
|
||||
"import": "./src/index.ts"
|
||||
},
|
||||
"./*": "./*"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"generate:expressions:grammar": "lezer-generator --typeScript --output src/expressions/grammar.ts src/expressions/expressions.grammar",
|
||||
"generate": "pnpm generate:expressions:grammar && pnpm format",
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"test": "vitest run",
|
||||
"test:unit": "vitest run",
|
||||
"lint": "eslint . --quiet",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "biome format --write src test",
|
||||
"format:check": "biome ci src test"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codemirror/language": "catalog:",
|
||||
"@lezer/highlight": "catalog:",
|
||||
"@lezer/lr": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lezer/generator": "catalog:",
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
"@n8n/vitest-config": "workspace:*",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
# n8n Expression language support
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import { parserWithMetaData as n8nParser } from '@n8n/codemirror-lang';
|
||||
import { LanguageSupport, LRLanguage } from '@codemirror/language';
|
||||
import { parseMixed } from '@lezer/common';
|
||||
import { parser as jsParser } from '@lezer/javascript';
|
||||
|
||||
const n8nPlusJsParser = n8nParser.configure({
|
||||
wrap: parseMixed((node) => {
|
||||
if (node.type.isTop) return null;
|
||||
|
||||
return node.name === 'Resolvable'
|
||||
? { parser: jsParser, overlay: (node) => node.type.name === 'Resolvable' }
|
||||
: null;
|
||||
}),
|
||||
});
|
||||
|
||||
const n8nLanguage = LRLanguage.define({ parser: n8nPlusJsParser });
|
||||
|
||||
export function n8nExpressionLanguageSupport() {
|
||||
return new LanguageSupport(n8nLanguage);
|
||||
}
|
||||
```
|
||||
|
||||
## Supported Unicode ranges
|
||||
|
||||
- From `Basic Latin` up to and including `Currency Symbols`
|
||||
- `Miscellaneous Symbols and Pictographs`
|
||||
- `CJK Unified Ideographs`
|
||||
@@ -0,0 +1,21 @@
|
||||
@top Program { entity* }
|
||||
|
||||
entity { Plaintext | Resolvable }
|
||||
|
||||
@tokens {
|
||||
Plaintext { ![{] Plaintext? | "{" (@eof | ![{] Plaintext?) }
|
||||
|
||||
OpenMarker[closedBy="CloseMarker"] { "{{" }
|
||||
|
||||
CloseMarker[openedBy="OpenMarker"] { "}}" }
|
||||
|
||||
Resolvable {
|
||||
OpenMarker resolvableChar* CloseMarker
|
||||
}
|
||||
|
||||
resolvableChar { unicodeChar | "}" ![}] | "\\}}" }
|
||||
|
||||
unicodeChar { $[\u0000-\u007C] | $[\u007E-\u{10FFFF}] }
|
||||
}
|
||||
|
||||
@detectDelim
|
||||
@@ -0,0 +1,4 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
export const Program = 1,
|
||||
Plaintext = 2,
|
||||
Resolvable = 3;
|
||||
@@ -0,0 +1,17 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import { LRParser } from '@lezer/lr';
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states: "nQQOPOOOOOO'#Cb'#CbOOOO'#C`'#C`QQOPOOOOOO-E6^-E6^",
|
||||
stateData: 'Y~OQPORPO~O',
|
||||
goto: 'bVPPPPWP^QRORSRTQOR',
|
||||
nodeNames: '⚠ Program Plaintext Resolvable',
|
||||
maxTerm: 6,
|
||||
skippedNodes: [0],
|
||||
repeatNodeCount: 1,
|
||||
tokenData:
|
||||
"%o~RTO#ob#o#p!h#p;'Sb;'S;=`!]<%lOb~gTQ~O#ob#o#pv#p;'Sb;'S;=`!]<%lOb~yUO#ob#p;'Sb;'S;=`!]<%l~b~Ob~~!c~!`P;=`<%lb~!hOQ~~!kVO#ob#o#p#Q#p;'Sb;'S;=`!]<%l~b~Ob~~!c~#TVO#O#Q#O#P#j#P#q#Q#q#r%Q#r;'S#Q;'S;=`%i<%lO#Q~#mVO#O#Q#O#P#j#P#q#Q#q#r$S#r;'S#Q;'S;=`%i<%lO#Q~$VTO#q#Q#q#r$f#r;'S#Q;'S;=`%i<%lO#Q~$kVR~O#O#Q#O#P#j#P#q#Q#q#r%Q#r;'S#Q;'S;=`%i<%lO#Q~%TTO#q#Q#q#r%d#r;'S#Q;'S;=`%i<%lO#Q~%iOR~~%lP;=`<%l#Q",
|
||||
tokenizers: [0],
|
||||
topRules: { Program: [0, 1] },
|
||||
tokenPrec: 0,
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { LRLanguage, LanguageSupport, foldNodeProp, foldInside } from '@codemirror/language';
|
||||
import { styleTags, tags as t } from '@lezer/highlight';
|
||||
|
||||
import { parser } from './grammar';
|
||||
|
||||
export const expressionParser = parser;
|
||||
|
||||
export const parserWithMetaData = parser.configure({
|
||||
props: [
|
||||
foldNodeProp.add({
|
||||
Application: foldInside,
|
||||
}),
|
||||
styleTags({
|
||||
OpenMarker: t.brace,
|
||||
CloseMarker: t.brace,
|
||||
Plaintext: t.content,
|
||||
Resolvable: t.string,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export const n8nLanguage = LRLanguage.define({
|
||||
parser: parserWithMetaData,
|
||||
languageData: {
|
||||
commentTokens: { line: ';' },
|
||||
},
|
||||
});
|
||||
|
||||
export function n8nExpression() {
|
||||
return new LanguageSupport(n8nLanguage);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { parserWithMetaData, n8nLanguage, expressionParser } from './expressions';
|
||||
@@ -0,0 +1,326 @@
|
||||
# Resolvable
|
||||
|
||||
{{ 1 + 1 }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Empty Resolvable
|
||||
|
||||
{{}}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Resolvable of only whitespace
|
||||
|
||||
{{ }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# No content
|
||||
|
||||
|
||||
|
||||
==>
|
||||
|
||||
Program
|
||||
|
||||
# Plaintext
|
||||
|
||||
text
|
||||
|
||||
==>
|
||||
|
||||
Program(Plaintext)
|
||||
|
||||
# Plaintext of single-brace-wrapped text
|
||||
|
||||
{text}
|
||||
|
||||
==>
|
||||
|
||||
Program(Plaintext)
|
||||
|
||||
# Plaintext then Resolvable
|
||||
|
||||
text {{ 1 + 1 }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Plaintext, Resolvable)
|
||||
|
||||
# Resolvable then Plaintext
|
||||
|
||||
{{ 1 + 1 }} Plaintext
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable, Plaintext)
|
||||
|
||||
# Plaintext then Resolvable then Plaintext
|
||||
|
||||
text {{ 1 + 1 }} text
|
||||
|
||||
==>
|
||||
|
||||
Program(Plaintext, Resolvable, Plaintext)
|
||||
|
||||
# Resolvable then Plaintext then Resolvable
|
||||
|
||||
{{ 1 + 1 }} text {{ 1 + 1 }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable,Plaintext,Resolvable)
|
||||
|
||||
# Plaintext then Resolvable then Plaintext then Resolvable
|
||||
|
||||
text {{ 1 + 1 }} text {{ 1 + 1 }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Plaintext, Resolvable, Plaintext, Resolvable)
|
||||
|
||||
# Resolvable then Plaintext then Resolvable then Plaintext
|
||||
|
||||
{{ 1 + 1 }} text {{ 1 + 1 }} text
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable,Plaintext,Resolvable,Plaintext)
|
||||
|
||||
# Resolvable containing all resolvable chars
|
||||
|
||||
{{ he ()[]{<>~`!@#$%^&*-_+=|\;:'",./?\{ llo }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Resolvable containing single left brace
|
||||
|
||||
{{ he { llo }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Resolvable containing double left brace
|
||||
|
||||
{{ he {{ llo }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Resolvable containing triple left brace
|
||||
|
||||
{{ he {{{ llo }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Resolvable containing single right brace
|
||||
|
||||
{{ he } llo }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Resolvable containing escaped double right brace
|
||||
|
||||
{{ he \}} llo }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Resolvable containing escaped triple right brace
|
||||
|
||||
{{ he \}}} llo }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Resolvable containing single-brace-wrapped text with escaping
|
||||
|
||||
{{ he { abc } llo }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Resolvable containing double-brace-wrapped text with escaping
|
||||
|
||||
{{ he {{ abc \}} llo }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Resolvable containing triple-brace-wrapped text with escaping
|
||||
|
||||
{{ he {{{ abc \}}} llo }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Resolvable containing single-bracket-wrapped text
|
||||
|
||||
{{ he [ abc ] llo }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Resolvable containing double-bracket-wrapped text
|
||||
|
||||
{{ he [[ abc ]] llo }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Resolvable containing triple-bracket-wrapped text
|
||||
|
||||
{{ he [[[ abc ]]] llo }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Plaintext of one opening brace
|
||||
|
||||
{
|
||||
|
||||
==>
|
||||
|
||||
Program(Plaintext)
|
||||
|
||||
# Plaintext of one opening brace and two closing braces
|
||||
|
||||
{ }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Plaintext)
|
||||
|
||||
# Plaintext then Resolvable with non-ASCII chars then Plaintext
|
||||
|
||||
a {{ 'áßи' }} a
|
||||
|
||||
==>
|
||||
|
||||
Program(Plaintext, Resolvable, Plaintext)
|
||||
|
||||
# Resolvable with currency symbol
|
||||
|
||||
{{ '€' }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Resolvable with cyrillic char
|
||||
|
||||
{{ 'л' }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Resolvable with Pictographs char
|
||||
|
||||
{{ '🎉' }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Resolvable with Emoticons char
|
||||
|
||||
{{ '😎' }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Resolvable with general punctuation char
|
||||
|
||||
{{ '†' }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Resolvable with superscript char
|
||||
|
||||
{{ '⁷' }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Resolvable with CJK char
|
||||
|
||||
{{ '漢' }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Resolvable with new emoji range
|
||||
|
||||
{{ '🟢' }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Resolvable with new emoji range end of range
|
||||
|
||||
{{ '🫸' }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Resolvable with start of Dingbat range - U+2700
|
||||
|
||||
{{ '✀' }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Resolvable with end of Dingbat range - U+27BF
|
||||
|
||||
{{ '➿' }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Resolvable with zap emoji - U+26A1
|
||||
|
||||
{{ '⚡' }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
|
||||
# Resolvable with character near end of Unicode range U+10FFFD
|
||||
{{ '' }}
|
||||
|
||||
==>
|
||||
|
||||
Program(Resolvable)
|
||||
@@ -0,0 +1,22 @@
|
||||
import { fileTests as runTestFile } from '@lezer/generator/dist/test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { n8nLanguage } from '../../src/expressions/index';
|
||||
|
||||
describe('expressions language', () => {
|
||||
const CASES_DIR = __dirname;
|
||||
for (const testFile of fs.readdirSync(CASES_DIR)) {
|
||||
if (!/\.txt$/.test(testFile)) continue;
|
||||
|
||||
const testFileName = /^[^\.]*/.exec(testFile)![0];
|
||||
describe(testFileName, () => {
|
||||
for (const { name, run } of runTestFile(
|
||||
fs.readFileSync(path.join(CASES_DIR, testFile), 'utf8'),
|
||||
testFile,
|
||||
)) {
|
||||
it(name, () => run(n8nLanguage.parser));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.json"],
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"tsBuildInfoFile": "dist/build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["test/**"]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@n8n/typescript-config/tsconfig.common.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*.ts", "test/**/*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createVitestConfig } from '@n8n/vitest-config/node';
|
||||
|
||||
export default createVitestConfig();
|
||||
Reference in New Issue
Block a user