Pārlūkot izejas kodu

Merge pull request #128 from goodproblems/liquid

Add basic language support for Liquid
Alexandru Dima 4 gadi atpakaļ
vecāks
revīzija
ab8e37a748

+ 12 - 0
src/liquid/liquid.contribution.ts

@@ -0,0 +1,12 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { registerLanguage } from '../_.contribution';
+
+registerLanguage({
+	id: 'liquid',
+	extensions: ['.liquid', '.liquid.html', '.liquid.css'],
+	loader: () => import('./liquid')
+});

+ 211 - 0
src/liquid/liquid.test.ts

@@ -0,0 +1,211 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { testTokenization } from '../test/testRunner';
+
+testTokenization(
+	['liquid', 'css'],
+	[
+		// Just HTML
+		[
+			{
+				line: '<h1>liquid!</h1>',
+				tokens: [
+					{ startIndex: 0, type: 'delimiter.html' },
+					{ startIndex: 1, type: 'tag.html' },
+					{ startIndex: 3, type: 'delimiter.html' },
+					{ startIndex: 4, type: '' },
+					{ startIndex: 11, type: 'delimiter.html' },
+					{ startIndex: 13, type: 'tag.html' },
+					{ startIndex: 15, type: 'delimiter.html' }
+				]
+			}
+		],
+
+		// Simple output
+		[
+			{
+				line: '<h1>{{ title }}</h1>',
+				tokens: [
+					{ startIndex: 0, type: 'delimiter.html' },
+					{ startIndex: 1, type: 'tag.html' },
+					{ startIndex: 3, type: 'delimiter.html' },
+					{ startIndex: 4, type: 'delimiter.output.liquid' },
+					{ startIndex: 6, type: '' },
+					{ startIndex: 7, type: 'variable.liquid' },
+					{ startIndex: 12, type: '' },
+					{ startIndex: 13, type: 'delimiter.output.liquid' },
+					{ startIndex: 15, type: 'delimiter.html' },
+					{ startIndex: 17, type: 'tag.html' },
+					{ startIndex: 19, type: 'delimiter.html' }
+				]
+			}
+		],
+
+		// // Output filter
+		[
+			{
+				line: '<h1>{{ 3.14159265 | round | default: "pi"  }}</h1>',
+				tokens: [
+					{ startIndex: 0, type: 'delimiter.html' },
+					{ startIndex: 1, type: 'tag.html' },
+					{ startIndex: 3, type: 'delimiter.html' },
+					{ startIndex: 4, type: 'delimiter.output.liquid' },
+					{ startIndex: 6, type: '' },
+					{ startIndex: 7, type: 'number.liquid' },
+					{ startIndex: 17, type: '' },
+					{ startIndex: 20, type: 'predefined.liquid' },
+					{ startIndex: 25, type: '' },
+					{ startIndex: 28, type: 'predefined.liquid' },
+					{ startIndex: 35, type: 'variable.liquid' },
+					{ startIndex: 36, type: '' },
+					{ startIndex: 37, type: 'string.liquid' },
+					{ startIndex: 41, type: '' },
+					{ startIndex: 43, type: 'delimiter.output.liquid' },
+					{ startIndex: 45, type: 'delimiter.html' },
+					{ startIndex: 47, type: 'tag.html' },
+					{ startIndex: 49, type: 'delimiter.html' }
+				]
+			}
+		],
+
+		// Simple Tag
+		[
+			{
+				line: '<div>{% render "files/file123.html" %}</div>',
+				tokens: [
+					{ startIndex: 0, type: 'delimiter.html' },
+					{ startIndex: 1, type: 'tag.html' },
+					{ startIndex: 4, type: 'delimiter.html' },
+					{ startIndex: 5, type: 'delimiter.tag.liquid' },
+					{ startIndex: 7, type: '' },
+					{ startIndex: 8, type: 'predefined.liquid' },
+					{ startIndex: 14, type: '' },
+					{ startIndex: 15, type: 'string.liquid' },
+					{ startIndex: 35, type: '' },
+					{ startIndex: 36, type: 'delimiter.tag.liquid' },
+					{ startIndex: 38, type: 'delimiter.html' },
+					{ startIndex: 40, type: 'tag.html' },
+					{ startIndex: 43, type: 'delimiter.html' }
+				]
+			}
+		],
+
+		// Tag with drop
+		[
+			{
+				line: '<div>{{ thing.other_thing }}</div>',
+				tokens: [
+					{ startIndex: 0, type: 'delimiter.html' },
+					{ startIndex: 1, type: 'tag.html' },
+					{ startIndex: 4, type: 'delimiter.html' },
+					{ startIndex: 5, type: 'delimiter.output.liquid' },
+					{ startIndex: 7, type: '' },
+					{ startIndex: 8, type: 'variable.liquid' },
+					{ startIndex: 13, type: '' },
+					{ startIndex: 14, type: 'variable.liquid' },
+					{ startIndex: 25, type: '' },
+					{ startIndex: 26, type: 'delimiter.output.liquid' },
+					{ startIndex: 28, type: 'delimiter.html' },
+					{ startIndex: 30, type: 'tag.html' },
+					{ startIndex: 33, type: 'delimiter.html' }
+				]
+			}
+		],
+
+		// If tag / keywords / block style tags
+		[
+			{
+				line:
+					'<div>{% if true=false %}<div>True</div>{% else %}<div>False</div>{% endif %}</div>',
+				tokens: [
+					{ startIndex: 0, type: 'delimiter.html' },
+					{ startIndex: 1, type: 'tag.html' },
+					{ startIndex: 4, type: 'delimiter.html' },
+					{ startIndex: 5, type: 'delimiter.tag.liquid' },
+					{ startIndex: 7, type: '' },
+					{ startIndex: 8, type: 'predefined.liquid' },
+					{ startIndex: 10, type: '' },
+					{ startIndex: 11, type: 'keyword.liquid' },
+					{ startIndex: 15, type: '' },
+					{ startIndex: 16, type: 'keyword.liquid' },
+					{ startIndex: 21, type: '' },
+					{ startIndex: 22, type: 'delimiter.tag.liquid' },
+					{ startIndex: 24, type: 'delimiter.html' },
+					{ startIndex: 25, type: 'tag.html' },
+					{ startIndex: 28, type: 'delimiter.html' },
+					{ startIndex: 29, type: '' },
+					{ startIndex: 33, type: 'delimiter.html' },
+					{ startIndex: 35, type: 'tag.html' },
+					{ startIndex: 38, type: 'delimiter.html' },
+					{ startIndex: 39, type: 'delimiter.tag.liquid' },
+					{ startIndex: 41, type: '' },
+					{ startIndex: 42, type: 'predefined.liquid' },
+					{ startIndex: 46, type: '' },
+					{ startIndex: 47, type: 'delimiter.tag.liquid' },
+					{ startIndex: 49, type: 'delimiter.html' },
+					{ startIndex: 50, type: 'tag.html' },
+					{ startIndex: 53, type: 'delimiter.html' },
+					{ startIndex: 54, type: '' },
+					{ startIndex: 59, type: 'delimiter.html' },
+					{ startIndex: 61, type: 'tag.html' },
+					{ startIndex: 64, type: 'delimiter.html' },
+					{ startIndex: 65, type: 'delimiter.tag.liquid' },
+					{ startIndex: 67, type: '' },
+					{ startIndex: 68, type: 'predefined.liquid' },
+					{ startIndex: 73, type: '' },
+					{ startIndex: 74, type: 'delimiter.tag.liquid' },
+					{ startIndex: 76, type: 'delimiter.html' },
+					{ startIndex: 78, type: 'tag.html' },
+					{ startIndex: 81, type: 'delimiter.html' }
+				]
+			}
+		],
+
+		// Comment tag
+		[
+			{
+				line: '<div>Anything you put between {% comment %} and {% endcomment %} tags</div>',
+				tokens: [
+					{ startIndex: 0, type: 'delimiter.html' },
+					{ startIndex: 1, type: 'tag.html' },
+					{ startIndex: 4, type: 'delimiter.html' },
+					{ startIndex: 5, type: '' },
+					{ startIndex: 30, type: 'comment.start.liquid' },
+					{ startIndex: 43, type: 'comment.content.liquid' },
+					{ startIndex: 48, type: 'comment.end.liquid' },
+					{ startIndex: 64, type: '' },
+					{ startIndex: 69, type: 'delimiter.html' },
+					{ startIndex: 71, type: 'tag.html' },
+					{ startIndex: 74, type: 'delimiter.html' }
+				]
+			}
+		],
+
+		// Raw tag
+		[
+			{
+				line:
+					'<div>Everything here should be escaped {% raw %} In Handlebars, {{ this }} will be HTML-escaped, but {{{ that }}} will not. {% endraw %}</div>',
+				tokens: [
+					{ startIndex: 0, type: 'delimiter.html' },
+					{ startIndex: 1, type: 'tag.html' },
+					{ startIndex: 4, type: 'delimiter.html' },
+					{ startIndex: 5, type: '' },
+					{ startIndex: 39, type: 'delimiter.tag.liquid' },
+					{ startIndex: 41, type: '' },
+					{ startIndex: 42, type: 'delimiter.tag.liquid' },
+					{ startIndex: 48, type: '' },
+					{ startIndex: 124, type: 'delimiter.tag.liquid' },
+					{ startIndex: 126, type: '' },
+					{ startIndex: 134, type: 'delimiter.tag.liquid' },
+					{ startIndex: 136, type: 'delimiter.html' },
+					{ startIndex: 138, type: 'tag.html' },
+					{ startIndex: 141, type: 'delimiter.html' }
+				]
+			}
+		]
+	]
+);

