Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,7 @@ jobs:
- ember-lts-4.8
- ember-lts-4.12
- ember-lts-5.4
- ember-release
- ember-beta
- ember-canary
- ember-lts-6.12
- embroider-safe
timeout-minutes: 7
steps:
Expand Down
27 changes: 27 additions & 0 deletions addon/components/docs-code-highlight/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,31 @@ import diff from 'highlight.js/lib/languages/diff';
import shell from 'highlight.js/lib/languages/shell';
import typescript from 'highlight.js/lib/languages/typescript';

// highlight.js has no grammar for gjs/gts, so we extend the JS/TS grammars to
// highlight `<template>` blocks as Handlebars. `<template>` can directly follow
// modes that swallow whitespace and enable JSX, so it is added to those as well.
function glimmerTemplateTag(baseLanguage) {
return (hljs) => {
const language = baseLanguage(hljs);
const templateTag = () => ({
begin: /<template>/,
end: /<\/template>/,
beginScope: 'tag',
endScope: 'tag',
subLanguage: 'handlebars',
});

language.contains.unshift(templateTag());
for (let mode of language.contains) {
if (mode.contains?.some((child) => child.subLanguage === 'xml')) {
mode.contains.unshift(templateTag());
}
}

return language;
};
}

hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('js', javascript);
hljs.registerLanguage('css', css);
Expand All @@ -24,6 +49,8 @@ hljs.registerLanguage('shell', shell);
hljs.registerLanguage('sh', shell);
hljs.registerLanguage('typescript', typescript);
hljs.registerLanguage('ts', typescript);
hljs.registerLanguage('gjs', glimmerTemplateTag(javascript));
hljs.registerLanguage('gts', glimmerTemplateTag(typescript));

