Skip to content

Commit

Permalink
enh(handlebars) Support for sub-expressions, path-expressions, hashes…
Browse files Browse the repository at this point in the history
…, block-parameters and literals (#2344)

- `htmlbars` grammar is now deprecated. Use `handlebars` instead.

A stub is included so that anyone literally referencing the old `htmlbars` file (say manually requiring it in Node.js, etc) is still covered, but everyone should transition to `handlebars` now.
  • Loading branch information
nknapp committed May 19, 2020
1 parent 63f367c commit e9e7b44
Show file tree
Hide file tree
Showing 30 changed files with 408 additions and 138 deletions.
3 changes: 3 additions & 0 deletions CHANGES.md
Expand Up @@ -13,6 +13,7 @@ Parser Engine:

Deprecations:

- `htmlbars` grammar is now deprecated. Use `handlebars` instead. (#2344) [Nils Knappmeier][]
- when using `highlightBlock` `result.re` deprecated. Use `result.relevance` instead. (#2552) [Josh Goebel][]
- ditto for `result.second_best.re` => `result.second_best.relevance` (#2552)
- `lexemes` is now deprecated in favor of `keywords.$pattern` key (#2519) [Josh Goebel][]
Expand All @@ -37,6 +38,7 @@ Language Improvements:
- fix(swift) `@objcMembers` was being partially highlighted (#2543) [Nick Randall][]
- enh(dart) Add `late` and `required` keywords, and `Never` built-in type (#2550) [Sam Rawlins][]
- enh(erlang) Add underscore separators to numeric literals (#2554) [Sergey Prokhorov][]
- enh(handlebars) Support for sub-expressions, path-expressions, hashes, block-parameters and literals (#2344) [Nils Knappmeier][]

[Josh Goebel]: https://github.com/yyyc514
[Peter Plantinga]: https://github.com/pplantinga
Expand All @@ -46,6 +48,7 @@ Language Improvements:
[Nick Randall]: https://github.com/nicked
[Sam Rawlins]: https://github.com/srawlins
[Sergey Prokhorov]: https://github.com/seriyps
[Nils Knappmeier]: https://github.com/nknapp


## Version 10.0.2
Expand Down
213 changes: 187 additions & 26 deletions src/languages/handlebars.js
Expand Up @@ -7,38 +7,183 @@ Website: https://handlebarsjs.com
Category: template
*/

import * as regex from '../lib/regex'

export default function(hljs) {
var BUILT_INS = {'builtin-name': 'each in with if else unless bindattr action collection debugger log outlet template unbound view yield lookup'};
const BUILT_INS = {
'builtin-name': [
'action',
'bindattr',
'collection',
'component',
'concat',
'debugger',
'each',
'each-in',
'get',
'hash',
'if',
'in',
'input',
'link-to',
'loc',
'log',
'lookup',
'mut',
'outlet',
'partial',
'query-params',
'render',
'template',
'textarea',
'unbound',
'unless',
'view',
'with',
'yield'
].join(" ")
};

var IDENTIFIER_PLAIN_OR_QUOTED = {
begin: /".*?"|'.*?'|\[.*?\]|\w+/
const LITERALS = {
literal: [
'true',
'false',
'undefined',
'null'
].join(" ")
};

var EXPRESSION_OR_HELPER_CALL = hljs.inherit(IDENTIFIER_PLAIN_OR_QUOTED, {
keywords: BUILT_INS,
// as defined in https://handlebarsjs.com/guide/expressions.html#literal-segments
// this regex matches literal segments like ' abc ' or [ abc ] as well as helpers and paths
// like a/b, ./abc/cde, and abc.bcd

const DOUBLE_QUOTED_ID_REGEX=/".*?"/;
const SINGLE_QUOTED_ID_REGEX=/'.*?'/;
const BRACKET_QUOTED_ID_REGEX=/\[.*?\]/;
const PLAIN_ID_REGEX=/[^\s!"#%&'()*+,.\/;<=>@\[\\\]^`{|}~]+/;
const PATH_DELIMITER_REGEX=/\.|\//;

const IDENTIFIER_REGEX = regex.concat(
'(',
SINGLE_QUOTED_ID_REGEX, '|',
DOUBLE_QUOTED_ID_REGEX, '|',
BRACKET_QUOTED_ID_REGEX, '|',
PLAIN_ID_REGEX, '|',
PATH_DELIMITER_REGEX,
')+'
);

// identifier followed by a equal-sign (without the equal sign)
const HASH_PARAM_REGEX = regex.concat(
'(',
BRACKET_QUOTED_ID_REGEX, '|',
PLAIN_ID_REGEX,
')(?==)'
);

const HELPER_NAME_OR_PATH_EXPRESSION = {
begin: IDENTIFIER_REGEX,
lexemes: /[\w.\/]+/
};

const HELPER_PARAMETER = hljs.inherit(HELPER_NAME_OR_PATH_EXPRESSION, {
keywords: LITERALS
});

const SUB_EXPRESSION = {
begin: /\(/,
end: /\)/
// the "contains" is added below when all necessary sub-modes are defined
};

const HASH = {
// fka "attribute-assignment", parameters of the form 'key=value'
className: 'attr',
begin: HASH_PARAM_REGEX,
relevance: 0,
starts: {
// helper params
endsWithParent: true,
relevance: 0,
contains: [hljs.inherit(IDENTIFIER_PLAIN_OR_QUOTED, {relevance: 0})]
begin: /=/,
end: /=/,
starts: {
contains: [
hljs.NUMBER_MODE,
hljs.QUOTE_STRING_MODE,
hljs.APOS_STRING_MODE,
HELPER_PARAMETER,
SUB_EXPRESSION
]
}
}
};

const BLOCK_PARAMS = {
// parameters of the form '{{#with x as | y |}}...{{/with}}'
begin: /as\s+\|/,
keywords: { keyword: 'as' },
end: /\|/,
contains: [
{
// define sub-mode in order to prevent highlighting of block-parameter named "as"
begin: /\w+/
}
]
};

const HELPER_PARAMETERS = {
contains: [
hljs.NUMBER_MODE,
hljs.QUOTE_STRING_MODE,
hljs.APOS_STRING_MODE,
BLOCK_PARAMS,
HASH,
HELPER_PARAMETER,
SUB_EXPRESSION
],
returnEnd: true
// the property "end" is defined through inheritance when the mode is used. If depends
// on the surrounding mode, but "endsWithParent" does not work here (i.e. it includes the
// end-token of the surrounding mode)
};

const SUB_EXPRESSION_CONTENTS = hljs.inherit(HELPER_NAME_OR_PATH_EXPRESSION, {
className: 'name',
keywords: BUILT_INS,
starts: hljs.inherit(HELPER_PARAMETERS, {
end: /\)/,
})
});

var BLOCK_MUSTACHE_CONTENTS = hljs.inherit(EXPRESSION_OR_HELPER_CALL, {
SUB_EXPRESSION.contains = [
SUB_EXPRESSION_CONTENTS
];

const OPENING_BLOCK_MUSTACHE_CONTENTS = hljs.inherit(HELPER_NAME_OR_PATH_EXPRESSION, {
keywords: BUILT_INS,
className: 'name',
starts: hljs.inherit(HELPER_PARAMETERS, {
end: /}}/,
})
});

const CLOSING_BLOCK_MUSTACHE_CONTENTS = hljs.inherit(HELPER_NAME_OR_PATH_EXPRESSION, {
keywords: BUILT_INS,
className: 'name'
});

var BASIC_MUSTACHE_CONTENTS = hljs.inherit(EXPRESSION_OR_HELPER_CALL, {
// relevance 0 for backward compatibility concerning auto-detection
relevance: 0
const BASIC_MUSTACHE_CONTENTS = hljs.inherit(HELPER_NAME_OR_PATH_EXPRESSION, {
className: 'name',
keywords: BUILT_INS,
starts: hljs.inherit(HELPER_PARAMETERS, {
end: /}}/,
})
});

var ESCAPE_MUSTACHE_WITH_PRECEEDING_BACKSLASH = {begin: /\\\{\{/, skip: true};
var PREVENT_ESCAPE_WITH_ANOTHER_PRECEEDING_BACKSLASH = {begin: /\\\\(?=\{\{)/, skip: true};
const ESCAPE_MUSTACHE_WITH_PRECEEDING_BACKSLASH = {begin: /\\\{\{/, skip: true};
const PREVENT_ESCAPE_WITH_ANOTHER_PRECEEDING_BACKSLASH = {begin: /\\\\(?=\{\{)/, skip: true};

return {
name: 'Handlebars',
aliases: ['hbs', 'html.hbs', 'html.handlebars'],
aliases: ['hbs', 'html.hbs', 'html.handlebars', 'htmlbars'],
case_insensitive: true,
subLanguage: 'xml',
contains: [
Expand All @@ -49,34 +194,50 @@ export default function(hljs) {
{
// open raw block "{{{{raw}}}} content not evaluated {{{{/raw}}}}"
className: 'template-tag',
begin: /\{\{\{\{(?!\/)/, end: /\}\}\}\}/,
contains: [BLOCK_MUSTACHE_CONTENTS],
begin: /\{\{\{\{(?!\/)/,
end: /\}\}\}\}/,
contains: [OPENING_BLOCK_MUSTACHE_CONTENTS],
starts: {end: /\{\{\{\{\//, returnEnd: true, subLanguage: 'xml'}
},
{
// close raw block
className: 'template-tag',
begin: /\{\{\{\{\//, end: /\}\}\}\}/,
contains: [BLOCK_MUSTACHE_CONTENTS]
begin: /\{\{\{\{\//,
end: /\}\}\}\}/,
contains: [CLOSING_BLOCK_MUSTACHE_CONTENTS]
},
{
// open block statement
className: 'template-tag',
begin: /\{\{[#\/]/, end: /\}\}/,
contains: [BLOCK_MUSTACHE_CONTENTS],
begin: /\{\{#/,
end: /\}\}/,
contains: [OPENING_BLOCK_MUSTACHE_CONTENTS],
},
{
className: 'template-tag',
begin: /\{\{(?=else\}\})/,
end: /\}\}/,
keywords: 'else'
},
{
// closing block statement
className: 'template-tag',
begin: /\{\{\//,
end: /\}\}/,
contains: [CLOSING_BLOCK_MUSTACHE_CONTENTS],
},
{
// template variable or helper-call that is NOT html-escaped
className: 'template-variable',
begin: /\{\{\{/, end: /\}\}\}/,
keywords: BUILT_INS,
begin: /\{\{\{/,
end: /\}\}\}/,
contains: [BASIC_MUSTACHE_CONTENTS]
},
{
// template variable or helper-call that is html-escaped
className: 'template-variable',
begin: /\{\{/, end: /\}\}/,
keywords: BUILT_INS,
begin: /\{\{/,
end: /\}\}/,
contains: [BASIC_MUSTACHE_CONTENTS]
}
]
Expand Down

0 comments on commit e9e7b44

Please sign in to comment.