Skip to content

Commit

Permalink
fix(template): use more robust template tag identification
Browse files Browse the repository at this point in the history
  • Loading branch information
NullVoxPopuli committed Nov 6, 2022
1 parent 3a5107f commit b6f3e05
Show file tree
Hide file tree
Showing 4 changed files with 284 additions and 11 deletions.
41 changes: 35 additions & 6 deletions demo/samples.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,22 +114,51 @@ export const gjsTemplateOnly = {
name: 'template-only .gjs',
language: 'js',
sample: `import { helper } from '@ember/component/helper';
import { modifier } from 'ember-modifier';
import { modifier } from 'ember-modifier';
const plusOne = helper(([num]) => num + 1);
const plusOne = helper(([num]) => num + 1);
const setScrollPosition = modifier((element, [position]) => {
const setScrollPosition = modifier((element, [position]) => {
element.scrollTop = position
});
});
<template>
<template>
<div class="scroll-container" {{setScrollPosition @scrollPos}}>
{{#each @items as |item index|}}
Item #{{plusOne index}}: {{item}}
{{/each}}
</div>
</template>
`,
};

export const multipleTemplateOnly = {
name: 'multiple template-only .gjs',
language: 'js',
sample: `import WeatherSummary from './weather-summary.js';
const Greeting = <template>
<p>Hello, {{@name}}!</p>
</template>;
function isBirthday(dateOfBirth) {
const now = new Date();
return (
dateOfBirth.getDate() === now.getDate() &&
dateOfBirth.getMonth() === now.getMonth()
);
}
<template>
<div>
<Greeting @name="Chris" />
{{#if (isBirthday @user.dateOfBirth)}}
<Celebration type='birthday' />
{{/if}}
<WeatherSummary />
</div>
</template>
`,
`,
};

/** @type { Sample } */
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"scripts": {
"prepare": "node ./scripts/build.cjs",
"build": "node ./scripts/build.cjs",
"debug": "npx html-pages .",
"debug": "npx html-pages . --no-cache",
"lint:js": "eslint .",
"lint:js:fix": "eslint . --fix",
"lint": "pnpm lint:js && pnpm --filter '*' lint:js",
Expand Down
99 changes: 95 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,109 @@ function registerJavaScriptInjections(hljs) {

js = js.rawDefinition(hljs);

setupHBSLiteral(js);
swapXMLForGlimmer(js);
setupTemplateTag(js);

hljs.registerLanguage('javascript', () => js);
}

function setupHBSLiteral(js) {
let cssIndex = js.contains.findIndex((rule) => rule?.begin === 'css`');
let css = js.contains[cssIndex];

const HBS_TEMPLATE = hljs.inherit(css, { begin: /hbs`/ });

HBS_TEMPLATE.starts.subLanguage = 'glimmer';

js.contains.splice(cssIndex, 0, HBS_TEMPLATE);
}

function swapXMLForGlimmer(js) {
// The default JSX grammar is actually just XML, which... is also wrong :D
js.contains
.flatMap((contains) => contains?.contains || contains)
.filter((rule) => rule.subLanguage === 'xml')
.forEach((rule) => (rule.subLanguage = 'glimmer'));
}

const HBS_TEMPLATE = hljs.inherit(css, { begin: /hbs`/ });
/**
* A lot of this is stolen from XML
*/
function setupTemplateTag(js) {
const GLIMMER_TEMPLATE_TAG = {
begin: /<template>/,
end: /<\/template>/,
/**
* @param {RegExpMatchArray} match
* @param {CallbackResponse} response
*/
isTrulyOpeningTag: (match, response) => {
const afterMatchIndex = match[0].length + match.index;
const nextChar = match.input[afterMatchIndex];

HBS_TEMPLATE.starts.subLanguage = 'glimmer';
js.contains.splice(cssIndex, 0, HBS_TEMPLATE);
hljs.registerLanguage('javascript', () => js);
if (
// HTML should not include another raw `<` inside a tag
// nested type?
// `<Array<Array<number>>`, etc.
nextChar === '<' ||
// the , gives away that this is not HTML
// `<T, A extends keyof T, V>`
nextChar === ','
) {
response.ignoreMatch();

return;
}

// `<something>`
// Quite possibly a tag, lets look for a matching closing tag...
if (nextChar === '>') {
// if we cannot find a matching closing tag, then we
// will ignore it
if (!hasClosingTag(match, { after: afterMatchIndex })) {
response.ignoreMatch();
}
}

// `<blah />` (self-closing)
// handled by simpleSelfClosing rule

// `<From extends string>`
// technically this could be HTML, but it smells like a type
let m;
const afterMatch = match.input.substring(afterMatchIndex);

// NOTE: This is ugh, but added specifically for https://github.com/highlightjs/highlight.js/issues/3276
if ((m = afterMatch.match(/^\s+extends\s+/))) {
if (m.index === 0) {
response.ignoreMatch();

// eslint-disable-next-line no-useless-return
return;
}
}
},
};

js.contains.unshift({
variants: [
{
begin: GLIMMER_TEMPLATE_TAG.begin,
// we carefully check the opening tag to see if it truly
// is a tag and not a false positive
'on:begin': GLIMMER_TEMPLATE_TAG.isTrulyOpeningTag,
end: GLIMMER_TEMPLATE_TAG.end,
},
],
subLanguage: 'glimmer',
contains: [
{
begin: GLIMMER_TEMPLATE_TAG.begin,
end: GLIMMER_TEMPLATE_TAG.end,
skip: true,
contains: ['self'],
},
],
});
}
153 changes: 153 additions & 0 deletions tests-esm/unit/injections.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,158 @@ describe('Injections | JS', () => {
`
);
});

test('implied default export', () => {
let result = parse(
stripIndent`
<template>
{{@name}}
</template>
`,
'js'
);

expect(result).toEqual(
tag('hljs-name', ['{{', tag('punctuation', '@'), tag('params', 'name'), '}}'])
);
});

test('explicit default export', () => {
let result = parse(
stripIndent`
export default <template>
{{@name}}
</template>
`,
'js'
);

expect(result).toEqual();
});

test('with imports', () => {
let result = parse(
stripIndent`
import Greeting from './greeting.js';
import WeatherSummary from './weather-summary.js';
<template>
<div>
<Greeting @name="Chris" />
<WeatherSummary />
</div>
</template>
`,
'js'
);

expect(result).toEqual();
});

test('a function exists above the template', () => {
let result = parse(
stripIndent`
import Greeting from './greeting.js';
import WeatherSummary from './weather-summary.js';
function isBirthday(dateOfBirth) {
const now = new Date();
return (
dateOfBirth.getDate() === now.getDate() &&
dateOfBirth.getMonth() === now.getMonth()
);
}
<template>
<div>
<Greeting @name="Chris" />
{{#if (isBirthday @user.dateOfBirth)}}
<Celebration type='birthday' />
{{/if}}
<WeatherSummary />
</div>
</template>
`,
'js'
);

expect(result).toEqual();
});

test('is embedded in a class', () => {
let result = parse(
stripIndent`
import Component from '@glimmer/component';
import { gt, lt } from '@glimmer/helper';
export default class WeatherSummary extends Component {
@tracked currentTemp;
interval;
getWeather = () => {
this.currentTemp = // something
}
constructor(owner, args) {
super(owner, args);
this.interval = setInterval(this.getWeather, 10000);
}
willDestroy() {
super.willDestroy();
clearInterval(this.interval);
}
<template>
<p>
The current temperature is {{this.currentTemp}}!
{{#if (lt 50 this.currentTemp)}}
Brr! 🥶
{{else if (gt 80 this.currentTemp)}}
Yikes! 🥵
{{/if}}
</p>
</template>
}
`,
'js'
);

expect(result).toEqual();
});

test('multiple components', () => {
let result = parse(
stripIndent`
import WeatherSummary from './weather-summary.js';
const Greeting = <template>
<p>Hello, {{@name}}!</p>
</template>;
function isBirthday(dateOfBirth) {
const now = new Date();
return (
dateOfBirth.getDate() === now.getDate() &&
dateOfBirth.getMonth() === now.getMonth()
);
}
<template>
<div>
<Greeting @name="Chris" />
{{#if (isBirthday @user.dateOfBirth)}}
<Celebration type='birthday' />
{{/if}}
<WeatherSummary />
</div>
</template>
`,
'js'
);

expect(result).toEqual();
});
});
});

0 comments on commit b6f3e05

Please sign in to comment.