+ 251 - 0
src/liquid/liquid.ts

@@ -0,0 +1,251 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { languages } from '../fillers/monaco-editor-core';
+
+const EMPTY_ELEMENTS: string[] = [
+	'area',
+	'base',
+	'br',
+	'col',
+	'embed',
+	'hr',
+	'img',
+	'input',
+	'keygen',
+	'link',
+	'menuitem',
+	'meta',
+	'param',
+	'source',
+	'track',
+	'wbr'
+];
+
+export const conf: languages.LanguageConfiguration = {
+	wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,
+
+	brackets: [
+		['<!--', '-->'],
+		['<', '>'],
+		['{{', '}}'],
+		['{%', '%}'],
+		['{', '}'],
+		['(', ')']
+	],
+
+	autoClosingPairs: [
+		{ open: '{', close: '}' },
+		{ open: '%', close: '%' },
+		{ open: '[', close: ']' },
+		{ open: '(', close: ')' },
+		{ open: '"', close: '"' },
+		{ open: "'", close: "'" }
+	],
+
+	surroundingPairs: [
+		{ open: '<', close: '>' },
+		{ open: '"', close: '"' },
+		{ open: "'", close: "'" }
+	],
+
+	onEnterRules: [
+		{
+			beforeText: new RegExp(
+				`<(?!(?:${EMPTY_ELEMENTS.join('|')}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`,
+				'i'
+			),
+			afterText: /^<\/(\w[\w\d]*)\s*>$/i,
+			action: {
+				indentAction: languages.IndentAction.IndentOutdent
+			}
+		},
+		{
+			beforeText: new RegExp(
+				`<(?!(?:${EMPTY_ELEMENTS.join('|')}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`,
+				'i'
+			),
+			action: { indentAction: languages.IndentAction.Indent }
+		}
+	]
+};
+
+export const language = <languages.IMonarchLanguage>{
+	defaultToken: '',
+	tokenPostfix: '',
+
+	builtinTags: [
+		'if',
+		'else',
+		'elseif',
+		'endif',
+		'render',
+		'assign',
+		'capture',
+		'endcapture',
+		'case',
+		'endcase',
+		'comment',
+		'endcomment',
+		'cycle',
+		'decrement',
+		'for',
+		'endfor',
+		'include',
+		'increment',
+		'layout',
+		'raw',
+		'endraw',
+		'render',
+		'tablerow',
+		'endtablerow',
+		'unless',
+		'endunless'
+	],
+
+	builtinFilters: [
+		'abs',
+		'append',
+		'at_least',
+		'at_most',
+		'capitalize',
+		'ceil',
+		'compact',
+		'date',
+		'default',
+		'divided_by',
+		'downcase',
+		'escape',
+		'escape_once',
+		'first',
+		'floor',
+		'join',
+		'json',
+		'last',
+		'lstrip',
+		'map',
+		'minus',
+		'modulo',
+		'newline_to_br',
+		'plus',
+		'prepend',
+		'remove',
+		'remove_first',
+		'replace',
+		'replace_first',
+		'reverse',
+		'round',
+		'rstrip',
+		'size',
+		'slice',
+		'sort',
+		'sort_natural',
+		'split',
+		'strip',
+		'strip_html',
+		'strip_newlines',
+		'times',
+		'truncate',
+		'truncatewords',
+		'uniq',
+		'upcase',
+		'url_decode',
+		'url_encode',
+		'where'
+	],
+
+	constants: ['true', 'false'],
+	operators: ['==', '!=', '>', '<', '>=', '<='],
+
+	symbol: /[=><!]+/,
+	identifier: /[a-zA-Z_][\w]*/,
+
+	tokenizer: {
+		root: [
+			[/\{\%\s*comment\s*\%\}/, 'comment.start.liquid', '@comment'],
+			[/\{\{/, { token: '@rematch', switchTo: '@liquidState.root' }],
+			[/\{\%/, { token: '@rematch', switchTo: '@liquidState.root' }],
+			[/(<)(\w+)(\/>)/, ['delimiter.html', 'tag.html', 'delimiter.html']],
+			[/(<)([:\w]+)/, ['delimiter.html', { token: 'tag.html', next: '@otherTag' }]],
+			[/(<\/)(\w+)/, ['delimiter.html', { token: 'tag.html', next: '@otherTag' }]],
+			[/</, 'delimiter.html'],
+			[/\{/, 'delimiter.html'],
+			[/[^<{]+/] // text
+		],
+
+		comment: [
+			[/\{\%\s*endcomment\s*\%\}/, 'comment.end.liquid', '@pop'],
+			[/./, 'comment.content.liquid']
+		],
+
+		otherTag: [
+			[
+				/\{\{/,
+				{
+					token: '@rematch',
+					switchTo: '@liquidState.otherTag'
+				}
+			],
+			[
+				/\{\%/,
+				{
+					token: '@rematch',
+					switchTo: '@liquidState.otherTag'
+				}
+			],
+			[/\/?>/, 'delimiter.html', '@pop'],
+			[/"([^"]*)"/, 'attribute.value'],
+			[/'([^']*)'/, 'attribute.value'],
+			[/[\w\-]+/, 'attribute.name'],
+			[/=/, 'delimiter'],
+			[/[ \t\r\n]+/] // whitespace
+		],
+
+		liquidState: [
+			[/\{\{/, 'delimiter.output.liquid'],
+			[/\}\}/, { token: 'delimiter.output.liquid', switchTo: '@$S2.$S3' }],
+			[/\{\%/, 'delimiter.tag.liquid'],
+			[/raw\s*\%\}/, 'delimiter.tag.liquid', '@liquidRaw'],
+			[/\%\}/, { token: 'delimiter.tag.liquid', switchTo: '@$S2.$S3' }],
+			{ include: 'liquidRoot' }
+		],
+
+		liquidRaw: [
+			[/^(?!\{\%\s*endraw\s*\%\}).+/],
+			[/\{\%/, 'delimiter.tag.liquid'],
+			[/@identifier/],
+			[/\%\}/, { token: 'delimiter.tag.liquid', next: '@root' }],
+		],
+
+		liquidRoot: [
+			[/\d+(\.\d+)?/, 'number.liquid'],
+			[/"[^"]*"/, 'string.liquid'],
+			[/'[^']*'/, 'string.liquid'],
+			[/\s+/],
+			[
+				/@symbol/,
+				{
+					cases: {
+						'@operators': 'operator.liquid',
+						'@default': ''
+					}
+				}
+			],
+			[/\./],
+			[
+				/@identifier/,
+				{
+					cases: {
+						'@constants': 'keyword.liquid',
+						'@builtinFilters': 'predefined.liquid',
+						'@builtinTags': 'predefined.liquid',
+						'@default': 'variable.liquid'
+					}
+				}
+			],
+			[/[^}|%]/, 'variable.liquid']
+		]
+	}
+};

+ 1 - 0
src/monaco.contribution.ts

@@ -31,6 +31,7 @@ import './kotlin/kotlin.contribution';
 import './less/less.contribution';
 import './lexon/lexon.contribution';
 import './lua/lua.contribution';
+import './liquid/liquid.contribution';
 import './m3/m3.contribution';
 import './markdown/markdown.contribution';
 import './mips/mips.contribution';