diff --git a/README.md b/README.md
index 1ece6dd2..a649ab40 100644
--- a/README.md
+++ b/README.md
@@ -44,6 +44,7 @@ Colorization and configuration supports for multiple languages for the Monaco Ed
* sql
* st
* swift
+* twig
* typescript
* vb
* xml
diff --git a/scripts/bundle.js b/scripts/bundle.js
index ebef7632..bb99c435 100644
--- a/scripts/bundle.js
+++ b/scripts/bundle.js
@@ -76,6 +76,7 @@ bundleOne('azcli/azcli');
bundleOne('apex/apex');
bundleOne('tcl/tcl');
bundleOne('graphql/graphql');
+bundleOne('twig/twig');
function bundleOne(moduleId, exclude) {
requirejs.optimize({
diff --git a/src/monaco.contribution.ts b/src/monaco.contribution.ts
index 2a65b431..8f130a20 100644
--- a/src/monaco.contribution.ts
+++ b/src/monaco.contribution.ts
@@ -49,6 +49,7 @@ import './sql/sql.contribution';
import './st/st.contribution';
import './swift/swift.contribution';
import './tcl/tcl.contribution';
+import './twig/twig.contribution';
import './typescript/typescript.contribution';
import './vb/vb.contribution';
import './xml/xml.contribution';
diff --git a/src/twig/twig.contribution.ts b/src/twig/twig.contribution.ts
new file mode 100644
index 00000000..d5a02590
--- /dev/null
+++ b/src/twig/twig.contribution.ts
@@ -0,0 +1,15 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+'use strict';
+
+import { registerLanguage } from '../_.contribution';
+
+registerLanguage({
+ id: 'twig',
+ extensions: ['.twig'],
+ aliases: ['Twig', 'twig'],
+ mimetypes: ['text/x-twig'],
+ loader: () => import('./twig')
+});
diff --git a/src/twig/twig.test.ts b/src/twig/twig.test.ts
new file mode 100644
index 00000000..327f0e30
--- /dev/null
+++ b/src/twig/twig.test.ts
@@ -0,0 +1,894 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+'use strict';
+
+import { testTokenization } from '../test/testRunner';
+
+/**
+ * HTML Tests
+ */
+testTokenization(['twig', 'css', 'javascript'], [
+
+ // Open Start Tag #1'
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 1, type: '' }
+ ]
+ }],
+
+ // Open Start Tag #4
+ [{
+ line: 'i ',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 1, type: 'tag.html' },
+ { startIndex: 4, type: 'delimiter.html' }
+ ]
+ }],
+
+ // Complete Start Tag with Whitespace
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 1, type: 'tag.html' },
+ { startIndex: 4, type: '' },
+ { startIndex: 5, type: 'delimiter.html' }
+ ]
+ }],
+
+ // bug 9809 - Complete Start Tag with Namespaceprefix
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 1, type: 'tag.html' },
+ { startIndex: 8, type: 'delimiter.html' }
+ ]
+ }],
+
+ // Complete End Tag
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 2, type: 'tag.html' },
+ { startIndex: 5, type: 'delimiter.html' }
+ ]
+ }],
+
+ // Complete End Tag with Whitespace
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 2, type: 'tag.html' },
+ { startIndex: 5, type: '' },
+ { startIndex: 7, type: 'delimiter.html' }
+ ]
+ }],
+
+ // Empty Tag
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 1, type: 'tag.html' },
+ { startIndex: 4, type: '' },
+ { startIndex: 5, type: 'delimiter.html' }
+ ]
+ }],
+
+ // Embedded Content #1
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 1, type: 'tag.html' },
+ { startIndex: 7, type: '' },
+ { startIndex: 8, type: 'attribute.name.html' },
+ { startIndex: 12, type: 'delimiter.html' },
+ { startIndex: 13, type: 'attribute.value.html' },
+ { startIndex: 30, type: 'delimiter.html' },
+ { startIndex: 31, type: 'keyword.js' },
+ { startIndex: 34, type: '' },
+ { startIndex: 35, type: 'identifier.js' },
+ { startIndex: 36, type: 'delimiter.js' },
+ { startIndex: 37, type: '' },
+ { startIndex: 38, type: 'number.js' },
+ { startIndex: 40, type: 'delimiter.js' },
+ { startIndex: 41, type: 'delimiter.html' },
+ { startIndex: 43, type: 'tag.html' },
+ { startIndex: 49, type: 'delimiter.html' }
+ ]
+ }],
+
+ // Embedded Content #2
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 2, type: 'tag.html' },
+ { startIndex: 8, type: 'delimiter.html' }
+ ]
+ }],
+
+ // Embedded Content #3
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 2, type: 'tag.html' },
+ { startIndex: 8, type: 'delimiter.html' }
+ ]
+ }],
+
+ // Embedded Content #4
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'keyword.js' },
+ { startIndex: 3, type: '' },
+ { startIndex: 4, type: 'identifier.js' },
+ { startIndex: 5, type: 'delimiter.js' },
+ { startIndex: 6, type: '' },
+ { startIndex: 7, type: 'number.js' },
+ { startIndex: 9, type: 'delimiter.js' },
+ { startIndex: 10, type: 'delimiter.html' },
+ { startIndex: 12, type: 'tag.html' },
+ { startIndex: 18, type: 'delimiter.html' }
+ ]
+ }],
+
+ // Embedded Content #5
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: '' },
+ { startIndex: 2, type: 'delimiter.html' },
+ { startIndex: 4, type: 'tag.html' },
+ { startIndex: 10, type: 'delimiter.html' }
+ ]
+ }],
+
+ // Embedded Content #6
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 1, type: 'tag.html' },
+ { startIndex: 7, type: 'delimiter.html' },
+ { startIndex: 8, type: 'identifier.js' },
+ { startIndex: 9, type: 'delimiter.html' },
+ { startIndex: 11, type: 'tag.html' },
+ { startIndex: 17, type: 'delimiter.html' },
+ // { startIndex:18, type: 'delimiter.html' },
+ { startIndex: 19, type: 'tag.html' },
+ { startIndex: 25, type: 'delimiter.html' },
+ { startIndex: 26, type: 'identifier.js' },
+ { startIndex: 27, type: 'delimiter.html' },
+ { startIndex: 29, type: 'tag.html' },
+ { startIndex: 35, type: 'delimiter.html' }
+ ]
+ }],
+
+ // Embedded Content #7
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 1, type: 'tag.html' },
+ { startIndex: 7, type: '' },
+ { startIndex: 8, type: 'attribute.name.html' },
+ { startIndex: 12, type: 'delimiter.html' },
+ { startIndex: 13, type: 'attribute.value.html' },
+ { startIndex: 30, type: 'delimiter.html' },
+ // { startIndex:31, type: 'delimiter.html' },
+ { startIndex: 33, type: 'tag.html' },
+ { startIndex: 39, type: 'delimiter.html' }
+ ]
+ }],
+
+ // Embedded Content #8
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 1, type: 'tag.html' },
+ { startIndex: 7, type: 'delimiter.html' },
+ { startIndex: 8, type: 'keyword.js' },
+ { startIndex: 11, type: '' },
+ { startIndex: 12, type: 'identifier.js' },
+ { startIndex: 13, type: 'delimiter.js' },
+ { startIndex: 14, type: '' },
+ { startIndex: 15, type: 'number.js' },
+ { startIndex: 17, type: 'delimiter.js' },
+ { startIndex: 18, type: 'delimiter.html' },
+ { startIndex: 20, type: 'tag.html' },
+ { startIndex: 26, type: 'delimiter.html' }
+ ]
+ }],
+
+ // Embedded Content #9
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 1, type: 'tag.html' },
+ { startIndex: 7, type: '' },
+ { startIndex: 8, type: 'attribute.name.html' },
+ { startIndex: 12, type: 'delimiter.html' },
+ { startIndex: 13, type: 'attribute.value.html' },
+ { startIndex: 30, type: '' },
+ { startIndex: 31, type: 'attribute.name.html' },
+ { startIndex: 34, type: 'delimiter.html' },
+ { startIndex: 35, type: 'attribute.value.html' },
+ { startIndex: 44, type: 'delimiter.html' },
+ // { startIndex:45, type: 'delimiter.html' },
+ { startIndex: 47, type: 'tag.html' },
+ { startIndex: 53, type: 'delimiter.html' }
+ ]
+ }],
+
+ // Tag with Attribute
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 1, type: 'tag.html' },
+ { startIndex: 4, type: '' },
+ { startIndex: 5, type: 'attribute.name.html' },
+ { startIndex: 8, type: 'delimiter.html' },
+ { startIndex: 9, type: 'attribute.value.html' },
+ { startIndex: 14, type: 'delimiter.html' }
+ ]
+ }],
+
+ // Tag with Empty Attribute Value
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 1, type: 'tag.html' },
+ { startIndex: 4, type: '' },
+ { startIndex: 5, type: 'attribute.name.html' },
+ { startIndex: 8, type: 'delimiter.html' },
+ { startIndex: 9, type: 'attribute.value.html' },
+ { startIndex: 14, type: 'delimiter.html' }
+ ]
+ }],
+
+ // Tag with empty attributes
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 1, type: 'tag.html' },
+ { startIndex: 4, type: '' },
+ { startIndex: 5, type: 'attribute.name.html' },
+ { startIndex: 8, type: 'delimiter.html' },
+ { startIndex: 9, type: 'attribute.value.html' },
+ { startIndex: 11, type: 'delimiter.html' }
+ ]
+ }],
+
+ // Tag with Attributes
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 1, type: 'tag.html' },
+ { startIndex: 4, type: '' },
+ { startIndex: 5, type: 'attribute.name.html' },
+ { startIndex: 8, type: 'delimiter.html' },
+ { startIndex: 9, type: 'attribute.value.html' },
+ { startIndex: 14, type: '' },
+ { startIndex: 15, type: 'attribute.name.html' },
+ { startIndex: 18, type: 'delimiter.html' },
+ { startIndex: 19, type: 'attribute.value.html' },
+ { startIndex: 24, type: 'delimiter.html' }
+ ]
+ }],
+
+ // Tag with Attributes, no quotes
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 1, type: 'tag.html' },
+ { startIndex: 4, type: '' },
+ { startIndex: 5, type: 'attribute.name.html' },
+ { startIndex: 8, type: 'delimiter.html' },
+ { startIndex: 9, type: 'attribute.name.html' }, // slightly incorrect
+ { startIndex: 12, type: '' },
+ { startIndex: 13, type: 'attribute.name.html' },
+ { startIndex: 16, type: 'delimiter.html' },
+ { startIndex: 17, type: 'attribute.name.html' }, // slightly incorrect
+ { startIndex: 24, type: 'delimiter.html' }
+ ]
+ }],
+
+ // Tag with Attribute And Whitespace
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 1, type: 'tag.html' },
+ { startIndex: 4, type: '' },
+ { startIndex: 5, type: 'attribute.name.html' },
+ { startIndex: 8, type: 'delimiter.html' },
+ { startIndex: 9, type: '' },
+ { startIndex: 11, type: 'attribute.value.html' },
+ { startIndex: 16, type: 'delimiter.html' }
+ ]
+ }],
+
+ // Tag with Attribute And Whitespace #2
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 1, type: 'tag.html' },
+ { startIndex: 4, type: '' },
+ { startIndex: 5, type: 'attribute.name.html' },
+ { startIndex: 8, type: '' },
+ { startIndex: 9, type: 'delimiter.html' },
+ { startIndex: 10, type: '' },
+ { startIndex: 11, type: 'attribute.value.html' },
+ { startIndex: 16, type: 'delimiter.html' }
+ ]
+ }],
+
+ // Tag with Name-Only-Attribute #1
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 1, type: 'tag.html' },
+ { startIndex: 4, type: '' },
+ { startIndex: 5, type: 'attribute.name.html' },
+ { startIndex: 8, type: 'delimiter.html' }
+ ]
+ }],
+
+ // Tag with Name-Only-Attribute #2
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 1, type: 'tag.html' },
+ { startIndex: 4, type: '' },
+ { startIndex: 5, type: 'attribute.name.html' },
+ { startIndex: 8, type: '' },
+ { startIndex: 9, type: 'attribute.name.html' },
+ { startIndex: 12, type: 'delimiter.html' }
+ ]
+ }],
+
+ // Tag with Interesting Attribute Name
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 1, type: 'tag.html' },
+ { startIndex: 4, type: '' },
+ { startIndex: 5, type: 'attribute.name.html' },
+ { startIndex: 8, type: '' },
+ { startIndex: 11, type: 'delimiter.html' },
+ { startIndex: 12, type: 'attribute.value.html' },
+ { startIndex: 17, type: 'delimiter.html' }
+ ]
+ }],
+
+ // Tag with Angular Attribute Name
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 1, type: 'tag.html' },
+ { startIndex: 4, type: '' },
+ { startIndex: 6, type: 'attribute.name.html' },
+ { startIndex: 13, type: '' },
+ { startIndex: 15, type: 'attribute.name.html' },
+ { startIndex: 20, type: '' },
+ { startIndex: 21, type: 'delimiter.html' },
+ { startIndex: 22, type: 'attribute.value.html' },
+ { startIndex: 27, type: '' },
+ { startIndex: 29, type: 'attribute.name.html' },
+ { startIndex: 34, type: '' },
+ { startIndex: 35, type: 'delimiter.html' },
+ { startIndex: 36, type: 'attribute.value.html' },
+ { startIndex: 50, type: '' },
+ { startIndex: 52, type: 'attribute.name.html' },
+ { startIndex: 56, type: 'delimiter.html' },
+ { startIndex: 57, type: 'attribute.value.html' },
+ { startIndex: 72, type: 'delimiter.html' }
+ ]
+ }],
+
+ // Tag with Invalid Attribute Value
+ [{
+ line: '',
+ tokens: [
+ { startIndex: 0, type: 'metatag.content.html' },
+ { startIndex: 11, type: 'metatag.html' }
+ ]
+ }],
+
+ // PR #14
+ [{
+ line: 'asd',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.html' },
+ { startIndex: 1, type: 'tag.html' },
+ { startIndex: 9, type: 'delimiter.html' },
+ { startIndex: 10, type: '' },
+ { startIndex: 13, type: 'delimiter.html' },
+ { startIndex: 15, type: 'tag.html' },
+ { startIndex: 23, type: 'delimiter.html' }
+ ]
+ }]
+]);
+
+/**
+ * Twig Tests
+ */
+testTokenization(['twig'], [
+ /**
+ * Comments
+ */
+ [{
+ line: '{# Hello World! #}',
+ tokens: [
+ { startIndex: 0, type: 'comment.twig' },
+ ],
+ }],
+ [{
+ line: '{#Hello World!#}',
+ tokens: [
+ { startIndex: 0, type: 'comment.twig' },
+ ],
+ }],
+
+ /**
+ * Variables Tags
+ */
+ // Whitespace
+ [{
+ line: '{{}}',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.twig' },
+ ],
+ }],
+ [{
+ line: '{{ }}',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.twig' },
+ { startIndex: 2, type: '' },
+ { startIndex: 3, type: 'delimiter.twig' },
+ ],
+ }],
+ // Numbers
+ [{
+ line: '{{1}}',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.twig' },
+ { startIndex: 2, type: 'number.twig' },
+ { startIndex: 3, type: 'delimiter.twig' },
+ ],
+ }],
+ [{
+ line: '{{ 1 }}',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.twig' },
+ { startIndex: 2, type: '' },
+ { startIndex: 3, type: 'number.twig' },
+ { startIndex: 4, type: '' },
+ { startIndex: 5, type: 'delimiter.twig' },
+ ],
+ }],
+ [{
+ line: '{{ 1 }}',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.twig' },
+ { startIndex: 2, type: '' },
+ { startIndex: 3, type: 'number.twig' },
+ { startIndex: 4, type: '' },
+ { startIndex: 5, type: 'delimiter.twig' },
+ ],
+ }],
+ [{
+ line: '{{ 1.1 }}',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.twig' },
+ { startIndex: 2, type: '' },
+ { startIndex: 3, type: 'number.twig' },
+ { startIndex: 6, type: '' },
+ { startIndex: 7, type: 'delimiter.twig' },
+ ],
+ }],
+ // Strings
+ [{
+ line: "{{ 'hi' }}",
+ tokens: [
+ { startIndex: 0, type: 'delimiter.twig' },
+ { startIndex: 2, type: '' },
+ { startIndex: 3, type: 'string.twig' },
+ { startIndex: 7, type: '' },
+ { startIndex: 8, type: 'delimiter.twig' },
+ ],
+ }],
+ [{
+ line: '{{ "hi" }}',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.twig' },
+ { startIndex: 2, type: '' },
+ { startIndex: 3, type: 'string.twig' },
+ { startIndex: 7, type: '' },
+ { startIndex: 8, type: 'delimiter.twig' },
+ ],
+ }],
+ [{
+ line: '{{ "hi #{1}" }}',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.twig' },
+ { startIndex: 2, type: '' },
+ { startIndex: 3, type: 'string.twig' },
+ { startIndex: 9, type: 'number.twig' },
+ { startIndex: 10, type: 'string.twig' },
+ { startIndex: 12, type: '' },
+ { startIndex: 13, type: 'delimiter.twig' },
+ ],
+ }],
+ // Variables and functions
+ [{
+ line: '{{ foo }}',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.twig' },
+ { startIndex: 2, type: '' },
+ { startIndex: 3, type: 'variable.twig' },
+ { startIndex: 6, type: '' },
+ { startIndex: 7, type: 'delimiter.twig' },
+ ],
+ }],
+ [{
+ line: '{{ foo(42) }}',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.twig' },
+ { startIndex: 2, type: '' },
+ { startIndex: 3, type: 'variable.twig' },
+ { startIndex: 6, type: 'delimiter.twig' },
+ { startIndex: 7, type: 'number.twig' },
+ { startIndex: 9, type: 'delimiter.twig' },
+ { startIndex: 10, type: '' },
+ { startIndex: 11, type: 'delimiter.twig' },
+ ],
+ }],
+ // Operators
+ [{
+ line: '{{ 1 + 2 - 3 / 4 // 5 % 6 * 7 ** 8 }}',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.twig' },
+ { startIndex: 2, type: '' },
+ { startIndex: 3, type: 'number.twig' },
+ { startIndex: 4, type: '' },
+ { startIndex: 5, type: 'operators.twig' },
+ { startIndex: 6, type: '' },
+ { startIndex: 7, type: 'number.twig' },
+ { startIndex: 8, type: '' },
+ { startIndex: 9, type: 'operators.twig' },
+ { startIndex: 10, type: '' },
+ { startIndex: 11, type: 'number.twig' },
+ { startIndex: 12, type: '' },
+ { startIndex: 13, type: 'operators.twig' },
+ { startIndex: 14, type: '' },
+ { startIndex: 15, type: 'number.twig' },
+ { startIndex: 16, type: '' },
+ { startIndex: 17, type: 'operators.twig' },
+ { startIndex: 19, type: '' },
+ { startIndex: 20, type: 'number.twig' },
+ { startIndex: 21, type: '' },
+ { startIndex: 22, type: 'operators.twig' },
+ { startIndex: 23, type: '' },
+ { startIndex: 24, type: 'number.twig' },
+ { startIndex: 25, type: '' },
+ { startIndex: 26, type: 'operators.twig' },
+ { startIndex: 27, type: '' },
+ { startIndex: 28, type: 'number.twig' },
+ { startIndex: 29, type: '' },
+ { startIndex: 30, type: 'operators.twig' },
+ { startIndex: 32, type: '' },
+ { startIndex: 33, type: 'number.twig' },
+ { startIndex: 34, type: '' },
+ { startIndex: 35, type: 'delimiter.twig' },
+ ],
+ }],
+ [{
+ line: '{{ true and false or true and not false }}',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.twig' },
+ { startIndex: 2, type: '' },
+ { startIndex: 3, type: 'keyword.twig' },
+ { startIndex: 7, type: '' },
+ { startIndex: 8, type: 'operators.twig' },
+ { startIndex: 11, type: '' },
+ { startIndex: 12, type: 'keyword.twig' },
+ { startIndex: 17, type: '' },
+ { startIndex: 18, type: 'operators.twig' },
+ { startIndex: 20, type: '' },
+ { startIndex: 21, type: 'keyword.twig' },
+ { startIndex: 25, type: '' },
+ { startIndex: 26, type: 'operators.twig' },
+ { startIndex: 29, type: '' },
+ { startIndex: 30, type: 'operators.twig' },
+ { startIndex: 33, type: '' },
+ { startIndex: 34, type: 'keyword.twig' },
+ { startIndex: 39, type: '' },
+ { startIndex: 40, type: 'delimiter.twig' },
+ ],
+ }],
+
+ /**
+ * Block Tags
+ */
+ [{
+ line: '{%%}',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.twig' },
+ ],
+ }],
+ [{
+ line: '{% %}',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.twig' },
+ { startIndex: 2, type: '' },
+ { startIndex: 3, type: 'delimiter.twig' },
+ ],
+ }],
+ [{
+ line: '{% for item in navigation %}',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.twig' },
+ { startIndex: 2, type: '' },
+ { startIndex: 3, type: 'keyword.twig' },
+ { startIndex: 6, type: '' },
+ { startIndex: 7, type: 'variable.twig' },
+ { startIndex: 11, type: '' },
+ { startIndex: 12, type: 'operators.twig' },
+ { startIndex: 14, type: '' },
+ { startIndex: 15, type: 'variable.twig' },
+ { startIndex: 25, type: '' },
+ { startIndex: 26, type: 'delimiter.twig' },
+ ],
+ }],
+ [{
+ line: '{% verbatim %}',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.twig' },
+ { startIndex: 2, type: '' },
+ { startIndex: 3, type: 'keyword.twig' },
+ { startIndex: 11, type: '' },
+ { startIndex: 12, type: 'delimiter.twig' },
+ ],
+ }],
+ [{
+ line: '{% verbatim %}raw data{% endverbatim %}',
+ tokens: [
+ { startIndex: 0, type: 'delimiter.twig' },
+ { startIndex: 2, type: '' },
+ { startIndex: 3, type: 'keyword.twig' },
+ { startIndex: 11, type: '' },
+ { startIndex: 12, type: 'delimiter.twig' },
+ { startIndex: 14, type: 'string.twig' },
+ { startIndex: 22, type: 'delimiter.twig' },
+ { startIndex: 24, type: '' },
+ { startIndex: 25, type: 'keyword.twig' },
+ { startIndex: 36, type: '' },
+ { startIndex: 37, type: 'delimiter.twig' },
+ ],
+ }],
+]);
diff --git a/src/twig/twig.ts b/src/twig/twig.ts
new file mode 100644
index 00000000..de4f319b
--- /dev/null
+++ b/src/twig/twig.ts
@@ -0,0 +1,314 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+'use strict';
+
+import IRichLanguageConfiguration = monaco.languages.LanguageConfiguration;
+import ILanguage = monaco.languages.IMonarchLanguage;
+
+export const conf: IRichLanguageConfiguration = {
+ wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,
+
+ comments: {
+ blockComment: ['{#', '#}'],
+ },
+
+ brackets: [
+ ['{#', '#}'],
+ ['{%', '%}'],
+ ['{{', '}}'],
+ ['(', ')'],
+ ['[', ']'],
+
+ // HTML
+ [''],
+ ['<', '>'],
+ ],
+
+ autoClosingPairs: [
+ { open: '{# ', close: ' #}' },
+ { open: '{% ', close: ' %}' },
+ { open: '{{ ', close: ' }}' },
+ { open: '[', close: ']' },
+ { open: '(', close: ')' },
+ { open: '"', close: '"' },
+ { open: '\'', close: '\'' },
+ ],
+
+ surroundingPairs: [
+ { open: '"', close: '"' },
+ { open: '\'', close: '\'' },
+
+ // HTML
+ { open: '<', close: '>' },
+ ],
+}
+
+export const language = {
+ defaultToken: '',
+ tokenPostfix: '',
+ ignoreCase: true,
+
+ keywords: [
+ // (opening) tags
+ 'apply', 'autoescape', 'block', 'deprecated', 'do', 'embed', 'extends',
+ 'flush', 'for', 'from', 'if', 'import', 'include', 'macro', 'sandbox',
+ 'set', 'use', 'verbatim', 'with',
+ // closing tags
+ 'endapply', 'endautoescape', 'endblock', 'endembed', 'endfor', 'endif',
+ 'endmacro', 'endsandbox', 'endset', 'endwith',
+ // literals
+ 'true', 'false',
+ ],
+
+ tokenizer: {
+ root: [
+ // whitespace
+ [/\s+/],
+
+ // Twig Tag Delimiters
+ [/{#/, 'comment.twig', '@commentState'],
+ [/{%[-~]?/, 'delimiter.twig', '@blockState'],
+ [/{{[-~]?/, 'delimiter.twig', '@variableState'],
+
+ // HTML
+ [/)/, ['delimiter.html', 'tag.html', '', 'delimiter.html']],
+ [/(<)(script)/, ['delimiter.html', { token: 'tag.html', next: '@script' }]],
+ [/(<)(style)/, ['delimiter.html', { token: 'tag.html', next: '@style' }]],
+ [/(<)((?:[\w\-]+:)?[\w\-]+)/, ['delimiter.html', { token: 'tag.html', next: '@otherTag' }]],
+ [/(<\/)((?:[\w\-]+:)?[\w\-]+)/, ['delimiter.html', { token: 'tag.html', next: '@otherTag' }]],
+ [/, 'delimiter.html'],
+ [/[^<]+/], // text
+ ],
+
+ /**
+ * Comment Tag Handling
+ */
+ commentState: [
+ [/#}/, 'comment.twig', '@pop'],
+ [/./, 'comment.twig'],
+ ],
+
+ /**
+ * Block Tag Handling
+ */
+ blockState: [
+ [/[-~]?%}/, 'delimiter.twig', '@pop'],
+ // whitespace
+ [/\s+/],
+ // verbatim
+ // Unlike other blocks, verbatim ehas its own state
+ // transition to ensure we mark its contents as strings.
+ [/(verbatim)(\s*)([-~]?%})/, [
+ 'keyword.twig',
+ '',
+ { token: 'delimiter.twig', next: '@rawDataState' },
+ ]],
+ { include: 'expression' }
+ ],
+
+ rawDataState: [
+ // endverbatim
+ [/({%[-~]?)(\s*)(endverbatim)(\s*)([-~]?%})/, [
+ 'delimiter.twig',
+ '',
+ 'keyword.twig',
+ '',
+ { token: 'delimiter.twig', next: '@popall' },
+ ]],
+ [/./, 'string.twig'],
+ ],
+
+ /**
+ * Variable Tag Handling
+ */
+ variableState: [
+ [/[-~]?}}/, 'delimiter.twig', '@pop'],
+ { include: 'expression' },
+ ],
+
+ stringState: [
+ // closing double quoted string
+ [/"/, 'string.twig', '@pop'],
+ // interpolation start
+ [/#{\s*/, 'string.twig', '@interpolationState'],
+ // string part
+ [/[^#"\\]*(?:(?:\\.|#(?!\{))[^#"\\]*)*/, 'string.twig'],
+ ],
+
+ interpolationState: [
+ // interpolation end
+ [/}/, 'string.twig', '@pop'],
+ { include: 'expression' },
+ ],
+
+ /**
+ * Expression Handling
+ */
+ expression: [
+ // whitespace
+ [/\s+/],
+ // operators - math
+ [/\+|-|\/{1,2}|%|\*{1,2}/, 'operators.twig'],
+ // operators - logic
+ [/(and|or|not|b-and|b-xor|b-or)(\s+)/, ['operators.twig', '']],
+ // operators - comparison (symbols)
+ [/==|!=|<|>|>=|<=/, 'operators.twig'],
+ // operators - comparison (words)
+ [/(starts with|ends with|matches)(\s+)/, ['operators.twig', '']],
+ // operators - containment
+ [/(in)(\s+)/, ['operators.twig', '']],
+ // operators - test
+ [/(is)(\s+)/, ['operators.twig', '']],
+ // operators - misc
+ [/\||~|:|\.{1,2}|\?{1,2}/, 'operators.twig'],
+ // names
+ [/[^\W\d][\w]*/, {
+ cases: {
+ '@keywords': 'keyword.twig',
+ '@default': 'variable.twig'
+ }
+ }],
+ // numbers
+ [/\d+(\.\d+)?/, 'number.twig'],
+ // punctuation
+ [/\(|\)|\[|\]|{|}|,/, 'delimiter.twig'],
+ // strings
+ [/"([^#"\\]*(?:\\.[^#"\\]*)*)"|\'([^\'\\]*(?:\\.[^\'\\]*)*)\'/, 'string.twig'],
+ // opening double quoted string
+ [/"/, 'string.twig', '@stringState'],
+
+ // misc syntactic constructs
+ // These are not operators per se, but for the purposes of lexical analysis we
+ // can treat them as such.
+ // arrow functions
+ [/=>/, 'operators.twig'],
+ // assignment
+ [/=/, 'operators.twig'],
+ ],
+
+ /**
+ * HTML
+ */
+ doctype: [
+ [/[^>]+/, 'metatag.content.html'],
+ [/>/, 'metatag.html', '@pop'],
+ ],
+
+ comment: [
+ [/-->/, 'comment.html', '@pop'],
+ [/[^-]+/, 'comment.content.html'],
+ [/./, 'comment.content.html']
+ ],
+
+ otherTag: [
+ [/\/?>/, 'delimiter.html', '@pop'],
+ [/"([^"]*)"/, 'attribute.value.html'],
+ [/'([^']*)'/, 'attribute.value.html'],
+ [/[\w\-]+/, 'attribute.name.html'],
+ [/=/, 'delimiter.html'],
+ [/[ \t\r\n]+/], // whitespace
+ ],
+
+ // -- BEGIN