Skip to content

Commit 2163943

Browse files
author
Damir
committed
More tests
1 parent 8a1d117 commit 2163943

18 files changed

+242
-125
lines changed

src/find-path.js

+9-11
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ function findPathByNamespace(tag, [namespace, fileNameFromTag], options) {
5050

5151
if (!customTagNamespace) {
5252
if (options.strict) {
53-
throw new Error(`[custom-tag] Unknown module namespace ${namespace}.`);
53+
throw new Error(`[components] Unknown module namespace ${namespace}.`);
5454
} else {
5555
return false;
5656
}
@@ -90,20 +90,18 @@ function findPathByNamespace(tag, [namespace, fileNameFromTag], options) {
9090
} catch {
9191
// With disabled strict mode we will never enter here as findPathByRoot() return false
9292
// so we don't need to check if options.strict is true
93-
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.`);
93+
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.`);
9494
}
9595
} else if (options.strict) {
96-
throw new Error(`[custom-tag] For the tag ${tag} was not found the template in the defined namespace's path ${customTagNamespace.root}.`);
96+
throw new Error(`[components] For the tag ${tag} was not found the template in the defined namespace's path ${customTagNamespace.root}.`);
9797
} else {
9898
return false;
9999
}
100100
}
101101

102-
// Return dirname + filename
103-
return customTagNamespace.root
104-
.replace(options.root, '')
105-
.replace(options.absolute ? '' : path.sep, '')
106-
.concat(path.sep, fileNameFromTag);
102+
// Set root to namespace root
103+
options.root = customTagNamespace.root;
104+
return fileNameFromTag;
107105
}
108106

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

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

126-
root = options.roots.find(root => fs.existsSync(path.join(options.root, root, fileNameFromTag)));
124+
root = options.roots.find(root => fs.existsSync(path.join(root, fileNameFromTag)));
127125
}
128126

