stylejam
Version:
Basic Sass styleguides effortlessly
635 lines (620 loc) • 16.1 kB
JavaScript
// Copyright (c) 2016-present, salesforce.com, inc. All rights reserved
// Licensed under BSD 3-Clause - see LICENSE.txt or git.io/sfdc-license
'use strict'
const _ = require('lodash')
const invariant = require('invariant')
let { parse, stringify } = require('scss-parser')
/**
* Create a new {@link QueryWrapper}
*
* @function createQueryWrapper
* @param {object} ast
* @param {QueryWrapperOptions} options
* @returns {function}
*/
module.exports = (ast, options) => {
invariant(_.isPlainObject(ast), '"ast" must be a plain object')
/**
* @namespace QueryWrapperOptions
*/
options = _.defaults({}, options, {
/**
* Return true if the node has children
*
* @memberof QueryWrapperOptions
* @instance
* @param {object} node
* @returns {boolean}
*/
hasChildren: (node) => Array.isArray(node.value),
/**
* Return an array of children for a node
*
* @memberof QueryWrapperOptions
* @instance
* @param {object} node
* @returns {object[]}
*/
getChildren: (node) => node.value,
/**
* Return a string representation of the node's type
*
* @memberof QueryWrapperOptions
* @instance
* @param {object} node
* @returns {string}
*/
getType: (node) => node.type,
/**
* Convert the node back to JSON. This usually just means merging the
* children back into the node
*
* @memberof QueryWrapperOptions
* @instance
* @param {object} node
* @param {object[]} [children]
* @returns {string}
*/
toJSON: (node, children) => {
return Object.assign({}, node, {
value: children || node.value
})
},
/**
* Convert the node to a string
*
* @memberof QueryWrapperOptions
* @instance
* @param {object} node
* @returns {string}
*/
toString: (node) => {
return _.isString(node.value) ? node.value : ''
}
})
for (let key of ['hasChildren', 'getChildren', 'getType', 'toJSON', 'toString']) {
invariant(_.isFunction(options[key]),
`options.${key} must be a function`)
}
// Commonly used options
let { hasChildren, getChildren, getType, toJSON, toString } = options
/**
* Wrap an AST node to get some basic helpers / parent reference
*/
class NodeWrapper {
/**
* Create a new NodeWrapper
*
* @param {object} node
* @param {NodeWrapper} [parent]
*/
constructor (node, parent) {
/**
* @member {object}
*/
this.node = node
/**
* @member {NodeWrapper}
*/
this.parent = parent
/**
* @member {NodeWrapper[]}
*/
this.children = this.hasChildren
? getChildren(node).map((n) => new NodeWrapper(n, this))
: null
Object.freeze(this)
}
get hasChildren () {
return hasChildren(this.node)
}
/**
* Return the JSON representation
*
* @returns {object}
*/
toJSON () {
return toJSON(
this.node,
this.hasChildren ? this.children.map((n) => n.toJSON()) : null
)
}
/**
* Recursivley reduce the node and it's children
*
* @param {function} fn
* @param {any} acc
* @returns {object}
*/
reduce (fn, acc) {
return this.hasChildren
? fn(this.children.reduce((a, n) => n.reduce(fn, a), acc), this)
: fn(acc, this)
}
/**
* Create a new NodeWrapper or return the argument if it's already a NodeWrapper
*
* @param {object|NodeWrapper} node
* @param {NodeWrapper} [parent]
* @returns {NodeWrapper}
*/
static create (node, parent) {
if (node instanceof NodeWrapper) return node
return new NodeWrapper(node, parent)
}
/**
* Return true if the provided argument is a NodeWrapper
*
* @param {any} node
* @returns {NodeWrapper}
*/
static isNodeWrapper (node) {
return node instanceof NodeWrapper
}
}
/**
* The root node that will be used if no argument is provided to $()
*/
const ROOT = NodeWrapper.create(ast)
/*
* @typedef {string|regexp|function} Wrapper~Selector
*/
/**
* Return a function that will be used to filter an array of QueryWrappers
*
* @private
* @param {string|function} selector
* @param {boolean} required
* @returns {function}
*/
let getSelector = (selector, defaultValue) => {
defaultValue = !_.isUndefined(defaultValue)
? defaultValue : (n) => true
let isString = _.isString(selector)
let isRegExp = _.isRegExp(selector)
let isFunction = _.isFunction(selector)
if (!(isString || isRegExp || isFunction)) return defaultValue
if (isString) return (n) => getType(n.node) === selector
if (isRegExp) return (n) => selector.test(getType(n.node))
if (isFunction) return selector
}
/**
* Convenience function to return a new Wrapper
*
* @private
* @param {function|string|NodeWrapper|NodeWrapper[]} [selector]
* @param {NodeWrapper|NodeWrapper[]} [context]
* @returns {QueryWrapper}
*/
let $ = (selector, context) => {
let maybeSelector = getSelector(selector, false)
let nodes = _.flatten([(maybeSelector ? context : selector) || ROOT])
invariant(_.every(nodes, NodeWrapper.isNodeWrapper),
'context must be a NodeWrapper or array of NodeWrappers')
return maybeSelector
? new QueryWrapper(nodes).find(maybeSelector)
: new QueryWrapper(nodes)
}
/**
* Wrap a {@link NodeWrapper} with chainable traversal/modification functions
*/
class QueryWrapper {
/**
* Create a new QueryWrapper
*
* @private
* @param {NodeWrapper[]} nodes
*/
constructor (nodes) {
this.nodes = nodes
}
/**
* Return a new wrapper filtered by a selector
*
* @private
* @param {NodeWrapper[]} nodes
* @param {function} selector
* @returns {QueryWrapper}
*/
$filter (nodes, selector) {
nodes = nodes.filter(selector)
return $(nodes)
}
/**
* Return the wrapper as a JSON node or array of JSON nodes
*
* @param {number} [index]
* @returns {object|object[]}
*/
get (index) {
return _.isInteger(index)
? this.nodes[index].toJSON()
: this.nodes.map((n) => n.toJSON())
}
/**
* Return the number of nodes in the wrapper
*
* @returns {number}
*/
length () {
return this.nodes.length
}
/**
* Search for a given node in the set of matched nodes.
*
* If no argument is passed, the return value is an integer indicating
* the position of the first node within the Wrapper relative
* to its sibling nodes.
*
* If called on a collection of nodes and a NodeWrapper is passed in, the return value
* is an integer indicating the position of the passed NodeWrapper relative
* to the original collection.
*
* If a selector is passed as an argument, the return value is an integer
* indicating the position of the first node within the Wrapper relative
* to the nodes matched by the selector.
*
* If the selctor doesn't match any nodes, it will return -1.
*
* @param {NodeWrapper|Wrapper~Selector} [node]
* @returns {number}
*/
index (node) {
if (!node) {
let n = this.nodes[0]
if (n) {
let p = n.parent
if (p && p.hasChildren) return p.children.indexOf(n)
}
return -1
}
if (NodeWrapper.isNodeWrapper(node)) {
return this.nodes.indexOf(node)
}
let n = this.nodes[0]
let p = n.parent
if (!p.hasChildren) return -1
let selector = getSelector(node)
return this.$filter(p.children, selector).index(this.nodes[0])
}
/**
* Insert a node after each node in the set of matched nodes
*
* @param {object} node
* @returns {QueryWrapper}
*/
after (node) {
for (let n of this.nodes) {
let p = n.parent
if (!p.hasChildren) continue
let i = $(n).index()
if (i >= 0) p.children.splice(i + 1, 0, NodeWrapper.create(node, p))
}
return this
}
/**
* Insert a node before each node in the set of matched nodes
*
* @param {object} node
* @returns {QueryWrapper}
*/
before (node) {
for (let n of this.nodes) {
let p = n.parent
if (!p.hasChildren) continue
let i = p.children.indexOf(n)
if (i >= 0) p.children.splice(i, 0, NodeWrapper.create(node, p))
}
return this
}
/**
* Remove the set of matched nodes
*
* @returns {QueryWrapper}
*/
remove () {
for (let n of this.nodes) {
let p = n.parent
if (!p.hasChildren) continue
let i = p.children.indexOf(n)
if (i >= 0) p.children.splice(i, 1)
}
return this
}
/**
* Replace each node in the set of matched nodes by returning a new node
* for each node that will be replaced
*
* @param {function} fn
* @returns {QueryWrapper}
*/
replace (fn) {
for (let n of this.nodes) {
let p = n.parent
if (!p.hasChildren) continue
let i = p.children.indexOf(n)
if (i >= 0) p.children.splice(i, 1, NodeWrapper.create(fn(n), p))
}
return this
}
/**
* Map the set of matched nodes
*
* @param {function} fn
* @returns {array}
*/
map (fn) {
return this.nodes.map(fn)
}
/**
* Execute a function on each node in matched nodes
*
* @param {function} fn
* @returns {any}
*/
each (fn) {
return this.nodes.forEach(fn)
}
/**
* Reduce the set of matched nodes
*
* @param {function} fn
* @param {any} acc
* @returns {any}
*/
reduce (fn, acc) {
return this.nodes.reduce(fn, acc)
}
/**
* Combine the nodes of two QueryWrappers
*
* @param {QueryWrapper} wrapper
* @returns {any}
*/
concat (wrapper) {
invariant(wrapper instanceof QueryWrapper,
'concat requires a QueryWrapper')
return $(this.nodes.concat(wrapper.nodes))
}
/**
* Get the children of each node in the set of matched nodes,
* optionally filtered by a selector
*
* @param {Wrapper~Selector} [selector]
* @returns {QueryWrapper}
*/
children (selector) {
selector = getSelector(selector)
let nodes = _.flatMap(this.nodes, (n) => n.hasChildren ? n.children : [])
return this.$filter(nodes, selector)
}
/**
* For each node in the set of matched nodes, get the first node that matches
* the selector by testing the node itself and traversing up through its ancestors
*
* @param {Wrapper~Selector} [selector]
* @returns {QueryWrapper}
*/
closest (selector) {
selector = getSelector(selector)
let nodes = _.uniq(_.flatMap(this.nodes, (n) => {
let parent = n
while (parent) {
if (selector(parent)) break
parent = parent.parent
}
return parent || []
}))
return $(nodes)
}
/**
* Reduce the set of matched nodes to the one at the specified index
*
* @param {number} index
* @returns {QueryWrapper}
*/
eq (index) {
invariant(_.isInteger(index),
'eq() requires an index')
return $(this.nodes[index] || [])
}
/**
* Get the descendants of each node in the set of matched nodes,
* optionally filtered by a selector
*
* @param {Wrapper~Selector} [selector]
* @returns {QueryWrapper}
*/
find (selector) {
selector = getSelector(selector)
let nodes = _.uniq(_.flatMap(this.nodes, (n) =>
n.reduce((a, n) => selector(n) ? a.concat(n) : a, [])))
return $(nodes)
}
/**
* Reduce the set of matched nodes to those that match the selector
*
* @param {Wrapper~Selector} [selector]
* @returns {QueryWrapper}
*/
filter (selector) {
selector = getSelector(selector)
return this.$filter(this.nodes, selector)
}
/**
* Reduce the set of matched nodes to the first in the set.
*
* @returns {QueryWrapper}
*/
first () {
return this.eq(0)
}
/**
* Reduce the set of matched nodes to those that have a descendant
* that matches the selector
*
* @param {Wrapper~Selector} [selector]
* @returns {QueryWrapper}
*/
has (selector) {
let filter = (n) => $(n).find(selector).length() > 0
return this.$filter(this.nodes, filter)
}
/**
* Reduce the set of matched nodes to those that have a parent
* that matches the selector
*
* @param {Wrapper~Selector} [selector]
* @returns {QueryWrapper}
*/
hasParent (selector) {
let filter = (n) => $(n).parent(selector).length() > 0
return this.$filter(this.nodes, filter)
}
/**
* Reduce the set of matched nodes to those that have an ancestor
* that matches the selector
*
* @param {Wrapper~Selector} [selector]
* @returns {QueryWrapper}
*/
hasParents (selector) {
let filter = (n) => $(n).parents(selector).length() > 0
return this.$filter(this.nodes, filter)
}
/**
* Reduce the set of matched nodes to the final one in the set
*
* @returns {QueryWrapper}
*/
last () {
return this.eq(this.length() - 1)
}
/**
* Get the immediately following sibling of each node in the set of matched nodes,
* optionally filtered by a selector
*
* @param {Wrapper~Selector} [selector]
* @returns {QueryWrapper}
*/
next (selector) {
selector = getSelector(selector)
let nodes = _.flatMap(this.nodes, (n) => {
let index = this.index()
return index >= 0 && index < n.parent.children.length - 1
? n.parent.children[index + 1] : []
})
return this.$filter(nodes, selector)
}
/**
* Get all following siblings of each node in the set of matched nodes,
* optionally filtered by a selector
*
* @param {Wrapper~Selector} [selector]
* @returns {QueryWrapper}
*/
nextAll (selector) {
selector = getSelector(selector)
let nodes = _.flatMap(this.nodes, (n) => {
let index = this.index()
return index >= 0 && index < n.parent.children.length - 1
? _.drop(n.parent.children, index + 1) : []
})
return this.$filter(nodes, selector)
}
/**
* Get the parent of each nodes in the current set of matched nodess,
* optionally filtered by a selector
*
* @param {Wrapper~Selector} [selector]
* @returns {QueryWrapper}
*/
parent (selector) {
selector = getSelector(selector)
let nodes = this.nodes.map((n) => n.parent)
return this.$filter(nodes, selector)
}
/**
* Get the ancestors of each nodes in the current set of matched nodess,
* optionally filtered by a selector
*
* @param {Wrapper~Selector} [selector]
* @returns {QueryWrapper}
*/
parents (selector) {
selector = getSelector(selector)
let nodes = _.uniq(_.flatMap(this.nodes, (n) => {
let parents = []
let parent = n.parent
while (parent) {
parents.push(parent)
parent = parent.parent
}
return parents
}))
return this.$filter(nodes, selector)
}
/**
* Get the ancestors of each node in the set of matched nodes,
* up to but not including the node matched by the selector
*
* @param {Wrapper~Selector} [selector]
* @returns {QueryWrapper}
*/
parentsUntil (selector) {
selector = getSelector(selector)
let nodes = _.uniq(_.flatMap(this.nodes, (n) => {
let parents = []
let parent = n.parent
while (parent && !selector(parent)) {
parents.push(parent)
parent = parent.parent
}
return parents
}))
return $(nodes)
}
/**
* Get the immediately preceding sibling of each node in the set of matched nodes,
* optionally filtered by a selector
*
* @param {Wrapper~Selector} [selector]
* @returns {QueryWrapper}
*/
prev (selector) {
selector = getSelector(selector)
let nodes = _.flatMap(this.nodes, (n) => {
let index = this.index()
return index > 0 ? n.parent.children[index - 1] : []
})
return this.$filter(nodes, selector)
}
/**
* Get all preceding siblings of each node in the set of matched nodes,
* optionally filtered by a selector
*
* @param {Wrapper~Selector} [selector]
* @returns {QueryWrapper}
*/
prevAll (selector) {
selector = getSelector(selector)
let nodes = _.flatMap(this.nodes, (n) => {
let index = this.index()
return index > 0 ? _.take(n.parent.children, index).reverse() : []
})
return this.$filter(nodes, selector)
}
/**
* Get the combined string contents of each node in the set of matched nodes,
* including their descendants
*/
value () {
return this.nodes.reduce((v, n) => {
return n.reduce((v, n) => {
return v + toString(n.node)
}, v)
}, '')
}
}
return $
}