diff --git a/packages/heml-parse/package-lock.json b/packages/heml-parse/package-lock.json index 09210a0..6ac6661 100644 --- a/packages/heml-parse/package-lock.json +++ b/packages/heml-parse/package-lock.json @@ -1,6 +1,6 @@ { "name": "@heml/parse", - "version": "1.0.0", + "version": "1.0.2-0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/heml-parse/src/closeSelfClosingNodes.js b/packages/heml-parse/src/closeSelfClosingNodes.js new file mode 100644 index 0000000..99c15b7 --- /dev/null +++ b/packages/heml-parse/src/closeSelfClosingNodes.js @@ -0,0 +1,23 @@ +import selfClosingHtmlTags from 'html-tags/void' + +/** + * The HEML is parsed as XML. If the HEML contains a self closing tag without the closing slash + * all the siblings will be treated as children. This moves the children back to their place and + * forces the tag to be self closing + * @param {Cheerio} $ + * @param {Array} elements + */ +export default function($, elements) { + /** collect all the self closing nodes */ + const selfClosingTags = [ + ...selfClosingHtmlTags, + ...elements.filter((element) => element.children === false).map(({ tagName }) => tagName) ] + + const $selfClosingNodes = $.findNodes(selfClosingTags).reverse() + + /** Move contents from self wrapping tags outside of itself */ + $selfClosingNodes.forEach(($node) => { + $node.after($node.html()) + $node.html('') + }) +} diff --git a/packages/heml-parse/src/extractInlineStyles.js b/packages/heml-parse/src/extractInlineStyles.js new file mode 100644 index 0000000..3f605ec --- /dev/null +++ b/packages/heml-parse/src/extractInlineStyles.js @@ -0,0 +1,33 @@ +import randomString from 'crypto-random-string' +import { compact, first } from 'lodash' + +/** + * This extracts all inline styles on elements into a style tag to be inlined later + * so that the styles can be properly expanded and later re-inlined + * @param {Cheerio} $ + * @param {Array} elements + */ +export default function($, elements) { + /** try for head, fallback to body, then heml */ + const $head = first(compact([...$('head').toNodes(), ...$('body').toNodes(), ...$('heml').toNodes()])) + + /** move inline styles to a style tag with unique ids so they can be hit by the css processor */ + if ($head) { + const $inlineStyleNodes = $.findNodes(elements.map(({ tagName }) => tagName)).filter($node => !!$node.attr('style')) + + const inlineCSS = $inlineStyleNodes.map(($node) => { + let id = $node.attr('id') + const css = $node.attr('style') + $node.removeAttr('style') + + if (!id) { + id = `heml-${randomString(5)}` + $node.attr('id', id) + } + + return `#${id} {${css}}` + }).join('\n') + + if (inlineCSS.length > 0) $head.append(``) + } +} diff --git a/packages/heml-parse/src/index.js b/packages/heml-parse/src/index.js index 5325da8..0f617a6 100644 --- a/packages/heml-parse/src/index.js +++ b/packages/heml-parse/src/index.js @@ -1,10 +1,7 @@ import { load } from 'cheerio' -import { difference, compact, first } from 'lodash' -import randomString from 'crypto-random-string' -import htmlTags from 'html-tags' -import selfClosingHtmlTags from 'html-tags/void' - -const wrappingHtmlTags = difference(htmlTags, selfClosingHtmlTags) +import closeSelfClosingNodes from './closeSelfClosingNodes' +import openWrappingNodes from './openWrappingNodes' +import extractInlineStyles from './extractInlineStyles' function parse (contents, options = {}) { const { @@ -31,51 +28,9 @@ function parse (contents, options = {}) { .map((node) => $(node)) } - const selfClosingTags = [ - ...selfClosingHtmlTags, - ...elements.filter((element) => element.children === false).map(({ tagName }) => tagName) ] - const wrappingTags = [ - ...wrappingHtmlTags, - ...elements.filter((element) => element.children !== false).map(({ tagName }) => tagName) ] - - const $selfClosingNodes = $.findNodes(selfClosingTags).reverse() - const $wrappingNodes = $.findNodes(wrappingTags).reverse() - - /** Move contents from self wrapping tags outside of itself */ - $selfClosingNodes.forEach(($node) => { - $node.after($node.html()) - $node.html('') - }) - - /** ensure that all wrapping tags have at least a zero-width, non-joining character */ - $wrappingNodes.forEach(($node) => { - if ($node.html().length === 0) { - $node.html(' ') - } - }) - - /** try for head, fallback to body, then heml */ - const $head = first(compact([...$('head').toNodes(), ...$('body').toNodes(), ...$('heml').toNodes()])) - - /** move inline styles to a style tag with unique ids so they can be hit by the css processor */ - if ($head) { - const $inlineStyleNodes = $.findNodes(elements.map(({ tagName }) => tagName)).filter($node => !!$node.attr('style')) - - const inlineCSS = $inlineStyleNodes.map(($node) => { - let id = $node.attr('id') - const css = $node.attr('style') - $node.removeAttr('style') - - if (!id) { - id = `heml-${randomString(5)}` - $node.attr('id', id) - } - - return `#${id} {${css}}` - }).join('\n') - - $head.append(``) - } + closeSelfClosingNodes($, elements) + openWrappingNodes($, elements) + extractInlineStyles($, elements) return $ } diff --git a/packages/heml-parse/src/openWrappingNodes.js b/packages/heml-parse/src/openWrappingNodes.js new file mode 100644 index 0000000..1a3b651 --- /dev/null +++ b/packages/heml-parse/src/openWrappingNodes.js @@ -0,0 +1,27 @@ +import htmlTags from 'html-tags' +import selfClosingHtmlTags from 'html-tags/void' +import { difference } from 'lodash' + +const wrappingHtmlTags = difference(htmlTags, selfClosingHtmlTags) + +/** + * The HEML is parsed as XML. If the HEML contains a wrapping tag with no content it will be + * optimized to be self closing. This add a placeholder space to all empty wrapping tags + * @param {Cheerio} $ + * @param {Array} elements + */ +export default function($, elements) { + /** collect all the wrapping nodes */ + const wrappingTags = [ + ...wrappingHtmlTags, + ...elements.filter((element) => element.children !== false).map(({ tagName }) => tagName) ] + + const $wrappingNodes = $.findNodes(wrappingTags).reverse() + + /** ensure that all wrapping tags have at least a space */ + $wrappingNodes.forEach(($node) => { + if ($node.html().length === 0) { + $node.html(' ') + } + }) +} diff --git a/packages/heml-styles/package-lock.json b/packages/heml-styles/package-lock.json index 337ff10..d3cfb4b 100644 --- a/packages/heml-styles/package-lock.json +++ b/packages/heml-styles/package-lock.json @@ -1,6 +1,6 @@ { "name": "@heml/styles", - "version": "1.0.0", + "version": "1.0.2-0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -92,6 +92,16 @@ "write-file-stdout": "0.0.2" } }, + "css-selector-tokenizer": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz", + "integrity": "sha1-5piEdK6MlTR3v15+/s/OzNnPTIY=", + "requires": { + "cssesc": "0.1.0", + "fastparse": "1.1.1", + "regexpu-core": "1.0.0" + } + }, "css-shorthand-expand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/css-shorthand-expand/-/css-shorthand-expand-1.1.0.tgz", @@ -131,6 +141,11 @@ "resolved": "https://registry.npmjs.org/css-url-regex/-/css-url-regex-0.0.1.tgz", "integrity": "sha1-4Fr4xsKQ1FHvFjK0VepcgbSxOVw=" }, + "cssesc": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", + "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=" + }, "cssnano-util-get-arguments": { "version": "4.0.0-rc.2", "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0-rc.2.tgz", @@ -151,6 +166,11 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, + "fastparse": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.1.tgz", + "integrity": "sha1-0eJkOzipTXWDtHkGDmxK/8lAcfg=" + }, "flatten": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz", @@ -225,6 +245,11 @@ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.3.2.tgz", "integrity": "sha512-Y2/+DnfJJXT1/FCwUebUhLWb3QihxiSC42+ctHLGogmW2jPY6LCapMdFZXRvVP2z6qyKW7s6qncE/9gSqZiArw==" }, + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" + }, "lodash": { "version": "4.17.4", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", @@ -1480,6 +1505,34 @@ "postcss-value-parser": "3.3.0" } }, + "regenerate": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.3.tgz", + "integrity": "sha512-jVpo1GadrDAK59t/0jRx5VxYWQEDkkEKi6+HjE3joFVLfDOh9Xrdh0dF1eSq+BI/SwvTQ44gSscJ8N5zYL61sg==" + }, + "regexpu-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", + "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", + "requires": { + "regenerate": "1.3.3", + "regjsgen": "0.2.0", + "regjsparser": "0.1.5" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=" + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "requires": { + "jsesc": "0.5.0" + } + }, "repeat-element": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", diff --git a/packages/heml-styles/package.json b/packages/heml-styles/package.json index ce46fcf..2a46dbf 100644 --- a/packages/heml-styles/package.json +++ b/packages/heml-styles/package.json @@ -22,6 +22,7 @@ }, "dependencies": { "css-declaration-sorter": "^2.1.0", + "css-selector-tokenizer": "^0.7.0", "css-shorthand-expand": "^1.1.0", "lodash": "^4.17.4", "postcss": "^6.0.13", diff --git a/packages/heml-styles/src/plugins/postcss-element-expander/index.js b/packages/heml-styles/src/plugins/postcss-element-expander/index.js index ce1eca8..e03b90b 100644 --- a/packages/heml-styles/src/plugins/postcss-element-expander/index.js +++ b/packages/heml-styles/src/plugins/postcss-element-expander/index.js @@ -38,14 +38,6 @@ export default postcss.plugin('postcss-element-expander', ({ elements, aliases } return (root, result) => { for (let element of elements) { - /** - * add the element tag to any css selectors that implicitly target an element - * .i.e. #my-button that selects - */ - root.walkRules((rule) => { - tagAliasSelectors(element, aliases[element.tag], rule) - }) - /** * There are 3 (non-mutually exclusive) possibilities when it contains the element tag * diff --git a/packages/heml-styles/src/plugins/postcss-element-expander/tagAliasSelectors.js b/packages/heml-styles/src/plugins/postcss-element-expander/tagAliasSelectors.js deleted file mode 100644 index be625a4..0000000 --- a/packages/heml-styles/src/plugins/postcss-element-expander/tagAliasSelectors.js +++ /dev/null @@ -1,106 +0,0 @@ -import selectorParser from 'postcss-selector-parser' - -const simpleSelectorParser = selectorParser() - -/** - * Add the element tag to selectors from the rule that match the element alias - * @param {Object} element element definition - * @param {Array[$node]} aliases array of cheerio nodes - * @param {Rule} rule postcss node - */ -export default function (element, aliases, rule) { - if (!aliases) return - - let selectors = [] - - rule.selectors.forEach((selector) => { - const matchedAliases = aliases.filter((alias) => alias.is(selector.replace(/::?\S*/g, ''))).length > 0 - - /** the selector in an alias that doesn't target the tag already */ - if (matchedAliases && !targetsTag(selector)) { - selectors.push(appendElementSelector(element, selector)) - } - - /** dont add the original selector back in if it targets a pseudo selector */ - if (!targetsElementPseudo(element, selector)) { selectors.push(selector) } - }) - - rule.selectors = selectors -} - -/** - * checks if selector targets a tag - * @param {String} selector the selector - * @return {Boolean} if the selector targets a tag - */ -function targetsTag (selector) { - const selectors = simpleSelectorParser.process(selector).res - - return selectors.filter((selector) => { - let selectorNodes = selector.nodes.concat([]).reverse() // clone the array - - for (const node of selectorNodes) { - if (node.type === 'cominator') { break } - - if (node.type === 'tag') { return true } - } - - return false - }).length > 0 -} - -/** - * find all selectors that target the give element - * @param {Object} element the element definition - * @param {String} selector the selector - * @return {Array} the matched selectors - */ -function targetsElementPseudo (element, selector) { - const selectors = simpleSelectorParser.process(selector).res - - return selectors.filter((selector) => { - let selectorNodes = selector.nodes.concat([]).reverse() // clone the array - - for (const node of selectorNodes) { - if (node.type === 'cominator') { break } - - if (node.type === 'pseudo' && node.value.replace(/::?/, '') in element.pseudos) { - return true - } - - if (node.type === 'tag' && node.value === element.tag) { break } - } - - return false - }).length > 0 -} - -/** - * Add the element tag to the end of the selector - * @param {Object} element element definition - * @param {String} selector the selector - * @return {String} the modified selector - */ -function appendElementSelector (element, selector) { - const processor = selectorParser((selectors) => { - let combinatorNode = null - - /** - * looping breaks if we insert dynamically - */ - selectors.each((selector) => { - const elementNode = selectorParser.tag({ value: element.tag }) - selector.walk((node) => { - if (node.type === 'combinator') { combinatorNode = node } - }) - - if (combinatorNode) { - selector.insertAfter(combinatorNode, elementNode) - } else { - selector.prepend(elementNode) - } - }) - }) - - return processor.process(selector).result -} diff --git a/packages/heml-styles/src/preprocess.js b/packages/heml-styles/src/preprocess.js new file mode 100644 index 0000000..60071f7 --- /dev/null +++ b/packages/heml-styles/src/preprocess.js @@ -0,0 +1,315 @@ +import postcss, { plugin } from 'postcss' +import safeParser from 'postcss-safe-parser' +import { parse as parseSelector, stringify as stringfySelector } from 'css-selector-tokenizer' +import { get, first, last, intersection, uniq } from 'lodash' + +function stringifySelectorNodes(nodes) { + return stringfySelector({ type: 'selector', nodes }) +} + +const complexRelationships = ['>', '~', '+'] +// not ":not()" because it can contain a dynamicPseudoSelector +const staticPseudoSelectors = [ 'first-child', 'last-child', 'first-of-type', 'last-of-type', 'nth-child', 'nth-last-child', 'nth-of-type', 'nth-last-of-type', 'empty' ] +const dynamicPseudoSelectors = [ 'hover', 'active', 'focus', 'link', 'visited', 'target', 'checked', 'in-range', 'out-of-range', 'invalid', 'scope' ] +const pseudoElements = [ 'after', 'before', 'first-letter', 'first-line', 'selection', 'backdrop', 'placeholder', 'marker', 'spelling-error', 'grammar-error' ] + +/** + * process all the style tags + * @param {Cheerio} $ + * @param {Array} elements + */ +function processStyles($, elements) { + $.findNodes('style').forEach(($node) => { + const css = processCSS($, elements, $node.html()) + + $node.html(css) + }) +} + +/** + * process the given css + * @param {Cheerio} $ + * @param {String} contents some css + * @return {String} the modified css + */ +function processCSS($, elements, contents) { + const { css } = postcss([ + safeSelectorize($), + tagAllAliasSelectors($, elements), + ]).process(contents, { parser: safeParser }) + + return css +} + +const tagAllAliasSelectors = plugin('postcss-tag-aliases', ($, elements) => (root) => { + /** + * add the element tag to any css selectors that implicitly target an element + * .i.e. #my-button that selects + */ + const elementNames = elements.map(({ tagName }) => tagName) + + root.walkRules((rule) => { + let newSelectors = [] + + rule.selectors.forEach((selector) => { + /** skip if we already target a tag (no need to alias) */ + if (targetsTag(selector)) return newSelectors.push(selector) + + const selectedTags = uniq($.findNodes(queryableSelector(selector)).map(($node) => $node[0].name)) + const targetSingleTag = selectedTags.length === 1 + const selectedElements = intersection(selectedTags, elementNames) + const targetsNonElements = selectedTags.length > selectedElements.length + + /** skip if we are not targeting any elements (no need to alias) */ + if (selectedElements.length === 0) return newSelectors.push(selector) + + /** if we target only one tag/element, just drop the tag onto the selector */ + if (targetSingleTag) { + const elementName = first(selectedElements) + return newSelectors.push(appendElementSelector(elementName, selector)) + } + + /** if we target more than one tag/element, generate specific selector for each element */ + for (let elementName of selectedElements) { + newSelectors.push(buildTheElementSpecificSelector(elementName, selector, $)) + } + + /** if we target non-elements, we need to keep the original selector on */ + if (targetsNonElements) return newSelectors.push(selector) + }) + + rule.selectors = newSelectors + }) +}) + + +/** + * Add the element tag to the end of the selector + * @param {Object} element element definition + * @param {String} selector the selector + * @return {String} the modified selector + */ +function appendElementSelector (elementName, selector) { + const nodes = first(parseSelector(selector).nodes).nodes + + // default to the last node in case there is no combinator + let lastCombinatorIndex = nodes.length - 1 + nodes.forEach((node, i) => { + if (node.type === 'operator' || node.type === 'spacing') { + lastCombinatorIndex = i + 1 + } + }) + + nodes.splice(lastCombinatorIndex, 0, { name: elementName, type: 'element' }) + + return stringifySelectorNodes(nodes) +} + + +function buildTheElementSpecificSelector(elementName, selector, $) { + const nodes = first(parseSelector(selector).nodes).nodes.reverse() + const $elementNodes = $.findNodes(queryableSelector(appendElementSelector(elementName, selector))) + + for (const node of nodes) { + if (node.type === 'operator' || node.type === 'spacing') { break } + + if (node.type === 'class') { + const newClass = `${node.name}-${elementName}` + $elementNodes.forEach(($node) => $node.removeClass(node.name).addClass(newClass)) + node.name = newClass + } + + if (node.type === 'id') { + const newId = `${node.name}-${elementName}` + $elementNodes.forEach(($node) => $node.attr('id', newId)) + node.name = newId + } + } + + return appendElementSelector(elementName, stringifySelectorNodes(nodes.reverse())) +} + +function queryableSelector(selector) { + const { nodes } = first(parseSelector(selector).nodes) + + /** remove all non-static pseudo selectors/elements */ + return stringifySelectorNodes(nodes.filter((node) => { + return !(node.type.startsWith('pseudo') && !staticPseudoSelectors.includes(node.name)) + })) +} + +/** + * checks if selector targets a tag + * @param {String} selector the selector + * @return {Boolean} if the selector targets a tag + */ +function targetsTag (selector) { + const nodes = first(parseSelector(selector).nodes).nodes.reverse() + + for (const node of nodes) { + if (node.type === 'operator' || node.type === 'spacing') { return false } + + if (node.type === 'element') { return true } + } +} + + + + + + + + + + + + + + + + + + + + + + +/** + * This converts all complex selectors into classes via shorthash and applies them + * to the elements that should be selected as to allow for the most cross client support + * @param {Cheerio} $ + * @param {Array} elements + */ +const safeSelectorize = plugin('postcss-safe-selectorize', ($) => (root) => { + root.walkRules((rule) => { + rule.selectors = rule.selectors.map((selector) => { + if (isComplexSelector(selector)) { + return convertToSafeSelector(selector, $) + } + + return selector + }) + }) +}) + +/** + * checks if the given selector contains any parts that have less then ideal support + * @param {String]} selector + * @return {Boolean} isComplex + */ +function isComplexSelector(selector) { + const { nodes } = first(parseSelector(selector).nodes) + + return nodes.filter(({ type, operator, name }) => { + /** complex relationships */ + if (type === 'operator' && complexRelationships.includes(operator)) return true + + /** attribute selector */ + if (type === 'attribute') return true + + /** static pseudo selectors */ + if (type.startsWith('pseudo') && staticPseudoSelectors.includes(name)) return true + + /** universal selector */ + if (type === 'universal') return true + + return false + }).length > 0 +} + +function () + +/** + * builds a map of selectors to be replaced with the corresponding class + * @param {String} selector + * @return {Map} selectorAndClassMap + */ +function convertToSafeSelector(selector, $) { + const { nodes } = first(parseSelector(selector).nodes) + const classes = new Map() + + + nodes.forEach((node, index) => { + if (type === 'operator' && complexRelationships.includes(operator)) { + const prevPart = getPrevPart(nodes, index) + const { id, classes, tags } = calculateSelectorStore(prevPart) + } + + if (type === 'universal') + }) + + + /** + * 1. gather all the nodes until a pseudo element or dynamic pseudo selector + * 2. create a class for the selector part and add selector part/class to the map + */ + // nodes.forEach((node, index) => { + /** we have a matched pseudo - drop the node, build the previous nodes to a string, and add it to the map entry */ + // if (node.type.startsWith('pseudo') && (pseudoElements.includes(node.name) || dynamicPseudoSelectors.includes(node.name))) { + // const selectorPart = stringifySelectorNodes(selectorPartNodes) + // map.set(toClass(selectorPart), selectorPart) + + /** + * keep the previous last node on so that the selector continues to work + * .i.e. a:hover > b will become these selectors ['a', 'a > b'] + */ + // selectorPartNodes = selectorPartNodes.length > 0 ? [ last(selectorPartNodes) ] : [] + // } + /** we are on the last element, push it on, and add the */ + // else if (index === nodes.length - 1) { + // selectorPartNodes.push(node) + // const selectorPart = stringifySelectorNodes(selectorPartNodes) + // map.set(toClass(selectorPart), selectorPart) + // } + /** push the node to the current selector part */ + // else { + // selectorPartNodes.push(node) + // } + }) + + return map +} + +/** + * generate a selector that uses the same classes as what was generated in generateClassesForSelector, but leaves in all the pseudo pieces + * @param {String} selector + * @return {Map} selectorAndClassMap + */ +function generateReplacementSelector(selector) { + const { nodes } = first(parseSelector(selector).nodes) + const map = new Map() + let selectorPartNodes = [] + let replacementSelector = '' + + /** + * 1. gather all the nodes until a pseudo element or dynamic pseudo selector + * 2. create a class for the selector part and add selector part/class to the map + */ + nodes.forEach((node, index) => { + /** we have a matched pseudo - build the previous nodes to a string, and add it to the replacement selector, append the pseudo */ + if (node.type.startsWith('pseudo') && (pseudoElements.includes(node.name) || dynamicPseudoSelectors.includes(node.name))) { + const selectorPart = stringifySelectorNodes(selectorPartNodes) + replacementSelector += `.${toClass(selectorPart)}${stringifySelectorNodes([node])} ` + + /** + * keep the previous last node on so that the selector continues to work + * .i.e. a:hover > b will become these selectors ['a', 'a > b'] + */ + selectorPartNodes = selectorPartNodes.length > 0 ? [ last(selectorPartNodes) ] : [] + } + /** we are on the last element, push it on, and add the class */ + else if (index === nodes.length - 1) { + selectorPartNodes.push(node) + const selectorPart = stringifySelectorNodes(selectorPartNodes) + replacementSelector += `.${toClass(selectorPart)} ` + } + /** push the node to the current selector part */ + else { + selectorPartNodes.push(node) + } + }) + + return replacementSelector +} + +export default processStyles diff --git a/packages/heml-utils/package-lock.json b/packages/heml-utils/package-lock.json index 4209c69..f3f4088 100644 --- a/packages/heml-utils/package-lock.json +++ b/packages/heml-utils/package-lock.json @@ -1,6 +1,8 @@ { - "requires": true, + "name": "@heml/utils", + "version": "1.0.2-0", "lockfileVersion": 1, + "requires": true, "dependencies": { "css-groups": { "version": "0.1.1", @@ -11,6 +13,11 @@ "version": "4.17.4", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + }, + "shorthash": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/shorthash/-/shorthash-0.0.2.tgz", + "integrity": "sha1-WbJo7sveWQOLMNogK8+93rLEpOs=" } } } diff --git a/packages/heml-utils/package.json b/packages/heml-utils/package.json index e5bc8b6..fbab087 100644 --- a/packages/heml-utils/package.json +++ b/packages/heml-utils/package.json @@ -23,6 +23,7 @@ "dependencies": { "@heml/render": "^1.0.2-0", "css-groups": "^0.1.1", - "lodash": "^4.17.4" + "lodash": "^4.17.4", + "shorthash": "0.0.2" } } diff --git a/packages/heml-utils/src/createElement.js b/packages/heml-utils/src/createElement.js index e39b887..a0003a6 100644 --- a/packages/heml-utils/src/createElement.js +++ b/packages/heml-utils/src/createElement.js @@ -27,7 +27,5 @@ export default function (name, element) { postRender () {} }) - element.defaultAttrs.class = element.defaultAttrs.class || '' - return element } diff --git a/packages/heml-utils/src/index.js b/packages/heml-utils/src/index.js index 77ebeed..6a428e2 100644 --- a/packages/heml-utils/src/index.js +++ b/packages/heml-utils/src/index.js @@ -4,5 +4,6 @@ import createElement from './createElement' import HEMLError from './HEMLError' import transforms from './transforms' import condition from './condition' +import toClass from './toClass' -module.exports = { createElement, renderElement, HEMLError, cssGroups, transforms, condition } +module.exports = { createElement, renderElement, HEMLError, cssGroups, transforms, condition, toClass } diff --git a/packages/heml-utils/src/toClass.js b/packages/heml-utils/src/toClass.js new file mode 100644 index 0000000..99fa1b0 --- /dev/null +++ b/packages/heml-utils/src/toClass.js @@ -0,0 +1,10 @@ +import { unique } from 'shorthash' + +/** + * generates a consistent short class-safe hash from a longer string + * @param {String} str the string used in the hash function + * @return {String} the class + */ +export default function toClass(s) { + return `c${unique(s)}` +}