Alex Dima 8 lat temu
rodzic
commit
d0f7359bc0
8 zmienionych plików z 539 dodań i 1 usunięć
  1. 1 0
      README.md
  2. 1 0
      gulpfile.js
  3. 1 1
      package.json
  4. 236 0
      src/handlebars.ts
  5. 7 0
      src/monaco.contribution.ts
  6. 1 0
      test/all.js
  7. 290 0
      test/handlebars.test.ts
  8. 2 0
      tsconfig.json

+ 1 - 0
README.md

@@ -10,6 +10,7 @@ Colorization and configuration supports for multiple languages for the Monaco Ed
 * csharp
 * fsharp
 * go
+* handlebars
 * html
 * ini
 * jade

+ 1 - 0
gulpfile.js

@@ -53,6 +53,7 @@ gulp.task('release', ['clean-release','compile'], function() {
 			bundleOne('src/dockerfile'),
 			bundleOne('src/fsharp'),
 			bundleOne('src/go'),
+			bundleOne('src/handlebars'),
 			bundleOne('src/html'),
 			bundleOne('src/ini'),
 			bundleOne('src/jade'),

+ 1 - 1
package.json

@@ -26,7 +26,7 @@
     "jsdom-no-contextify": "^3.1.0",
     "merge-stream": "^1.0.0",
     "mocha": "^2.5.3",
-    "monaco-editor-core": "0.7.0-next.2",
+    "monaco-editor-core": "0.7.0-next.3",
     "object-assign": "^4.1.0",
     "rimraf": "^2.5.2",
     "typescript": "^1.8.10",

+ 236 - 0
src/handlebars.ts

@@ -0,0 +1,236 @@
+/*---------------------------------------------------------------------------------------------
+ *  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;
+
+// Allow for running under nodejs/requirejs in tests
+var _monaco: typeof monaco = (typeof monaco === 'undefined' ? (<any>self).monaco : monaco);
+
+const EMPTY_ELEMENTS:string[] = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'];
+
+export var conf:IRichLanguageConfiguration = {
+	wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,
+
+	comments: {
+		blockComment: ['<!--', '-->']
+	},
+
+	brackets: [
+		['<!--', '-->'],
+		['{{', '}}']
+	],
+
+	__electricCharacterSupport: {
+		embeddedElectricCharacters: ['*', '}', ']', ')']
+	},
+
+	autoClosingPairs: [
+		{ 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: _monaco.languages.IndentAction.IndentOutdent }
+		},
+		{
+			beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'),
+			action: { indentAction: _monaco.languages.IndentAction.Indent }
+		}
+	],
+}
+
+export const htmlTokenTypes = {
+	DELIM_START: 'start.delimiter.tag.html',
+	DELIM_END: 'end.delimiter.tag.html',
+	DELIM_COMMENT: 'comment.html',
+	COMMENT: 'comment.content.html',
+	getTag: (name: string) => {
+		return 'tag.html';
+	}
+};
+
+export var language = <ILanguage> {
+	defaultToken: '',
+	tokenPostfix: '',
+	// ignoreCase: true,
+
+	// The main tokenizer for our languages
+	tokenizer: {
+		root: [
+			[/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.root' }],
+			[/<!DOCTYPE/, 'metatag.html', '@doctype'],
+			[/<!--/, 'comment.html', '@comment'],
+			[/(<)(\w+)(\/>)/, [htmlTokenTypes.DELIM_START, 'tag.html', htmlTokenTypes.DELIM_END]],
+			[/(<)(script)/, [htmlTokenTypes.DELIM_START, { token: 'tag.html', next: '@script'} ]],
+			[/(<)(style)/, [htmlTokenTypes.DELIM_START, { token: 'tag.html', next: '@style'} ]],
+			[/(<)([:\w]+)/, [htmlTokenTypes.DELIM_START, { token: 'tag.html', next: '@otherTag'} ]],
+			[/(<\/)(\w+)/, [htmlTokenTypes.DELIM_START, { token: 'tag.html', next: '@otherTag' }]],
+			[/</, htmlTokenTypes.DELIM_START],
+			[/\{/, htmlTokenTypes.DELIM_START],
+			[/[^<{]+/] // text
+		],
+
+		doctype: [
+			[/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.comment' }],
+			[/[^>]+/, 'metatag.content.html' ],
+			[/>/, 'metatag.html', '@pop' ],
+		],
+
+		comment: [
+			[/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.comment' }],
+			[/-->/, 'comment.html', '@pop'],
+			[/[^-]+/, 'comment.content.html'],
+			[/./, 'comment.content.html']
+		],
+
+		otherTag: [
+			[/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.otherTag' }],
+			[/\/?>/, htmlTokenTypes.DELIM_END, '@pop'],
+			[/"([^"]*)"/, 'attribute.value'],
+			[/'([^']*)'/, 'attribute.value'],
+			[/[\w\-]+/, 'attribute.name'],
+			[/=/, 'delimiter'],
+			[/[ \t\r\n]+/], // whitespace
+		],
+
+		// -- BEGIN <script> tags handling
+
+		// After <script
+		script: [
+			[/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.script' }],
+			[/type/, 'attribute.name', '@scriptAfterType'],
+			[/"([^"]*)"/, 'attribute.value'],
+			[/'([^']*)'/, 'attribute.value'],
+			[/[\w\-]+/, 'attribute.name'],
+			[/=/, 'delimiter'],
+			[/>/, { token: htmlTokenTypes.DELIM_END, next: '@scriptEmbedded.text/javascript', nextEmbedded: 'text/javascript'} ],
+			[/[ \t\r\n]+/], // whitespace
+			[/(<\/)(script\s*)(>)/, [ htmlTokenTypes.DELIM_START, 'tag.html', { token: htmlTokenTypes.DELIM_END, next: '@pop' } ]]
+		],
+
+		// After <script ... type
+		scriptAfterType: [
+			[/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.scriptAfterType' }],
+			[/=/,'delimiter', '@scriptAfterTypeEquals'],
+			[/[ \t\r\n]+/], // whitespace
+			[/<\/script\s*>/, { token: '@rematch', next: '@pop' }]
+		],
+
+		// After <script ... type =
+		scriptAfterTypeEquals: [
+			[/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.scriptAfterTypeEquals' }],
+			[/"([^"]*)"/, { token: 'attribute.value', switchTo: '@scriptWithCustomType.$1' } ],
+			[/'([^']*)'/, { token: 'attribute.value', switchTo: '@scriptWithCustomType.$1' } ],
+			[/[ \t\r\n]+/], // whitespace
+			[/<\/script\s*>/, { token: '@rematch', next: '@pop' }]
+		],
+
+		// After <script ... type = $S2
+		scriptWithCustomType: [
+			[/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.scriptWithCustomType.$S2' }],
+			[/>/, { token: htmlTokenTypes.DELIM_END, next: '@scriptEmbedded.$S2', nextEmbedded: '$S2'}],
+			[/"([^"]*)"/, 'attribute.value'],
+			[/'([^']*)'/, 'attribute.value'],
+			[/[\w\-]+/, 'attribute.name'],
+			[/=/, 'delimiter'],
+			[/[ \t\r\n]+/], // whitespace
+			[/<\/script\s*>/, { token: '@rematch', next: '@pop' }]
+		],
+
+		scriptEmbedded: [
+			[/\{\{/, { token: '@rematch', switchTo: '@handlebarsInEmbeddedState.scriptEmbedded.$S2', nextEmbedded: '@pop' }],
+			[/<\/script/, { token: '@rematch', next: '@pop', nextEmbedded: '@pop' }]
+		],
+
+		// -- END <script> tags handling
+
+
+		// -- BEGIN <style> tags handling
+
+		// After <style
+		style: [
+			[/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.style' }],
+			[/type/, 'attribute.name', '@styleAfterType'],
+			[/"([^"]*)"/, 'attribute.value'],
+			[/'([^']*)'/, 'attribute.value'],
+			[/[\w\-]+/, 'attribute.name'],
+			[/=/, 'delimiter'],
+			[/>/, { token: htmlTokenTypes.DELIM_END, next: '@styleEmbedded.text/css', nextEmbedded: 'text/css'} ],
+			[/[ \t\r\n]+/], // whitespace
+			[/(<\/)(style\s*)(>)/, [htmlTokenTypes.DELIM_START, 'tag.html', { token: htmlTokenTypes.DELIM_END, next: '@pop' } ]]
+		],
+
+		// After <style ... type
+		styleAfterType: [
+			[/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.styleAfterType' }],
+			[/=/,'delimiter', '@styleAfterTypeEquals'],
+			[/[ \t\r\n]+/], // whitespace
+			[/<\/style\s*>/, { token: '@rematch', next: '@pop' }]
+		],
+
+		// After <style ... type =
+		styleAfterTypeEquals: [
+			[/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.styleAfterTypeEquals' }],
+			[/"([^"]*)"/, { token: 'attribute.value', switchTo: '@styleWithCustomType.$1' } ],
+			[/'([^']*)'/, { token: 'attribute.value', switchTo: '@styleWithCustomType.$1' } ],
+			[/[ \t\r\n]+/], // whitespace
+			[/<\/style\s*>/, { token: '@rematch', next: '@pop' }]
+		],
+
+		// After <style ... type = $S2
+		styleWithCustomType: [
+			[/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.styleWithCustomType.$S2' }],
+			[/>/, { token: htmlTokenTypes.DELIM_END, next: '@styleEmbedded.$S2', nextEmbedded: '$S2'}],
+			[/"([^"]*)"/, 'attribute.value'],
+			[/'([^']*)'/, 'attribute.value'],
+			[/[\w\-]+/, 'attribute.name'],
+			[/=/, 'delimiter'],
+			[/[ \t\r\n]+/], // whitespace
+			[/<\/style\s*>/, { token: '@rematch', next: '@pop' }]
+		],
+
+		styleEmbedded: [
+			[/\{\{/, { token: '@rematch', switchTo: '@handlebarsInEmbeddedState.styleEmbedded.$S2', nextEmbedded: '@pop' }],
+			[/<\/style/, { token: '@rematch', next: '@pop', nextEmbedded: '@pop' }]
+		],
+
+		// -- END <style> tags handling
+
+
+		handlebarsInSimpleState: [
+			[/\{\{\{?/, 'metatag.handlebars'],
+			[/\}\}\}?/, { token: 'metatag.handlebars', switchTo: '@$S2.$S3' }],
+			{ include: 'handlebarsRoot' }
+		],
+
+		handlebarsInEmbeddedState: [
+			[/\{\{\{?/, 'metatag.handlebars'],
+			[/\}\}\}?/, { token: 'metatag.handlebars', switchTo: '@$S2.$S3', nextEmbedded: '$S3' }],
+			{ include: 'handlebarsRoot' }
+		],
+
+		handlebarsRoot: [
+			[/[#/][^\s}]+/, 'keyword.helper.handlebars'],
+			[/else\b/, 'keyword.helper.handlebars'],
+			[/[\s]+/],
+			[/[^}]/, 'variable.parameter.handlebars'],
+		],
+	},
+};

+ 7 - 0
src/monaco.contribution.ts

@@ -101,6 +101,13 @@ registerLanguage({
 	aliases: [ 'Go' ],
 	module: './go'
 });
+registerLanguage({
+	id: 'handlebars',
+	extensions: ['.handlebars', '.hbs'],
+	aliases: ['Handlebars', 'handlebars'],
+	mimetypes: ['text/x-handlebars-template'],
+	module: './handlebars'
+});
 registerLanguage({
 	id: 'html',
 	extensions: ['.html', '.htm', '.shtml', '.xhtml', '.mdoc', '.jsp', '.asp', '.aspx', '.jshtm'],

+ 1 - 0
test/all.js

@@ -30,6 +30,7 @@ requirejs([
 		'out/test/dockerfile.test',
 		'out/test/fsharp.test',
 		'out/test/go.test',
+		'out/test/handlebars.test',
 		'out/test/html.test',
 		'out/test/jade.test',
 		'out/test/java.test',

+ 290 - 0
test/handlebars.test.ts

@@ -0,0 +1,290 @@
+/*---------------------------------------------------------------------------------------------
+ *  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 './testRunner';
+import {htmlTokenTypes} from '../src/handlebars';
+
+const HTML_DELIM_START = htmlTokenTypes.DELIM_START;
+const HTML_DELIM_END = htmlTokenTypes.DELIM_END;
+const DELIM_ASSIGN = 'delimiter';
+const HTML_ATTRIB_NAME = 'attribute.name';
+const HTML_ATTRIB_VALUE = 'attribute.value';
+function getTag(name: string) {
+	return htmlTokenTypes.getTag(name);
+}
+
+const handlebarsTokenTypes = {
+	EMBED: 'metatag.handlebars',
+	EMBED_UNESCAPED: 'metatag.handlebars',
+	KEYWORD: 'keyword.helper.handlebars',
+	VARIABLE: 'variable.parameter.handlebars',
+}
+
+testTokenization(['handlebars', 'css'], [
+
+	// Just HTML
+	[{
+		line: '<h1>handlebars!</h1>',
+		tokens: [
+			{ startIndex:0, type: HTML_DELIM_START },
+			{ startIndex:1, type: getTag('h1') },
+			{ startIndex:3, type: HTML_DELIM_END },
+			{ startIndex:4, type: '' },
+			{ startIndex:15, type: HTML_DELIM_START },
+			{ startIndex:17, type: getTag('h1') },
+			{ startIndex:19, type: HTML_DELIM_END }
+		]
+	}],
+
+	// Expressions
+	[{
+		line: '<h1>{{ title }}</h1>',
+		tokens: [
+			{ startIndex:0, type: HTML_DELIM_START },
+			{ startIndex:1, type: getTag('h1') },
+			{ startIndex:3, type: HTML_DELIM_END },
+			{ startIndex:4, type: handlebarsTokenTypes.EMBED },
+			{ startIndex:6, type: '' },
+			{ startIndex:7, type: handlebarsTokenTypes.VARIABLE },
+			{ startIndex:12, type: '' },
+			{ startIndex:13, type: handlebarsTokenTypes.EMBED },
+			{ startIndex:15, type: HTML_DELIM_START },
+			{ startIndex:17, type: getTag('h1') },
+			{ startIndex:19, type: HTML_DELIM_END }
+		]
+	}],
+
+	// Expressions Sans Whitespace
+	[{
+		line: '<h1>{{title}}</h1>',
+		tokens: [
+			{ startIndex:0, type: HTML_DELIM_START },
+			{ startIndex:1, type: getTag('h1') },
+			{ startIndex:3, type: HTML_DELIM_END },
+			{ startIndex:4, type: handlebarsTokenTypes.EMBED },
+			{ startIndex:6, type: handlebarsTokenTypes.VARIABLE },
+			{ startIndex:11, type: handlebarsTokenTypes.EMBED },
+			{ startIndex:13, type: HTML_DELIM_START },
+			{ startIndex:15, type: getTag('h1') },
+			{ startIndex:17, type: HTML_DELIM_END }
+		]
+	}],
+
+	// Unescaped Expressions
+	[{
+		line: '<h1>{{{ title }}}</h1>',
+		tokens: [
+			{ startIndex:0, type: HTML_DELIM_START },
+			{ startIndex:1, type: getTag('h1') },
+			{ startIndex:3, type: HTML_DELIM_END },
+			{ startIndex:4, type: handlebarsTokenTypes.EMBED_UNESCAPED },
+			{ startIndex:7, type: '' },
+			{ startIndex:8, type: handlebarsTokenTypes.VARIABLE },
+			{ startIndex:13, type: '' },
+			{ startIndex:14, type: handlebarsTokenTypes.EMBED_UNESCAPED },
+			{ startIndex:17, type: HTML_DELIM_START },
+			{ startIndex:19, type: getTag('h1') },
+			{ startIndex:21, type: HTML_DELIM_END }
+		]
+	}],
+
+	// Blocks
+	[{
+		line: '<ul>{{#each items}}<li>{{item}}</li>{{/each}}</ul>',
+		tokens: [
+			{ startIndex:0, type: HTML_DELIM_START },
+			{ startIndex:1, type: getTag('ul') },
+			{ startIndex:3, type: HTML_DELIM_END },
+			{ startIndex:4, type: handlebarsTokenTypes.EMBED },
+			{ startIndex:6, type: handlebarsTokenTypes.KEYWORD },
+			{ startIndex:11, type: '' },
+			{ startIndex:12, type: handlebarsTokenTypes.VARIABLE },
+			{ startIndex:17, type: handlebarsTokenTypes.EMBED },
+			{ startIndex:19, type: HTML_DELIM_START },
+			{ startIndex:20, type: getTag('li') },
+			{ startIndex:22, type: HTML_DELIM_END },
+			{ startIndex:23, type: handlebarsTokenTypes.EMBED },
+			{ startIndex:25, type: handlebarsTokenTypes.VARIABLE },
+			{ startIndex:29, type: handlebarsTokenTypes.EMBED },
+			{ startIndex:31, type: HTML_DELIM_START },
+			{ startIndex:33, type: getTag('li') },
+			{ startIndex:35, type: HTML_DELIM_END },
+			{ startIndex:36, type: handlebarsTokenTypes.EMBED },
+			{ startIndex:38, type: handlebarsTokenTypes.KEYWORD },
+			{ startIndex:43, type: handlebarsTokenTypes.EMBED },
+			{ startIndex:45, type: HTML_DELIM_START },
+			{ startIndex:47, type: getTag('ul') },
+			{ startIndex:49, type: HTML_DELIM_END }
+		]
+	}],
+
+	// Multiline
+	[{
+		line: '<div>',
+		tokens: [
+			{ startIndex:0, type: HTML_DELIM_START },
+			{ startIndex:1, type: getTag('div') },
+			{ startIndex:4, type: HTML_DELIM_END }
+		]
+	}, {
+		line: '{{#if foo}}',
+		tokens: [
+			{ startIndex:0, type: handlebarsTokenTypes.EMBED },
+			{ startIndex:2, type: handlebarsTokenTypes.KEYWORD },
+			{ startIndex:5, type: '' },
+			{ startIndex:6, type: handlebarsTokenTypes.VARIABLE },
+			{ startIndex:9, type: handlebarsTokenTypes.EMBED }
+		]
+	}, {
+		line: '<span>{{bar}}</span>',
+		tokens: [
+			{ startIndex:0, type: HTML_DELIM_START },
+			{ startIndex:1, type: getTag('span') },
+			{ startIndex:5, type: HTML_DELIM_END },
+			{ startIndex:6, type: handlebarsTokenTypes.EMBED },
+			{ startIndex:8, type: handlebarsTokenTypes.VARIABLE },
+			{ startIndex:11, type: handlebarsTokenTypes.EMBED },
+			{ startIndex:13, type: HTML_DELIM_START },
+			{ startIndex:15, type: getTag('span') },
+			{ startIndex:19, type: HTML_DELIM_END }
+		]
+	}, {
+		line: '{{/if}}',
+		tokens: [
+			{ startIndex:0, type: handlebarsTokenTypes.EMBED },
+			{ startIndex:2, type: handlebarsTokenTypes.KEYWORD },
+			{ startIndex:5, type: handlebarsTokenTypes.EMBED }
+		]
+	}],
+
+	// Div end
+	[{
+		line: '</div>',
+		tokens: [
+			{ startIndex:0, type: HTML_DELIM_START },
+			{ startIndex:2, type: getTag('div') },
+			{ startIndex:5, type: HTML_DELIM_END }
+		]
+	}],
+
+	// HTML Expressions
+	[{
+		line: '<script type="text/x-handlebars-template"><h1>{{ title }}</h1></script>',
+		tokens: [
+			{ startIndex:0, type: HTML_DELIM_START },
+			{ startIndex:1, type: getTag('script') },
+			{ startIndex:7, type: '' },
+			{ startIndex:8, type: HTML_ATTRIB_NAME },
+			{ startIndex:12, type: DELIM_ASSIGN },
+			{ startIndex:13, type: HTML_ATTRIB_VALUE },
+			{ startIndex:41, type: HTML_DELIM_END },
+			{ startIndex:42, type: HTML_DELIM_START },
+			{ startIndex:43, type: getTag('h1') },
+			{ startIndex:45, type: HTML_DELIM_END },
+			{ startIndex:46, type: handlebarsTokenTypes.EMBED },
+			{ startIndex:48, type: '' },
+			{ startIndex:49, type: handlebarsTokenTypes.VARIABLE },
+			{ startIndex:54, type: '' },
+			{ startIndex:55, type: handlebarsTokenTypes.EMBED },
+			{ startIndex:57, type: HTML_DELIM_START },
+			{ startIndex:59, type: getTag('h1') },
+			{ startIndex:61, type: HTML_DELIM_END },
+			{ startIndex:62, type: HTML_DELIM_START },
+			{ startIndex:64, type: getTag('script') },
+			{ startIndex:70, type: HTML_DELIM_END }
+		]
+	}],
+
+	// Multi-line HTML Expressions
+	[{
+		line: '<script type="text/x-handlebars-template">',
+		tokens: [
+			{ startIndex:0, type: HTML_DELIM_START },
+			{ startIndex:1, type: getTag('script') },
+			{ startIndex:7, type: '' },
+			{ startIndex:8, type: HTML_ATTRIB_NAME },
+			{ startIndex:12, type: DELIM_ASSIGN },
+			{ startIndex:13, type: HTML_ATTRIB_VALUE },
+			{ startIndex:41, type: HTML_DELIM_END }
+		]
+	}, {
+		line: '<h1>{{ title }}</h1>',
+		tokens: [
+			{ startIndex:0, type: HTML_DELIM_START },
+			{ startIndex:1, type: getTag('h1') },
+			{ startIndex:3, type: HTML_DELIM_END },
+			{ startIndex:4, type: handlebarsTokenTypes.EMBED },
+			{ startIndex:6, type: '' },
+			{ startIndex:7, type: handlebarsTokenTypes.VARIABLE },
+			{ startIndex:12, type: '' },
+			{ startIndex:13, type: handlebarsTokenTypes.EMBED },
+			{ startIndex:15, type: HTML_DELIM_START },
+			{ startIndex:17, type: getTag('h1') },
+			{ startIndex:19, type: HTML_DELIM_END }
+		]
+	}, {
+		line: '</script>',
+		tokens: [
+			{ startIndex:0, type: HTML_DELIM_START },
+			{ startIndex:2, type: getTag('script') },
+			{ startIndex:8, type: HTML_DELIM_END }
+		]
+	}],
+
+	// HTML Nested Modes
+	[{
+		line: '{{foo}}<script></script>{{bar}}',
+		tokens: [
+			{ startIndex:0, type: handlebarsTokenTypes.EMBED },
+			{ startIndex:2, type: handlebarsTokenTypes.VARIABLE },
+			{ startIndex:5, type: handlebarsTokenTypes.EMBED },
+			{ startIndex:7, type: HTML_DELIM_START },
+			{ startIndex:8, type: getTag('script') },
+			{ startIndex:14, type: HTML_DELIM_END },
+			{ startIndex:15, type: HTML_DELIM_START },
+			{ startIndex:17, type: getTag('script') },
+			{ startIndex:23, type: HTML_DELIM_END },
+			{ startIndex:24, type: handlebarsTokenTypes.EMBED },
+			{ startIndex:26, type: handlebarsTokenTypes.VARIABLE },
+			{ startIndex:29, type: handlebarsTokenTypes.EMBED }
+		]
+	}],
+
+	// else keyword
+	[{
+		line: '{{else}}',
+		tokens: [
+			{ startIndex:0, type: handlebarsTokenTypes.EMBED },
+			{ startIndex:2, type: handlebarsTokenTypes.KEYWORD },
+			{ startIndex:6, type: handlebarsTokenTypes.EMBED }
+		]
+	}],
+
+	// else keyword #2
+	[{
+		line: '{{elseFoo}}',
+		tokens: [
+			{ startIndex:0, type: handlebarsTokenTypes.EMBED },
+			{ startIndex:2, type: handlebarsTokenTypes.VARIABLE },
+			{ startIndex:9, type: handlebarsTokenTypes.EMBED }
+		]
+	}],
+
+	// Token inside attribute
+	[{
+		line: '<a href="/posts/{{permalink}}">',
+		tokens: [
+			{ startIndex:0, type: HTML_DELIM_START },
+			{ startIndex:1, type: getTag('a') },
+			{ startIndex:2, type: '' },
+			{ startIndex:3, type: HTML_ATTRIB_NAME },
+			{ startIndex:7, type: DELIM_ASSIGN },
+			{ startIndex:8, type: HTML_ATTRIB_VALUE },
+			{ startIndex:30, type: HTML_DELIM_END }
+		]
+	}]
+]);

+ 2 - 0
tsconfig.json

@@ -18,6 +18,7 @@
     "src/dockerfile.ts",
     "src/fsharp.ts",
     "src/go.ts",
+    "src/handlebars.ts",
     "src/html.ts",
     "src/ini.ts",
     "src/jade.ts",
@@ -46,6 +47,7 @@
     "test/dockerfile.test.ts",
     "test/fsharp.test.ts",
     "test/go.test.ts",
+    "test/handlebars.test.ts",
     "test/html.test.ts",
     "test/jade.test.ts",
     "test/java.test.ts",