129127
if (!root) {
130128
if (options.strict) {
131-
throw new Error(`[custom-tag] For the tag ${tag} was not found the template in any defined root path ${options.roots.join(', ')}`);
129+
throw new Error(`[components] For the tag ${tag} was not found the template in any defined root path ${options.roots.join(', ')}`);
132130
} else {
133131
return false;
134132
}

src/index.js

+69-35
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const path = require('path');
55
const expressions = require('posthtml-expressions');
66
const scriptDataLocals = require('posthtml-expressions/lib/locals');
77
const {parser: parseToPostHtml} = require('posthtml-parser');
8-
const parseAttrs = require('posthtml-attrs-parser')
8+
const parseAttrs = require('posthtml-attrs-parser');
99
const {match} = require('posthtml/lib/api');
1010
const merge = require('deepmerge');
1111
const findPathFromTagName = require('./find-path');
@@ -14,48 +14,51 @@ function processNodes(tree, options, messages) {
1414
tree = applyPluginsToTree(tree, options.plugins);
1515

1616
match.call(tree, [{tag: options.tagName}, {tag: options.tagRegExp}], node => {
17-
if (!node.attrs || !node.attrs.src) {
18-
const path = findPathFromTagName(node, options);
19-
20-
if (path !== false) {
21-
node.attrs.src = path;
22-
}
17+
if (!node.attrs) {
18+
node.attrs = {};
2319
}
2420

25-
let attributes = {...node.attrs};
21+
const filePath = node.attrs.src || findPathFromTagName(node, options);
2622

27-
delete attributes.src;
28-
29-
Object.keys(attributes).forEach(attribute => {
30-
try {
31-
attributes = {...attributes, ...JSON.parse(attributes[attribute])};
32-
} catch {}
33-
});
34-
35-
delete attributes.locals;
23+
// Return node as-is when strict mode is disabled
24+
// otherwise raise error happen in find-path.js
25+
if (!filePath) {
26+
return node;
27+
}
3628

37-
attributes = merge(options.expressions.locals, attributes);
29+
delete node.attrs.src;
3830

39-
const layoutPath = path.resolve(options.root, node.attrs.src);
40-
const layoutHtml = fs.readFileSync(layoutPath, options.encoding);
41-
const parsedHtml = parseToPostHtml(layoutHtml);
31+
const layoutPath = path.resolve(options.root, filePath);
4232

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

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

5137
options.expressions.locals = attributes;
38+
5239
const plugins = [...options.plugins, expressions(options.expressions)];
5340

54-
const layoutTree = processNodes(applyPluginsToTree(parsedHtml, plugins), options, messages);
41+
const layoutTree = processNodes(applyPluginsToTree(html, plugins), options, messages);
5542

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

46+
const index = node.content.findIndex(content => typeof content === 'object');
47+
48+
const nodeAttrs = parseAttrs(node.content[index].attrs);
49+
50+
Object.keys(attributes).forEach(attr => {
51+
if (typeof defaultLocals[attr] === 'undefined') {
52+
if (['class'].includes(attr)) {
53+
nodeAttrs[attr].push(attributes[attr]);
54+
} else if (['style'].includes(attr)) {
55+
nodeAttrs[attr] = attributes[attr];
56+
}
57+
}
58+
});
59+
60+
node.content[index].attrs = nodeAttrs.compose();
61+
5962
messages.push({
6063
type: 'dependency',
6164
file: layoutPath,
@@ -68,6 +71,31 @@ function processNodes(tree, options, messages) {
6871
return tree;
6972
}
7073

74+
function parseLocals(options, {attrs}, html) {
75+
let attributes = {...attrs};
76+
77+
Object.keys(attributes).forEach(attribute => {
78+
try {
79+
// Use merge()
80+
attributes = {...attributes, ...JSON.parse(attributes[attribute])};
81+
} catch {}
82+
});
83+
84+
delete attributes.locals;
85+
86+
attributes = merge(options.expressions.locals, attributes);
87+
88+
// Retrieve default locals from <script defaultLocals> and merge with attributes
89+
const {locals: defaultLocals} = scriptDataLocals(html, {localsAttr: options.scriptLocalAttribute, removeScriptLocals: true, locals: attributes});
90+
91+
// Merge default locals and attributes
92+
if (defaultLocals) {
93+
attributes = merge(defaultLocals, attributes);
94+
}
95+
96+
return {attributes, defaultLocals};
97+
}
98+
7199
function mergeSlots(tree, component, strict, slotTagName) {
72100
const slots = getSlots(slotTagName, tree); // Slot in component.html
73101
const fillSlots = getSlots(slotTagName, component.content); // Slot in page.html
@@ -103,14 +131,14 @@ function mergeSlots(tree, component, strict, slotTagName) {
103131
}
104132

105133
if (strict) {
106-
let errors = '';
134+
const unexpectedSlots = [];
107135

108136
for (const fillSlotName of Object.keys(fillSlots)) {
109-
errors += `Unexpected slot "${fillSlotName}". `;
137+
unexpectedSlots.push(fillSlotName);
110138
}
111139

112-
if (errors) {
113-
throw new Error(errors);
140+
if (unexpectedSlots.length > 0) {
141+
throw new Error(`[components] Unexpected slot: ${unexpectedSlots.join(', ')}.`);
114142
}
115143
}
116144

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

136164
match.call(content, {tag}, node => {
137165
if (!node.attrs || !node.attrs.name) {
138-
throw new Error('Missing slot name');
166+
throw new Error('[components] Missing slot name');
139167
}
140168

141169
const {name} = node.attrs;
@@ -163,7 +191,7 @@ module.exports = (options = {}) => {
163191
options = {
164192
...{
165193
root: './',
166-
roots: '/',
194+
roots: '',
167195
namespaces: [], // Array of namespaces path or single namespaces as object
168196
namespaceSeparator: '::',
169197
namespaceFallback: false,
@@ -176,14 +204,20 @@ module.exports = (options = {}) => {
176204
expressions: {},
177205
plugins: [],
178206
encoding: 'utf8',
179-
strict: false,
207+
strict: true,
180208
scriptLocalAttribute: 'defaultLocals'
181209
},
182210
...options
183211
};
184212

213+
options.root = path.resolve(options.root);
214+
185215
options.roots = Array.isArray(options.roots) ? options.roots : [options.roots];
186216

217+
options.roots.forEach((root, index) => {
218+
options.roots[index] = path.join(options.root, root);
219+
});
220+
187221
options.namespaces = Array.isArray(options.namespaces) ? options.namespaces : [options.namespaces];
188222
options.namespaces.forEach((namespace, index) => {
189223
options.namespaces[index].root = path.resolve(namespace.root);
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1+
<script defaultLocals>
2+
module.exports = {
3+
title: 'Default title',
4+
body: 'Default body'
5+
}
6+
</script>
17
<div><h1>{{title}}</h1></div><div><slot name="body">{{body}}</slot></div>

test/templates/components/component-mapped-attributes.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
body: 'Default body'
55
}
66
</script>
7-
<div class="text-dark m-3" $attributes>{{ title }} {{ body }}</div>
7+
<div class="text-dark m-3">{{ title }} {{ body }}</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div class="nested-one"><x-nested-two></x-nested-two></div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div class="nested-three">Nested works!</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div class="nested-two"><x-nested-three></x-nested-three></div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<button class="bg-dark-custom text-light-custom"><slot name="content"></slot></button>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<button class="bg-dark text-light"><slot name="content"></slot></button>

test/templates/dark/layouts/base.html

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1+
<script defaultLocals>
2+
module.exports = {
3+
title: 'Default title'
4+
}
5+
</script>
16
<html><head><title>{{ title }}</title></head><body><main><slot name="content"></slot></main><footer><slot name="footer">footer content</slot></footer></body></html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<button class="bg-light text-dark"><slot name="content"></slot></button>
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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>

test/test-core.js

-78
This file was deleted.

test/test-errors.js

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use strict';
2+
3+
const test = require('ava');
4+
const plugin = require('../src');
5+
const posthtml = require('posthtml');
6+
const clean = html => html.replace(/(\n|\t)/g, '').trim();
7+
8+
test('Must fail when namespace path is not found without fallback root', async t => {
9+
const actual = `<div><x-empty-namespace::button>Submit</x-empty-namespace::button></div>`;
10+
11+
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)));
12+
});
13+
14+
test('Must fail when namespace path is not found with fallback root', async t => {
15+
const actual = `<div><x-empty-namespace::button>Submit</x-empty-namespace::button></div>`;
16+
17+
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)));
18+
});

0 commit comments

Comments
 (0)