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)}`
+}