Skip to content

Commit

Permalink
More tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Damir committed Oct 12, 2022
1 parent 8a1d117 commit 2163943
Show file tree
Hide file tree
Showing 18 changed files with 242 additions and 125 deletions.
20 changes: 9 additions & 11 deletions src/find-path.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function findPathByNamespace(tag, [namespace, fileNameFromTag], options) {

if (!customTagNamespace) {
if (options.strict) {
throw new Error(`[custom-tag] Unknown module namespace ${namespace}.`);
throw new Error(`[components] Unknown module namespace ${namespace}.`);
} else {
return false;
}
Expand Down Expand Up @@ -90,20 +90,18 @@ function findPathByNamespace(tag, [namespace, fileNameFromTag], options) {
} catch {
// With disabled strict mode we will never enter here as findPathByRoot() return false
// so we don't need to check if options.strict is true
throw new Error(`[custom-tag] For the tag ${tag} was not found the template in the defined namespace's root ${customTagNamespace.root} nor in any defined custom tag roots.`);
throw new Error(`[components] For the tag ${tag} was not found the template in the defined namespace's root ${customTagNamespace.root} nor in any defined custom tag roots.`);
}
} else if (options.strict) {
throw new Error(`[custom-tag] For the tag ${tag} was not found the template in the defined namespace's path ${customTagNamespace.root}.`);
throw new Error(`[components] For the tag ${tag} was not found the template in the defined namespace's path ${customTagNamespace.root}.`);
} else {
return false;
}
}

// Return dirname + filename
return customTagNamespace.root
.replace(options.root, '')
.replace(options.absolute ? '' : path.sep, '')
.concat(path.sep, fileNameFromTag);
// Set root to namespace root
options.root = customTagNamespace.root;
return fileNameFromTag;
}

