Add handlebars

pull/2748/head
Alex Dima 9 years ago
parent 83bf09fbef
commit d0f7359bc0

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

@ -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'),

@ -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",

@ -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'],
],
},
};

@ -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'],

@ -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',

@ -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 }
]
}]
]);

@ -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",

Loading…
Cancel
Save