diff --git a/package-lock.json b/package-lock.json index 70feff5..a6dac27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,8 @@ "posthtml-attrs-parser": "^0.1.1", "posthtml-expressions": "^1.9.0", "posthtml-match-helper": "^1.0.3", - "posthtml-parser": "^0.11.0" + "posthtml-parser": "^0.11.0", + "style-to-object": "^0.3.0" }, "devDependencies": { "@commitlint/cli": "^12.0.1", @@ -7916,6 +7917,11 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, + "node_modules/inline-style-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" + }, "node_modules/internal-slot": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", @@ -13069,6 +13075,14 @@ "node": ">=0.8.0" } }, + "node_modules/style-to-object": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", + "integrity": "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==", + "dependencies": { + "inline-style-parser": "0.1.1" + } + }, "node_modules/supertap": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supertap/-/supertap-2.0.0.tgz", @@ -21479,6 +21493,11 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, + "inline-style-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" + }, "internal-slot": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", @@ -25402,6 +25421,14 @@ "integrity": "sha512-AOPG8EBc5wAikaG1/7uFCNFJwnKOuQwFTpYBdTW6OvWHeZBQBrAA/amefHGrEiOnCPcLFZK6FUPtWVKpQVIRgg==", "dev": true }, + "style-to-object": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", + "integrity": "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==", + "requires": { + "inline-style-parser": "0.1.1" + } + }, "supertap": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supertap/-/supertap-2.0.0.tgz", diff --git a/package.json b/package.json index eb4cd97..85b1480 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "posthtml-attrs-parser": "^0.1.1", "posthtml-expressions": "^1.9.0", "posthtml-match-helper": "^1.0.3", - "posthtml-parser": "^0.11.0" + "posthtml-parser": "^0.11.0", + "style-to-object": "^0.3.0" }, "devDependencies": { "@commitlint/cli": "^12.0.1", diff --git a/src/index.js b/src/index.js index 9a5c194..b5995eb 100644 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,7 @@ const fs = require('fs'); const path = require('path'); const {inspect} = require('util'); const {sha256} = require('js-sha256'); +const styleToObject = require('style-to-object'); const expressions = require('posthtml-expressions'); const scriptDataLocals = require('posthtml-expressions/lib/locals'); const {parser: parseToPostHtml} = require('posthtml-parser'); @@ -12,6 +13,7 @@ const parseAttrs = require('posthtml-attrs-parser'); const {match} = require('posthtml/lib/api'); const merge = require('deepmerge'); const findPathFromTagName = require('./find-path'); +// const posthtml = require('posthtml'); const debug = true; @@ -63,7 +65,7 @@ function processNodes(tree, options, messages) { options.expressions.locals = {...options.locals, ...options.aware}; const defaultSlotName = sha256(filePath); - const slotsLocals = parseSlotsLocals(options.slotTagName, html, node.content, defaultSlotName); + const slotsLocals = parseSlotsLocals(options.fillTagName, html, node.content, defaultSlotName); const {attributes, locals} = parseLocals(options, slotsLocals, node, html); options.expressions.locals = attributes; @@ -74,33 +76,9 @@ function processNodes(tree, options, messages) { const layoutTree = processNodes(applyPluginsToTree(html, plugins), options, messages); node.tag = false; - node.content = mergeSlots(layoutTree, node, options.strict, options, defaultSlotName); - - const index = node.content.findIndex(content => typeof content === 'object'); - - if (index !== -1) { - // Map component attributes that it's not defined - // as locals to first element of node - // for now only class and style - const nodeAttrs = parseAttrs(node.content[index].attrs); - - Object.keys(attributes).forEach(attr => { - if (typeof locals[attr] === 'undefined') { - if (['class'].includes(attr)) { - if (typeof nodeAttrs[attr] === 'undefined') { - nodeAttrs[attr] = []; - } - - nodeAttrs[attr].push(attributes[attr]); - } else if (['style'].includes(attr)) { - // Append style ? - nodeAttrs[attr] = attributes[attr]; - } - } - }); + node.content = mergeSlots(layoutTree, node, options, defaultSlotName); - node.content[index].attrs = nodeAttrs.compose(); - } + parseAttributes(node, attributes, locals, options); messages.push({ type: 'dependency', @@ -266,26 +244,77 @@ function parseSlotsLocals(tag, html, content, defaultSlotName) { return slots; } +/** + * Map component attributes that it's not defined as locals to first element of node + * @param {Object} node + * @param {Object} attributes + * @param {Object} locals + * @param {Object} options + * @return {void} + */ +function parseAttributes(node, attributes, locals, options) { + const index = node.content.findIndex(content => typeof content === 'object'); + + if (index !== -1) { + const nodeAttrs = parseAttrs(node.content[index].attrs, options.attrsParserRules); + + Object.keys(attributes).forEach(attr => { + if (typeof locals[attr] === 'undefined') { + if (['class'].includes(attr)) { + // Merge class + if (typeof nodeAttrs.class === 'undefined') { + nodeAttrs.class = []; + } + + nodeAttrs.class.push(attributes.class); + + delete attributes.class; + } else if (['override:class'].includes(attr)) { + // Override class + nodeAttrs.class = attributes['override:class']; + + delete attributes['override:class']; + } else if (['style'].includes(attr)) { + // Merge style + if (typeof nodeAttrs.style === 'undefined') { + nodeAttrs.style = {}; + } + + nodeAttrs.style = Object.assign(nodeAttrs.style, styleToObject(attributes.style)); + delete attributes.style; + } else if (['override:style'].includes(attr)) { + // Override style + nodeAttrs.style = attributes['override:style']; + delete attributes['override:style']; + } + } + }); + + node.content[index].attrs = nodeAttrs.compose(); + } +} + /** * Merge slots content * @param {Object} tree * @param {Object} node * @param {Boolean} strict * @param {String} slotTagName + * @param {String} fillTagName * @param {Boolean|String} fallbackSlotTagName * @param defaultSlotName * @return {Object} tree */ -function mergeSlots(tree, node, strict, {slotTagName, fallbackSlotTagName}, defaultSlotName) { +function mergeSlots(tree, node, {strict, slotTagName, fillTagName, fallbackSlotTagName}, defaultSlotName) { const slots = getSlots(slotTagName, tree, defaultSlotName, fallbackSlotTagName); // Slot in component.html - const fillSlots = getSlots(slotTagName, node.content, defaultSlotName); // Slot in page.html + const fillSlots = getSlots(fillTagName, node.content, defaultSlotName); // Slot in page.html const clean = content => content.replace(/(\n|\t)/g, '').trim(); // Retrieve main content, means everything that is not inside slots if (node.content) { - const contentOutsideSlots = node.content.filter(content => (content.tag !== slotTagName)); + const contentOutsideSlots = node.content.filter(content => (content.tag !== fillTagName)); if (contentOutsideSlots.filter(c => typeof c !== 'string' || clean(c) !== '').length > 0) { - fillSlots[defaultSlotName] = [{tag: slotTagName, attrs: {name: defaultSlotName}, content: [...contentOutsideSlots]}]; + fillSlots[defaultSlotName] = [{tag: fillTagName, attrs: {name: defaultSlotName}, content: [...contentOutsideSlots]}]; } // Replace with @@ -429,6 +458,12 @@ function applyPluginsToTree(tree, plugins) { }, tree); } +// function processWithPostHtml(html, options = {}, plugins = []) { +// return posthtml(plugins) +// .process(html, options) +// .then(result => result.tree); +// } + module.exports = (options = {}) => { options = { ...{ @@ -441,6 +476,7 @@ module.exports = (options = {}) => { tagPrefix: 'x-', tagRegExp: new RegExp(`^${options.tagPrefix || 'x-'}`, 'i'), slotTagName: 'slot', + fillTagName: 'slot', // Used for compatibility with modules plugin slot, set to true only if you have migrated from modules plugin fallbackSlotTagName: false, tagName: 'component', @@ -452,7 +488,8 @@ module.exports = (options = {}) => { encoding: 'utf8', scriptLocalAttribute: 'props', matcher: [], - strict: true + strict: true, + attrsParserRules: {} }, ...options }; diff --git a/test/test-locals.js b/test/test-locals.js index b08176d..c6ed1bd 100644 --- a/test/test-locals.js +++ b/test/test-locals.js @@ -24,8 +24,8 @@ test('Must process attributes as locals', async t => { }); test('Must process default attributes and map style and class for the first node', async t => { - const actual = ``; - const expected = `
My Title Default body
`; + const actual = ``; + const expected = `
My Title Default body
`; const html = await posthtml([plugin({root: './test/templates'})]).process(actual).then(result => clean(result.html)); diff --git a/test/test-plugins.js b/test/test-plugins.js index bba9217..e7b95a3 100644 --- a/test/test-plugins.js +++ b/test/test-plugins.js @@ -9,7 +9,7 @@ test('Must work with posthtml-extend syntax', async t => { const actual = `My Content`; const expected = `Extend Layout
My Content
footer content
`; - const html = await posthtml([plugin({root: './test/templates', tagName: 'extends', attribute: 'src', slotTagName: 'block'})]).process(actual).then(result => clean(result.html)); + const html = await posthtml([plugin({root: './test/templates', tagName: 'extends', attribute: 'src', slotTagName: 'block', fillTagName: 'block'})]).process(actual).then(result => clean(result.html)); t.is(html, expected); }); @@ -27,7 +27,7 @@ test('Must work with posthtml-extend and posthtml-modules syntax together', asyn const actual = `My Module Content`; const expected = `Extend With Module Layout
My Module Content
`; - const html = await posthtml([plugin({root: './test/templates', tagNames: ['extends', 'module'], attributes: ['src', 'href'], slotTagName: 'block', fallbackSlotTagName: true})]).process(actual).then(result => clean(result.html)); + const html = await posthtml([plugin({root: './test/templates', tagNames: ['extends', 'module'], attributes: ['src', 'href'], slotTagName: 'block', fillTagName: 'block', fallbackSlotTagName: true})]).process(actual).then(result => clean(result.html)); t.is(html, expected); });