Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(handlebars): add subexpressions, block-params and hash-parameters #2344

Merged
merged 15 commits into from May 19, 2020
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
110 changes: 33 additions & 77 deletions src/languages/htmlbars.js
@@ -1,86 +1,42 @@
/*
Language: HTMLBars
Requires: xml.js, handlebars.js
Author: Michael Johnston <lastobelus@gmail.com>
Description: Matcher for HTMLBars
Website: https://github.com/tildeio/htmlbars
Category: template
Language: HTMLBars (legacy)
Requires: xml.js
Description: Matcher for Handlebars as well as EmberJS additions.
Website: https://github.com/tildeio/htmlbars
Category: template
*/

/*

See: https://github.com/highlightjs/highlight.js/issues/2181

This file is a stub that is only left in place for compatbility reasons for
anyone who may be manually pulling in this file via a require or fetching
it individually via CDN.

TODO: Remove in version 11.0.

*/

export default function(hljs) {
// This work isn't complete yet but this is done so that this technically
// breaking change becomes a part of the 10.0 release and won't force
// us to prematurely release 11.0 just to break this.
var SHOULD_INHERIT_FROM_HANDLEBARS = hljs.requireLanguage('handlebars');
// https://github.com/highlightjs/highlight.js/issues/2181
// compile time dependency on handlebars
import handlebars from "./handlebars"

var BUILT_INS = 'action collection component concat debugger each each-in else get hash if input link-to loc log mut outlet partial query-params render textarea unbound unless with yield view';
export default function(hljs) {
const definition = handlebars(hljs)

var ATTR_ASSIGNMENT = {
illegal: /\}\}/,
begin: /[a-zA-Z0-9_]+=/,
returnBegin: true,
relevance: 0,
contains: [
{
className: 'attr', begin: /[a-zA-Z0-9_]+/
}
]
};
definition.name = "HTMLbars"

var SUB_EXPR = {
illegal: /\}\}/,
begin: /\)/, end: /\)/,
contains: [
{
begin: /[a-zA-Z\.\-]+/,
keywords: {built_in: BUILT_INS},
starts: {
endsWithParent: true, relevance: 0,
contains: [
hljs.QUOTE_STRING_MODE,
]
}
}
]
};
// HACK: This lets handlebars do the auto-detection if it's been loaded (by
// default the build script will load in alphabetical order) and if not (perhaps
// an install is only using `htmlbars`, not `handlebars`) then this will still
// allow HTMLBars to participate in the auto-detection

var TAG_INNARDS = {
endsWithParent: true, relevance: 0,
keywords: {keyword: 'as', built_in: BUILT_INS},
contains: [
hljs.QUOTE_STRING_MODE,
ATTR_ASSIGNMENT,
hljs.NUMBER_MODE
]
};
// worse case someone will have HTMLbars and handlebars competing for the same
// content and will need to change their setup to only require handlebars, but
// I don't consider this a breaking change
if (hljs.getLanguage("handlebars")) {
definition.disableAutodetect = true
}

return {
name: 'HTMLBars',
case_insensitive: true,
subLanguage: 'xml',
contains: [
hljs.COMMENT('{{!(--)?', '(--)?}}'),
{
className: 'template-tag',
begin: /\{\{[#\/]/, end: /\}\}/,
contains: [
{
className: 'name',
begin: /[a-zA-Z\.\-]+/,
keywords: {'builtin-name': BUILT_INS},
starts: TAG_INNARDS
}
]
},
{
className: 'template-variable',
begin: /\{\{[a-zA-Z][a-zA-Z\-]+/, end: /\}\}/,
keywords: {keyword: 'as', built_in: BUILT_INS},
contains: [
hljs.QUOTE_STRING_MODE
]
}
]
};
return definition
}
2 changes: 1 addition & 1 deletion src/lib/regex.js
Expand Up @@ -26,7 +26,7 @@ export function lookahead(re) {
}

/**
* @param {(RegExp | string)[] } args
* @param {...(RegExp | string) } args
* @returns {string}
*/
export function concat(...args) {
Expand Down
9 changes: 0 additions & 9 deletions test/detect/htmlbars/default.txt

This file was deleted.