Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancement/issue 529 restore frontmatter imports #554

Merged
Merged
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
3 changes: 3 additions & 0 deletions packages/cli/src/lifecycles/graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)}`;
Expand Down Expand Up @@ -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
Expand All @@ -107,6 +109,7 @@ module.exports = generateGraph = async (compilation) => {
filename,
id,
label,
imports,
path: route === '/' || relativePagePath.lastIndexOf('/') === 0
? `${relativeWorkspacePath}${filename}`
: `${relativeWorkspacePath}/${filename}`,
Expand Down
49 changes: 47 additions & 2 deletions packages/cli/src/plugins/resource/plugin-standard-html.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -140,6 +140,30 @@ const getAppTemplate = (contents, userWorkspace) => {
}
});

customImports.forEach((customImport) => {
const extension = path.extname(customImport);

switch (extension) {

case '.js':
appTemplateContents = appTemplateContents.replace('</head>', `
<script src="${customImport}" type="module"></script>
</head>
`);
break;
case '.css':
appTemplateContents = appTemplateContents.replace('</head>', `
<link rel="stylesheet" href="${customImport}"></link>
</head>
`);
break;

default:
break;

}
});

return appTemplateContents;
};

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 = /<p><[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('<p>', '')
.replace('</p>', '');

processedMarkdown.contents = processedMarkdown.contents.replace(match, stripWrappingTags);
});
}

body = body.replace(/\<content-outlet>(.*)<\/content-outlet>/s, processedMarkdown.contents);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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 <x-counter> component from front matter', () => {
it('should output a custom <x-counter> tag that', function() {
const counter = dom.window.document.querySelectorAll('body x-counter');

expect(counter.length).to.be.equal(1);
});

it('should output a custom <x-counter> tag that is _not_ wrapped in a <p> tag', function() {
expect((/<p><x-counter>/).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 <app-header> component', () => {
it('should output a custom <app-header> tag that', function() {
const header = dom.window.document.querySelectorAll('body app-header');

expect(header.length).to.be.equal(1);
});

it('should output a <app-header> tag that is _not_ wrapped in a <p> tag', function() {
expect((/<p><app-header>/).test(html)).to.be.false;
expect((/<\/app-header><\/p>/).test(html)).to.be.false;
});

it('should output a <app-header> 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 <multihyphen-custom-element> tag that', function() {
const header = dom.window.document.querySelectorAll('body multihyphen-custom-element');

expect(header.length).to.be.equal(1);
});

it('should output a <multihyphen-custom-element> tag that is _not_ wrapped in a <p> tag', function() {
expect((/<p><multihyphen-custom-element>/).test(html)).to.be.false;
expect((/<\/multihyphen-custom-element><\/p>/).test(html)).to.be.false;
});
});
});
});

after(function() {
runner.teardown(getOutputTeardownFiles(outputPath));
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
h2 {
color: red;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const template = document.createElement('template');

template.innerHTML = `
<style>
:host {
color: blue;
}
</style>
<h3>My Counter</h3>
<button id="dec">-</button>
<span id="count"></span>
<button id="inc">+</button>
`;

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);
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
title: Counter Page
imports:
- /components/counter/counter.js
- /components/counter/counter.css
---

## Counter Page Example

<x-counter></x-counter>

<app-header>I am a header</app-header>

<multihyphen-custom-element></multihyphen-custom-element>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## Home Page

This is just a test.
14 changes: 9 additions & 5 deletions www/pages/docs/front-matter.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,24 @@ label: 'My Blog Post from 3/5/2020'


### Imports
> ⛔ _**Coming Soon!**_

<!-- If you want to include files on a _per **page** basis_, you can use the predefined `imports` feature from Greenwood. This is great for one off use cases where you dont want to ship a third party lib in all your templates, but just for this one particular page. This is effectively a naive form of code splitting. 🤓
If you want to include files on a _per **page** basis_, you can use the predefined `imports` feature from Greenwood. This is great for one off use cases where you dont want to ship a third party lib in all your templates, but just for this one particular page. This is effectively a naive form of code splitting. 🤓

#### Example
```md
---
imports:
MyFile: '../components/MyFile/myfile.js'
- /components/my-component/component.js
- /components/my-component/component.css
---
```

You will then see the following emitted for file
```html
<script type="module" src="/components/my-component/component.js"></script>
<link rel="stylesheet" href="/components/my-component/component.css"/>
```

See our [Markdown Docs](/docs/markdown#imports) for more information about rendering custom elements in markdown files. -->
> _See our [Markdown Docs](/docs/markdown#imports) for more information about rendering custom elements in markdown files._


### Template
Expand Down
13 changes: 5 additions & 8 deletions www/pages/docs/markdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!**_

<!--
From within the markdown you can also render components, not just their syntax, by importing them via [front-matter](/docs/front-matter).


#### Example
At the top of a `.md` file add an `import` section to render a component inline to the page itself. This can be helpful if there are situations where you may want to `import` a component for a specific set of pages, as opposed to through a page or app template.:
At the top of a `.md` file add an [`import` section](/docs/front-matter/) to render a component inline to the page itself. This can be helpful if there are situations where you may want to `import` a component for a specific set of pages, as opposed to through a page or app template.:

```md
---
imports:
HelloWorld: '../components/helloworld/helloworld.js'
- /components/counter/counter.js
---

<hello text='world'>World</hello>
## My Demo Page
<x-counter></x-counter>
```

> See our [component model docs](/docs/component-model) for more information on authoring custom elements / components. For information on configuring additional page meta data, see our section on [front-matter](/docs/front-matter/).

-->

### Configuration
Using your _greenwood.config.js_ you can have additional [markdown customizations and configurations](/docs/configuration#markdown) using unified presets and plugins.

Expand Down