export default class DocsCodeHighlight extends Component {
setupElement(element) {
Expand Down
12 changes: 12 additions & 0 deletions addon/components/docs-demo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ export default class DocsDemo extends Component {
label = 'controller.js';
language = 'javascript';
break;
case 'ts':
label = 'controller.ts';
language = 'typescript';
break;
case 'gjs':
label = 'component.gjs';
language = 'gjs';
break;
case 'gts':
label = 'component.gts';
language = 'gts';
break;
case 'css':
label = 'styles.css';
language = 'css';
Expand Down
2 changes: 1 addition & 1 deletion addon/components/docs-snippet/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@
</CopyButton>
{{/if}}

<DocsCodeHighlight @language={{@language}}>{{snippet.source}}</DocsCodeHighlight>
<DocsCodeHighlight @language={{this.language}}>{{snippet.source}}</DocsCodeHighlight>
{{/let}}
</div>
21 changes: 20 additions & 1 deletion addon/components/docs-snippet/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import Component from '@glimmer/component';
import { localCopy } from 'tracked-toolbox';

const LANGUAGES_BY_EXTENSION = {
js: 'javascript',
ts: 'typescript',
gjs: 'gjs',
gts: 'gts',
hbs: 'handlebars',
css: 'css',
json: 'json',
};

/**
A snippet component for demonstrating some code

Expand All @@ -21,11 +31,20 @@ export default class DocsSnippet extends Component {
*/

/**
The language of the snippet
The language of the snippet. Inferred from the extension of the snippet
name (e.g. `.ts`, `.gjs`, `.gts`) when not provided.

@argument language
@type String?
*/
get language() {
if (this.args.language) {
return this.args.language;
}

let extension = this.args.name?.split('.').pop();
return LANGUAGES_BY_EXTENSION[extension];
}

/**
The title of the snippet
Expand Down
2 changes: 1 addition & 1 deletion addon/components/docs-viewer/x-main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export default class XMain extends Component {
}
} else {
let file = appFiles
.filter((file) => file.match(/\.(hbs|md)$/))
.filter((file) => file.match(/\.(hbs|md|gjs|gts)$/))
.find((file) => file.match(path));
if (file) {
return { file, inTree: 'app' };
Expand Down
27 changes: 27 additions & 0 deletions addon/utils/compile-markdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,31 @@ import diff from 'highlight.js/lib/languages/diff';
import shell from 'highlight.js/lib/languages/shell';
import typescript from 'highlight.js/lib/languages/typescript';

// highlight.js has no grammar for gjs/gts, so we extend the JS/TS grammars to
// highlight `<template>` blocks as Handlebars. `<template>` can directly follow
// modes that swallow whitespace and enable JSX, so it is added to those as well.
function glimmerTemplateTag(baseLanguage) {
return (hljs) => {
const language = baseLanguage(hljs);
const templateTag = () => ({
begin: /<template>/,
end: /<\/template>/,
beginScope: 'tag',
endScope: 'tag',
subLanguage: 'handlebars',
});

language.contains.unshift(templateTag());
for (let mode of language.contains) {
if (mode.contains?.some((child) => child.subLanguage === 'xml')) {
mode.contains.unshift(templateTag());
}
}

return language;
};
}

hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('js', javascript);
hljs.registerLanguage('css', css);
Expand All @@ -29,6 +54,8 @@ hljs.registerLanguage('shell', shell);
hljs.registerLanguage('sh', shell);
hljs.registerLanguage('typescript', typescript);
hljs.registerLanguage('ts', typescript);
hljs.registerLanguage('gjs', glimmerTemplateTag(javascript));
hljs.registerLanguage('gts', glimmerTemplateTag(typescript));

const htmlComponent = {
name: 'htmlComponent',
Expand Down
4 changes: 4 additions & 0 deletions ember-cli-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ module.exports = function (defaults) {
},
},

'ember-cli-babel': {
enableTypeScriptTransform: true,
},

'ember-cli-addon-docs': {
projects: {
sandbox: new MergeTrees([
Expand Down
14 changes: 13 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ module.exports = {
snippetExtensions = [
'ts',
'js',
'gts',
'gjs',
'css',
'scss',
'hbs',
Expand Down Expand Up @@ -277,7 +279,17 @@ module.exports = {
}

let ContentExtractor = require('./lib/preprocessors/hbs-content-extractor');
let appTree = this._treeFor('app');
// gjs/gts templates are not part of the addon's app tree (they are built by
// the host app), so they are fed to the extractor separately for indexing.
// The search indexer expects them below the module prefix, like hbs files.
let templateTagTree = new Funnel(this.app.trees.app, {
destDir: this.project.config(EmberApp.env()).modulePrefix,
include: ['**/*.gjs', '**/*.gts'],
annotation: 'app gjs/gts templates',
});
let appTree = new MergeTrees([this._treeFor('app'), templateTagTree], {
annotation: 'app templates for content extraction',
});
const contentExtractor = new ContentExtractor(this.getBroccoliBridge());
contentExtractor.toTree(appTree);

Expand Down
80 changes: 50 additions & 30 deletions lib/broccoli/hbs-content-filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,53 @@

const Filter = require('broccoli-filter');
const syntax = require('@glimmer/syntax');
const { Preprocessor } = require('content-tag');

const TEMPLATE_TAG_EXTENSIONS = ['gjs', 'gts'];

module.exports = class HBSContentFilter extends Filter {
constructor(input) {
super(input);
this.extensions = ['hbs'];
this.extensions = ['hbs', ...TEMPLATE_TAG_EXTENSIONS];
this.targetExtension = 'template-contents';
}

processString(content) {
processString(content, relativePath) {
let templates = extractTemplates(content, relativePath);
let parents = new Map();
let title = { level: Infinity, node: null, contents: null };
let body = [];

syntax.traverse(syntax.preprocess(content), {
Program(node) {
addContentNodes(parents, node, node.body);
},

ElementNode(node) {
addContentNodes(parents, node, node.children);
},

TextNode(node) {
let parent = parents.get(node);
if (!parent) {
return;
}

body.push(node.chars);

if (title.node === parent) {
title.contents.push(node.chars);
} else if (isHeading(parent) && headingLevel(parent) < title.level) {
title = {
level: headingLevel(parent),
node: parent,
contents: [node.chars],
};
}
},
});
for (let template of templates) {
syntax.traverse(syntax.preprocess(template), {
Program(node) {
addContentNodes(parents, node, node.body);
},

ElementNode(node) {
addContentNodes(parents, node, node.children);
},

TextNode(node) {
let parent = parents.get(node);
if (!parent) {
return;
}

body.push(node.chars);

if (title.node === parent) {
title.contents.push(node.chars);
} else if (isHeading(parent) && headingLevel(parent) < title.level) {
title = {
level: headingLevel(parent),
node: parent,
contents: [node.chars],
};
}
},
});
}

let output = {
title: title.contents ? title.contents.join('') : null,
Expand All @@ -53,6 +59,20 @@ module.exports = class HBSContentFilter extends Filter {
}
};

// A gjs/gts file can hold several `<template>` tags, while an hbs file is a
// single template.
function extractTemplates(content, relativePath) {
if (
!TEMPLATE_TAG_EXTENSIONS.some((ext) => relativePath.endsWith(`.${ext}`))
) {
return [content];
}

return new Preprocessor()
.parse(content, { filename: relativePath })
.map((template) => template.contents);
}

function isHeading(node) {
return node.type === 'ElementNode' && /^h\d$/.test(node.tag);
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"broccoli-source": "^3.0.1",
"broccoli-stew": "^3.0.0",
"chalk": "4.1.2",
"content-tag": "^4.1.1",
"ember-auto-import": "^2.12.0",
"ember-cli-autoprefixer": "^2.0.0",
"ember-cli-babel": "^8.2.0",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading