diff --git a/packages/cli/src/lifecycles/graph.js b/packages/cli/src/lifecycles/graph.js
index dde8db77a..75eac9de2 100644
--- a/packages/cli/src/lifecycles/graph.js
+++ b/packages/cli/src/lifecycles/graph.js
@@ -25,6 +25,7 @@ module.exports = generateGraph = async (compilation) => {
const template = attributes.template || 'page';
const title = attributes.title || compilation.config.title || '';
const id = attributes.label || filename.split('/')[filename.split('/').length - 1].replace('.md', '').replace('.html', '');
+ const imports = attributes.imports || [];
const label = id.split('-')
.map((idPart) => {
return `${idPart.charAt(0).toUpperCase()}${idPart.substring(1)}`;
@@ -97,6 +98,7 @@ module.exports = generateGraph = async (compilation) => {
* filename: name of the file
* id: filename without the extension
* label: "pretty" text representation of the filename
+ * imports: per page JS or CSS file imports to be included in HTML output
* path: path to the file relative to the workspace
* route: URL route for a given page on outputFilePath
* template: page template to use as a base for a generated component
@@ -107,6 +109,7 @@ module.exports = generateGraph = async (compilation) => {
filename,
id,
label,
+ imports,
path: route === '/' || relativePagePath.lastIndexOf('/') === 0
? `${relativeWorkspacePath}${filename}`
: `${relativeWorkspacePath}/${filename}`,
diff --git a/packages/cli/src/plugins/resource/plugin-standard-html.js b/packages/cli/src/plugins/resource/plugin-standard-html.js
index 20339ed28..6a45d85a4 100644
--- a/packages/cli/src/plugins/resource/plugin-standard-html.js
+++ b/packages/cli/src/plugins/resource/plugin-standard-html.js
@@ -45,7 +45,7 @@ const getPageTemplate = (barePath, workspace, template) => {
return contents;
};
-const getAppTemplate = (contents, userWorkspace) => {
+const getAppTemplate = (contents, userWorkspace, customImports = []) => {
function sliceTemplate(template, pos, needle, replacer) {
return template.slice(0, pos) + template.slice(pos).replace(needle, replacer);
}
@@ -140,6 +140,30 @@ const getAppTemplate = (contents, userWorkspace) => {
}
});
+ customImports.forEach((customImport) => {
+ const extension = path.extname(customImport);
+
+ switch (extension) {
+
+ case '.js':
+ appTemplateContents = appTemplateContents.replace('', `
+
+
+ `);
+ break;
+ case '.css':
+ appTemplateContents = appTemplateContents.replace('', `
+
+
+ `);
+ break;
+
+ default:
+ break;
+
+ }
+ });
+
return appTemplateContents;
};
@@ -217,6 +241,8 @@ class StandardHtmlResource extends ResourceInterface {
const config = Object.assign({}, this.compilation.config);
const { userWorkspace, projectDirectory } = this.compilation.context;
const normalizedUrl = this.getRelativeUserworkspaceUrl(url);
+ let customImports;
+
let body = '';
let template = null;
let processedMarkdown = null;
@@ -270,15 +296,34 @@ class StandardHtmlResource extends ResourceInterface {
if (attributes.template) {
template = attributes.template;
}
+
+ if (attributes.imports) {
+ customImports = attributes.imports;
+ }
}
}
body = getPageTemplate(barePath, userWorkspace, template);
- body = getAppTemplate(body, userWorkspace);
+ body = getAppTemplate(body, userWorkspace, customImports);
body = getUserScripts(body, projectDirectory);
body = getMetaContent(normalizedUrl, config, body);
if (processedMarkdown) {
+ const wrappedCustomElementRegex = /
<[a-zA-Z]*-[a-zA-Z](.*)>(.*)<\/[a-zA-Z]*-[a-zA-Z](.*)><\/p>/g;
+ const ceTest = wrappedCustomElementRegex.test(processedMarkdown.contents);
+
+ if (ceTest) {
+ const ceMatches = processedMarkdown.contents.match(wrappedCustomElementRegex);
+
+ ceMatches.forEach((match) => {
+ const stripWrappingTags = match
+ .replace('
', '')
+ .replace('
', '');
+
+ processedMarkdown.contents = processedMarkdown.contents.replace(match, stripWrappingTags);
+ });
+ }
+
body = body.replace(/\(.*)<\/content-outlet>/s, processedMarkdown.contents);
}
diff --git a/packages/cli/test/cases/build.default.workspace-frontmatter-imports/build.default.workspace-frontmatter-imports.spec.js b/packages/cli/test/cases/build.default.workspace-frontmatter-imports/build.default.workspace-frontmatter-imports.spec.js
new file mode 100644
index 000000000..70e13e0d8
--- /dev/null
+++ b/packages/cli/test/cases/build.default.workspace-frontmatter-imports/build.default.workspace-frontmatter-imports.spec.js
@@ -0,0 +1,143 @@
+/*
+ * Use Case
+ * Run Greenwood build command with a workspace that uses frontmatter imports.
+ *
+ * User Result
+ * Should generate a bare bones Greenwood build.
+ *
+ * User Command
+ * greenwood build
+ *
+ * User Config
+ * None (Greenwood Default)
+ *
+ * User Workspace
+ * src/
+ * components/
+ * counter/
+ * counter.js
+ * counter.css
+ * pages/
+ * examples/
+ * counter.md
+ * index.md
+ */
+const expect = require('chai').expect;
+const fs = require('fs');
+const glob = require('glob-promise');
+const { JSDOM } = require('jsdom');
+const path = require('path');
+const { getSetupFiles, getOutputTeardownFiles } = require('../../../../../test/utils');
+const runSmokeTest = require('../../../../../test/smoke-test');
+const Runner = require('gallinago').Runner;
+
+describe('Build Greenwood With: ', function() {
+ const LABEL = 'Default Greenwood Configuration and Workspace with Frontmatter Imports';
+ const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js');
+ const outputPath = __dirname;
+ let runner;
+
+ before(async function() {
+ this.context = {
+ publicDir: path.join(outputPath, 'public')
+ };
+ runner = new Runner();
+ });
+
+ describe(LABEL, function() {
+
+ before(async function() {
+ await runner.setup(outputPath, getSetupFiles(outputPath));
+ await runner.runCommand(cliPath, 'build');
+ });
+
+ runSmokeTest(['public', 'index'], LABEL);
+
+ describe('Content and file output for the Counter page', function() {
+ let dom;
+ let html;
+
+ before(async function() {
+ const htmlPath = path.resolve(this.context.publicDir, 'examples/counter', 'index.html');
+
+ dom = await JSDOM.fromFile(path.resolve(htmlPath));
+ html = await fs.promises.readFile(htmlPath, 'utf-8');
+ });
+
+ it('should output a counter.css file from frontmatter import', async function() {
+ const cssFiles = await glob.promise(`${this.context.publicDir}**/**/counter.*.css`);
+
+ expect(cssFiles).to.have.lengthOf(1);
+ });
+
+ it('should output a counter.js file from frontmatter import', async function() {
+ const jsFiles = await glob.promise(`${this.context.publicDir}**/**/counter.*.js`);
+
+ expect(jsFiles).to.have.lengthOf(1);
+ });
+
+ it('should a page heading', function() {
+ const heading = dom.window.document.querySelectorAll('body h2');
+
+ expect(heading.length).to.be.equal(1);
+ expect(heading[0].textContent).to.be.equal('Counter Page Example');
+ });
+
+ describe('Counter component from front matter', () => {
+ it('should output a custom tag that', function() {
+ const counter = dom.window.document.querySelectorAll('body x-counter');
+
+ expect(counter.length).to.be.equal(1);
+ });
+
+ it('should output a custom tag that is _not_ wrapped in a tag', function() {
+ expect((/
/).test(html)).to.be.false;
+ expect((/<\/x-counter><\/p>/).test(html)).to.be.false;
+ });
+
+ it('should output a heading tag from the custom element', function() {
+ const heading = dom.window.document.querySelectorAll('body h3');
+
+ expect(heading.length).to.be.equal(1);
+ expect(heading[0].textContent).to.be.equal('My Counter');
+ });
+ });
+
+ describe('Custom component', () => {
+ it('should output a custom tag that', function() {
+ const header = dom.window.document.querySelectorAll('body app-header');
+
+ expect(header.length).to.be.equal(1);
+ });
+
+ it('should output a tag that is _not_ wrapped in a tag', function() {
+ expect((/
/).test(html)).to.be.false;
+ expect((/<\/app-header><\/p>/).test(html)).to.be.false;
+ });
+
+ it('should output a tag with expected content', function() {
+ const header = dom.window.document.querySelectorAll('body app-header');
+
+ expect(header[0].textContent).to.be.equal('I am a header');
+ });
+ });
+
+ describe('Custom Multihypen component', () => {
+ it('should output a custom tag that', function() {
+ const header = dom.window.document.querySelectorAll('body multihyphen-custom-element');
+
+ expect(header.length).to.be.equal(1);
+ });
+
+ it('should output a tag that is _not_ wrapped in a tag', function() {
+ expect((/
/).test(html)).to.be.false;
+ expect((/<\/multihyphen-custom-element><\/p>/).test(html)).to.be.false;
+ });
+ });
+ });
+ });
+
+ after(function() {
+ runner.teardown(getOutputTeardownFiles(outputPath));
+ });
+});
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/components/counter/counter.css b/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/components/counter/counter.css
new file mode 100644
index 000000000..d24b896b6
--- /dev/null
+++ b/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/components/counter/counter.css
@@ -0,0 +1,3 @@
+h2 {
+ color: red;
+}
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/components/counter/counter.js b/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/components/counter/counter.js
new file mode 100644
index 000000000..b102faa27
--- /dev/null
+++ b/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/components/counter/counter.js
@@ -0,0 +1,42 @@
+const template = document.createElement('template');
+
+template.innerHTML = `
+
+ My Counter
+
+
+
+`;
+
+class MyCounter extends HTMLElement {
+ constructor() {
+ super();
+ this.count = 0;
+ this.attachShadow({ mode: 'open' });
+ }
+
+ async connectedCallback() {
+ this.shadowRoot.appendChild(template.content.cloneNode(true));
+ this.shadowRoot.getElementById('inc').onclick = () => this.inc();
+ this.shadowRoot.getElementById('dec').onclick = () => this.dec();
+ this.update();
+ }
+
+ inc() {
+ this.update(++this.count); // eslint-disable-line
+ }
+
+ dec() {
+ this.update(--this.count); // eslint-disable-line
+ }
+
+ update(count) {
+ this.shadowRoot.getElementById('count').innerHTML = count || this.count;
+ }
+}
+
+customElements.define('x-counter', MyCounter);
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/pages/examples/counter.md b/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/pages/examples/counter.md
new file mode 100644
index 000000000..c77fc15f7
--- /dev/null
+++ b/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/pages/examples/counter.md
@@ -0,0 +1,14 @@
+---
+title: Counter Page
+imports:
+ - /components/counter/counter.js
+ - /components/counter/counter.css
+---
+
+## Counter Page Example
+
+
+
+I am a header
+
+
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/pages/index.md b/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/pages/index.md
new file mode 100644
index 000000000..db36a6f2d
--- /dev/null
+++ b/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/pages/index.md
@@ -0,0 +1,3 @@
+## Home Page
+
+This is just a test.
\ No newline at end of file
diff --git a/www/pages/docs/front-matter.md b/www/pages/docs/front-matter.md
index 203639a7f..ebb1659c0 100644
--- a/www/pages/docs/front-matter.md
+++ b/www/pages/docs/front-matter.md
@@ -25,20 +25,24 @@ label: 'My Blog Post from 3/5/2020'
### Imports
-> ⛔ _**Coming Soon!**_
-
-
+> _See our [Markdown Docs](/docs/markdown#imports) for more information about rendering custom elements in markdown files._
### Template
diff --git a/www/pages/docs/markdown.md b/www/pages/docs/markdown.md
index 1982a0ef8..a1f738e15 100644
--- a/www/pages/docs/markdown.md
+++ b/www/pages/docs/markdown.md
@@ -10,27 +10,24 @@ linkheadings: 3
In this section we'll cover some of the Markdown related feature of **Greenwood**, which by default supports the [CommonMark](https://commonmark.org/help/) specification and [**unifiedjs**](https://unifiedjs.com/) as the markdown / content framework.
### Imports
-> ⛔ _**Coming Soon!**_
-
-
-
### Configuration
Using your _greenwood.config.js_ you can have additional [markdown customizations and configurations](/docs/configuration#markdown) using unified presets and plugins.