/**
Expand All @@ -115,20 +113,20 @@ function findPathByNamespace(tag, [namespace, fileNameFromTag], options) {
* @return {String|boolean} [custom tag root where the module is found]
*/
function findPathByRoot(tag, fileNameFromTag, options) {
let root = options.roots.find(root => fs.existsSync(path.join(options.root, root, fileNameFromTag)));
let root = options.roots.find(root => fs.existsSync(path.join(root, fileNameFromTag)));

if (!root) {
// Check if module exist in folder `tag-name/index.html`
fileNameFromTag = fileNameFromTag
.replace(`.${options.fileExtension}`, '')
.concat(path.sep, 'index.', options.fileExtension);

root = options.roots.find(root => fs.existsSync(path.join(options.root, root, fileNameFromTag)));
root = options.roots.find(root => fs.existsSync(path.join(root, fileNameFromTag)));
}

if (!root) {
if (options.strict) {
throw new Error(`[custom-tag] For the tag ${tag} was not found the template in any defined root path ${options.roots.join(', ')}`);
throw new Error(`[components] For the tag ${tag} was not found the template in any defined root path ${options.roots.join(', ')}`);
} else {
return false;
}
Expand Down
104 changes: 69 additions & 35 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const path = require('path');
const expressions = require('posthtml-expressions');
const scriptDataLocals = require('posthtml-expressions/lib/locals');
const {parser: parseToPostHtml} = require('posthtml-parser');
const parseAttrs = require('posthtml-attrs-parser')
const parseAttrs = require('posthtml-attrs-parser');
const {match} = require('posthtml/lib/api');
const merge = require('deepmerge');
const findPathFromTagName = require('./find-path');
Expand All @@ -14,48 +14,51 @@ function processNodes(tree, options, messages) {
tree = applyPluginsToTree(tree, options.plugins);

match.call(tree, [{tag: options.tagName}, {tag: options.tagRegExp}], node => {
if (!node.attrs || !node.attrs.src) {
const path = findPathFromTagName(node, options);

if (path !== false) {
node.attrs.src = path;
}
if (!node.attrs) {
node.attrs = {};
}

let attributes = {...node.attrs};
const filePath = node.attrs.src || findPathFromTagName(node, options);

delete attributes.src;

Object.keys(attributes).forEach(attribute => {
try {
attributes = {...attributes, ...JSON.parse(attributes[attribute])};
} catch {}
});

delete attributes.locals;
// Return node as-is when strict mode is disabled
// otherwise raise error happen in find-path.js
if (!filePath) {
return node;
}

attributes = merge(options.expressions.locals, attributes);
delete node.attrs.src;

const layoutPath = path.resolve(options.root, node.attrs.src);
const layoutHtml = fs.readFileSync(layoutPath, options.encoding);
const parsedHtml = parseToPostHtml(layoutHtml);
const layoutPath = path.resolve(options.root, filePath);

// Retrieve default locals from <script defaultLocals> and merge with attributes
const {locals: defaultLocals} = scriptDataLocals(parsedHtml, {localsAttr: options.scriptLocalAttribute, removeScriptLocals: true, locals: attributes});
if (defaultLocals) {
attributes = merge(defaultLocals, attributes);
}
const html = parseToPostHtml(fs.readFileSync(layoutPath, options.encoding));

console.log(`defaultLocals %s`, defaultLocals);
const {attributes, defaultLocals} = parseLocals(options, node, html);

options.expressions.locals = attributes;

const plugins = [...options.plugins, expressions(options.expressions)];

const layoutTree = processNodes(applyPluginsToTree(parsedHtml, plugins), options, messages);
const layoutTree = processNodes(applyPluginsToTree(html, plugins), options, messages);

node.tag = false;
node.content = mergeSlots(layoutTree, node, options.strict, options.slotTagName);

const index = node.content.findIndex(content => typeof content === 'object');

const nodeAttrs = parseAttrs(node.content[index].attrs);

Object.keys(attributes).forEach(attr => {
if (typeof defaultLocals[attr] === 'undefined') {
if (['class'].includes(attr)) {
nodeAttrs[attr].push(attributes[attr]);
} else if (['style'].includes(attr)) {
nodeAttrs[attr] = attributes[attr];
}
}
});

node.content[index].attrs = nodeAttrs.compose();

messages.push({
type: 'dependency',
file: layoutPath,
Expand All @@ -68,6 +71,31 @@ function processNodes(tree, options, messages) {
return tree;
}

function parseLocals(options, {attrs}, html) {
let attributes = {...attrs};

Object.keys(attributes).forEach(attribute => {
try {
// Use merge()
attributes = {...attributes, ...JSON.parse(attributes[attribute])};
} catch {}
});

delete attributes.locals;

attributes = merge(options.expressions.locals, attributes);

// Retrieve default locals from <script defaultLocals> and merge with attributes
const {locals: defaultLocals} = scriptDataLocals(html, {localsAttr: options.scriptLocalAttribute, removeScriptLocals: true, locals: attributes});

// Merge default locals and attributes
if (defaultLocals) {
attributes = merge(defaultLocals, attributes);
}

return {attributes, defaultLocals};
}

function mergeSlots(tree, component, strict, slotTagName) {
const slots = getSlots(slotTagName, tree); // Slot in component.html
const fillSlots = getSlots(slotTagName, component.content); // Slot in page.html
Expand Down Expand Up @@ -103,14 +131,14 @@ function mergeSlots(tree, component, strict, slotTagName) {
}

if (strict) {
let errors = '';
const unexpectedSlots = [];

for (const fillSlotName of Object.keys(fillSlots)) {
errors += `Unexpected slot "${fillSlotName}". `;
unexpectedSlots.push(fillSlotName);
}

if (errors) {
throw new Error(errors);
if (unexpectedSlots.length > 0) {
throw new Error(`[components] Unexpected slot: ${unexpectedSlots.join(', ')}.`);
}
}

Expand All @@ -135,7 +163,7 @@ function getSlots(tag, content = []) {

match.call(content, {tag}, node => {
if (!node.attrs || !node.attrs.name) {
throw new Error('Missing slot name');
throw new Error('[components] Missing slot name');
}

const {name} = node.attrs;
Expand Down Expand Up @@ -163,7 +191,7 @@ module.exports = (options = {}) => {
options = {
...{
root: './',
roots: '/',
roots: '',
namespaces: [], // Array of namespaces path or single namespaces as object
namespaceSeparator: '::',
namespaceFallback: false,
Expand All @@ -176,14 +204,20 @@ module.exports = (options = {}) => {
expressions: {},
plugins: [],
encoding: 'utf8',
strict: false,
strict: true,
scriptLocalAttribute: 'defaultLocals'
},
...options
};

options.root = path.resolve(options.root);

options.roots = Array.isArray(options.roots) ? options.roots : [options.roots];

options.roots.forEach((root, index) => {
options.roots[index] = path.join(options.root, root);
});

options.namespaces = Array.isArray(options.namespaces) ? options.namespaces : [options.namespaces];
options.namespaces.forEach((namespace, index) => {
options.namespaces[index].root = path.resolve(namespace.root);
Expand Down
6 changes: 6 additions & 0 deletions test/templates/components/component-locals.html
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
<script defaultLocals>
module.exports = {
title: 'Default title',
body: 'Default body'
}
</script>
<div><h1>{{title}}</h1></div><div><slot name="body">{{body}}</slot></div>
2 changes: 1 addition & 1 deletion test/templates/components/component-mapped-attributes.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
body: 'Default body'
}
</script>
<div class="text-dark m-3" $attributes>{{ title }} {{ body }}</div>
<div class="text-dark m-3">{{ title }} {{ body }}</div>
1 change: 1 addition & 0 deletions test/templates/components/nested-one.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="nested-one"><x-nested-two></x-nested-two></div>
1 change: 1 addition & 0 deletions test/templates/components/nested-three.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="nested-three">Nested works!</div>
1 change: 1 addition & 0 deletions test/templates/components/nested-two.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="nested-two"><x-nested-three></x-nested-three></div>
1 change: 1 addition & 0 deletions test/templates/custom/dark/components/button.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<button class="bg-dark-custom text-light-custom"><slot name="content"></slot></button>
1 change: 1 addition & 0 deletions test/templates/dark/components/button.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<button class="bg-dark text-light"><slot name="content"></slot></button>
1 change: 1 addition & 0 deletions test/templates/dark/layouts/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<html><head><title>Base Dark Layout</title></head><body><main><slot name="content"></slot></main><footer><slot name="footer">footer content</slot></footer></body></html>
5 changes: 5 additions & 0 deletions test/templates/layouts/base-locals.html
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
<script defaultLocals>
module.exports = {
title: 'Default title'
}
</script>
<html><head><title>{{ title }}</title></head><body><main><slot name="content"></slot></main><footer><slot name="footer">footer content</slot></footer></body></html>
1 change: 1 addition & 0 deletions test/templates/light/components/button.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<button class="bg-light text-dark"><slot name="content"></slot></button>
1 change: 1 addition & 0 deletions test/templates/light/layouts/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<html><head><title>Base Light Layout</title></head><body><main><slot name="content"></slot></main><footer><slot name="footer">footer content</slot></footer></body></html>
78 changes: 0 additions & 78 deletions test/test-core.js

This file was deleted.

18 changes: 18 additions & 0 deletions test/test-errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict';

const test = require('ava');
const plugin = require('../src');
const posthtml = require('posthtml');
const clean = html => html.replace(/(\n|\t)/g, '').trim();

test('Must fail when namespace path is not found without fallback root', async t => {
const actual = `<div><x-empty-namespace::button>Submit</x-empty-namespace::button></div>`;

await t.throwsAsync(async () => posthtml([plugin({root: './test/templates', namespaces: [{name: 'empty-namespace', root: './test/templates/empty-namespace'}]})]).process(actual).then(result => clean(result.html)));
});

test('Must fail when namespace path is not found with fallback root', async t => {
const actual = `<div><x-empty-namespace::button>Submit</x-empty-namespace::button></div>`;

await t.throwsAsync(async () => posthtml([plugin({root: './test/templates', namespaces: [{name: 'empty-namespace', root: './test/templates/empty-namespace'}]})]).process(actual).then(result => clean(result.html)));
});
Loading

0 comments on commit 2163943

Please sign in to comment.