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.