htmlbars.js (7391B)
1 /** 2 * @param {string} value 3 * @returns {RegExp} 4 * */ 5 6 /** 7 * @param {RegExp | string } re 8 * @returns {string} 9 */ 10 function source(re) { 11 if (!re) return null; 12 if (typeof re === "string") return re; 13 14 return re.source; 15 } 16 17 /** 18 * @param {...(RegExp | string) } args 19 * @returns {string} 20 */ 21 function concat(...args) { 22 const joined = args.map((x) => source(x)).join(""); 23 return joined; 24 } 25 26 /* 27 Language: Handlebars 28 Requires: xml.js 29 Author: Robin Ward <robin.ward@gmail.com> 30 Description: Matcher for Handlebars as well as EmberJS additions. 31 Website: https://handlebarsjs.com 32 Category: template 33 */ 34 35 function handlebars(hljs) { 36 const BUILT_INS = { 37 'builtin-name': [ 38 'action', 39 'bindattr', 40 'collection', 41 'component', 42 'concat', 43 'debugger', 44 'each', 45 'each-in', 46 'get', 47 'hash', 48 'if', 49 'in', 50 'input', 51 'link-to', 52 'loc', 53 'log', 54 'lookup', 55 'mut', 56 'outlet', 57 'partial', 58 'query-params', 59 'render', 60 'template', 61 'textarea', 62 'unbound', 63 'unless', 64 'view', 65 'with', 66 'yield' 67 ].join(" ") 68 }; 69 70 const LITERALS = { 71 literal: [ 72 'true', 73 'false', 74 'undefined', 75 'null' 76 ].join(" ") 77 }; 78 79 // as defined in https://handlebarsjs.com/guide/expressions.html#literal-segments 80 // this regex matches literal segments like ' abc ' or [ abc ] as well as helpers and paths 81 // like a/b, ./abc/cde, and abc.bcd 82 83 const DOUBLE_QUOTED_ID_REGEX=/".*?"/; 84 const SINGLE_QUOTED_ID_REGEX=/'.*?'/; 85 const BRACKET_QUOTED_ID_REGEX=/\[.*?\]/; 86 const PLAIN_ID_REGEX=/[^\s!"#%&'()*+,.\/;<=>@\[\\\]^`{|}~]+/; 87 const PATH_DELIMITER_REGEX=/\.|\//; 88 89 const IDENTIFIER_REGEX = concat( 90 '(', 91 SINGLE_QUOTED_ID_REGEX, '|', 92 DOUBLE_QUOTED_ID_REGEX, '|', 93 BRACKET_QUOTED_ID_REGEX, '|', 94 PLAIN_ID_REGEX, '|', 95 PATH_DELIMITER_REGEX, 96 ')+' 97 ); 98 99 // identifier followed by a equal-sign (without the equal sign) 100 const HASH_PARAM_REGEX = concat( 101 '(', 102 BRACKET_QUOTED_ID_REGEX, '|', 103 PLAIN_ID_REGEX, 104 ')(?==)' 105 ); 106 107 const HELPER_NAME_OR_PATH_EXPRESSION = { 108 begin: IDENTIFIER_REGEX, 109 lexemes: /[\w.\/]+/ 110 }; 111 112 const HELPER_PARAMETER = hljs.inherit(HELPER_NAME_OR_PATH_EXPRESSION, { 113 keywords: LITERALS 114 }); 115 116 const SUB_EXPRESSION = { 117 begin: /\(/, 118 end: /\)/ 119 // the "contains" is added below when all necessary sub-modes are defined 120 }; 121 122 const HASH = { 123 // fka "attribute-assignment", parameters of the form 'key=value' 124 className: 'attr', 125 begin: HASH_PARAM_REGEX, 126 relevance: 0, 127 starts: { 128 begin: /=/, 129 end: /=/, 130 starts: { 131 contains: [ 132 hljs.NUMBER_MODE, 133 hljs.QUOTE_STRING_MODE, 134 hljs.APOS_STRING_MODE, 135 HELPER_PARAMETER, 136 SUB_EXPRESSION 137 ] 138 } 139 } 140 }; 141 142 const BLOCK_PARAMS = { 143 // parameters of the form '{{#with x as | y |}}...{{/with}}' 144 begin: /as\s+\|/, 145 keywords: { keyword: 'as' }, 146 end: /\|/, 147 contains: [ 148 { 149 // define sub-mode in order to prevent highlighting of block-parameter named "as" 150 begin: /\w+/ 151 } 152 ] 153 }; 154 155 const HELPER_PARAMETERS = { 156 contains: [ 157 hljs.NUMBER_MODE, 158 hljs.QUOTE_STRING_MODE, 159 hljs.APOS_STRING_MODE, 160 BLOCK_PARAMS, 161 HASH, 162 HELPER_PARAMETER, 163 SUB_EXPRESSION 164 ], 165 returnEnd: true 166 // the property "end" is defined through inheritance when the mode is used. If depends 167 // on the surrounding mode, but "endsWithParent" does not work here (i.e. it includes the 168 // end-token of the surrounding mode) 169 }; 170 171 const SUB_EXPRESSION_CONTENTS = hljs.inherit(HELPER_NAME_OR_PATH_EXPRESSION, { 172 className: 'name', 173 keywords: BUILT_INS, 174 starts: hljs.inherit(HELPER_PARAMETERS, { 175 end: /\)/, 176 }) 177 }); 178 179 SUB_EXPRESSION.contains = [ 180 SUB_EXPRESSION_CONTENTS 181 ]; 182 183 const OPENING_BLOCK_MUSTACHE_CONTENTS = hljs.inherit(HELPER_NAME_OR_PATH_EXPRESSION, { 184 keywords: BUILT_INS, 185 className: 'name', 186 starts: hljs.inherit(HELPER_PARAMETERS, { 187 end: /}}/, 188 }) 189 }); 190 191 const CLOSING_BLOCK_MUSTACHE_CONTENTS = hljs.inherit(HELPER_NAME_OR_PATH_EXPRESSION, { 192 keywords: BUILT_INS, 193 className: 'name' 194 }); 195 196 const BASIC_MUSTACHE_CONTENTS = hljs.inherit(HELPER_NAME_OR_PATH_EXPRESSION, { 197 className: 'name', 198 keywords: BUILT_INS, 199 starts: hljs.inherit(HELPER_PARAMETERS, { 200 end: /}}/, 201 }) 202 }); 203 204 const ESCAPE_MUSTACHE_WITH_PRECEEDING_BACKSLASH = {begin: /\\\{\{/, skip: true}; 205 const PREVENT_ESCAPE_WITH_ANOTHER_PRECEEDING_BACKSLASH = {begin: /\\\\(?=\{\{)/, skip: true}; 206 207 return { 208 name: 'Handlebars', 209 aliases: ['hbs', 'html.hbs', 'html.handlebars', 'htmlbars'], 210 case_insensitive: true, 211 subLanguage: 'xml', 212 contains: [ 213 ESCAPE_MUSTACHE_WITH_PRECEEDING_BACKSLASH, 214 PREVENT_ESCAPE_WITH_ANOTHER_PRECEEDING_BACKSLASH, 215 hljs.COMMENT(/\{\{!--/, /--\}\}/), 216 hljs.COMMENT(/\{\{!/, /\}\}/), 217 { 218 // open raw block "{{{{raw}}}} content not evaluated {{{{/raw}}}}" 219 className: 'template-tag', 220 begin: /\{\{\{\{(?!\/)/, 221 end: /\}\}\}\}/, 222 contains: [OPENING_BLOCK_MUSTACHE_CONTENTS], 223 starts: {end: /\{\{\{\{\//, returnEnd: true, subLanguage: 'xml'} 224 }, 225 { 226 // close raw block 227 className: 'template-tag', 228 begin: /\{\{\{\{\//, 229 end: /\}\}\}\}/, 230 contains: [CLOSING_BLOCK_MUSTACHE_CONTENTS] 231 }, 232 { 233 // open block statement 234 className: 'template-tag', 235 begin: /\{\{#/, 236 end: /\}\}/, 237 contains: [OPENING_BLOCK_MUSTACHE_CONTENTS], 238 }, 239 { 240 className: 'template-tag', 241 begin: /\{\{(?=else\}\})/, 242 end: /\}\}/, 243 keywords: 'else' 244 }, 245 { 246 // closing block statement 247 className: 'template-tag', 248 begin: /\{\{\//, 249 end: /\}\}/, 250 contains: [CLOSING_BLOCK_MUSTACHE_CONTENTS], 251 }, 252 { 253 // template variable or helper-call that is NOT html-escaped 254 className: 'template-variable', 255 begin: /\{\{\{/, 256 end: /\}\}\}/, 257 contains: [BASIC_MUSTACHE_CONTENTS] 258 }, 259 { 260 // template variable or helper-call that is html-escaped 261 className: 'template-variable', 262 begin: /\{\{/, 263 end: /\}\}/, 264 contains: [BASIC_MUSTACHE_CONTENTS] 265 } 266 ] 267 }; 268 } 269 270 /* 271 Language: HTMLBars (legacy) 272 Requires: xml.js 273 Description: Matcher for Handlebars as well as EmberJS additions. 274 Website: https://github.com/tildeio/htmlbars 275 Category: template 276 */ 277 278 function htmlbars(hljs) { 279 const definition = handlebars(hljs); 280 281 definition.name = "HTMLbars"; 282 283 // HACK: This lets handlebars do the auto-detection if it's been loaded (by 284 // default the build script will load in alphabetical order) and if not (perhaps 285 // an install is only using `htmlbars`, not `handlebars`) then this will still 286 // allow HTMLBars to participate in the auto-detection 287 288 // worse case someone will have HTMLbars and handlebars competing for the same 289 // content and will need to change their setup to only require handlebars, but 290 // I don't consider this a breaking change 291 if (hljs.getLanguage("handlebars")) { 292 definition.disableAutodetect = true; 293 } 294 295 return definition 296 } 297 298 module.exports = htmlbars;