Skip to content

Instantly share code, notes, and snippets.

@jonathantneal
Created December 28, 2019 07:02
Show Gist options
  • Save jonathantneal/c9fbfd9f6d7fdd8a4a4e4884052914ec to your computer and use it in GitHub Desktop.
Save jonathantneal/c9fbfd9f6d7fdd8a4a4e4884052914ec to your computer and use it in GitHub Desktop.
CSS Parser (2019-12-28)

At 1.7kB, parse.js can parse CSS as component values and mutate the tree with visitors:

var source = ':nth-child(5) {\n\tcolor: var(--foo, var(--bar, var(--cat, blue)));\n}';
var cssast = parse(source);
cssast.visit({
  CSSFunction: {
    exit: function(node) {
      if (node.name === 'var') {
        node.replaceSelf(node.last);
      }
    }
  }
});
var expect = ':nth-child(5) {\n\tcolor: blue;\n}';
console.log(String(cssast) === expect); // true
parse = (function(tokenRegExp, bracketOpeningRegExp, bracketClosingRegExp) {
// ...
var arrayPrototype = Array.prototype;
// ...
var arrayFilter = arrayPrototype.filter;
var arrayForEach = arrayPrototype.forEach;
var arrayIndexOf = arrayPrototype.indexOf;
var arrayJoin = arrayPrototype.join;
var arrayPush = arrayPrototype.push;
var arraySlice = arrayPrototype.slice;
var arraySplice = arrayPrototype.splice;
var arrayUnshift = arrayPrototype.unshift;
// ...
var objectGetPrototypeOf = Object.getPrototypeOf;
var objectCreate = Object.create;
var objectHasOwnProperty = Function.call.bind(Object.hasOwnProperty);
var objectDefineProperties = Object.defineProperties;
var objectDefineProperty = Object.defineProperty;
// ...
function noop() {}
function toCSSClass(name, Class, Super, protoDescriptors, staticDescriptors) {
return (objectDefineProperties((objectDefineProperties((Class = Function('f', 'return function CSS' + name + '){f.apply(this,arguments)}')(Class)), Object(staticDescriptors)).prototype = objectCreate(Object(Super).prototype)), Object(protoDescriptors)).constructor = Class);
}
function asValue(value) {
return {
configurable: true,
writable: true,
value: value
};
}
function asAccessor(getter, setter) {
return {
configurable: true,
get: getter,
set: setter
};
}
function cssNodeSourceSetter() {
var source = { start: null, end: null };
return this instanceof CSSNode && objectDefineProperty(this, 'source', { configurable: true, get: get, set: set }) && arguments.length ? set.apply(this, arguments) : source;
function get() {
return source;
}
function set(nextSource) {
var start = Object(nextSource).start;
var end = Object(nextSource).end;
source.start = isFinite(start) ? start : null;
source.end = isFinite(end) ? end : null;
return source;
}
}
function cssNodeParentAccessor() {
var parent = null;
return this instanceof CSSNode && objectDefineProperty(this, 'parent', { configurable: true, get: get, set: set }) && arguments.length ? set.apply(this, arguments) : parent;
function get() {
return parent;
}
function set(nextParent, ignorePush) {
if (nextParent !== parent) {
if (isCSSBlock(parent)) {
parent.remove(this);
}
if (isCSSBlock(nextParent)) {
if (!ignorePush) arrayPush.call(nextParent.value, this);
parent = nextParent;
} else {
parent = null;
}
}
return true;
}
}
function cssBlockValueAccessor() {
var value = [];
return this instanceof CSSBlock && objectDefineProperty(this, 'value', { configurable: true, get: get, set: set }) && arguments.length ? set.apply(this, arguments) : value;
function get() {
return value;
}
function set(nextValue) {
arraySplice.bind(value, 0, value.length).apply(null, nextValue == null ? [] : arraySlice.call(nextValue));
return true;
}
}
function cssNodeIndexSelfGetter() {
if (isCSSNode(this)) {
var parent = this.parent;
if (isCSSBlock(parent)) {
return arrayIndexOf.call(parent.value, this);
}
}
return -1;
}
function cssNodeIndexSelfSetter(nextIndex) {
var index = isFinite(nextIndex) && cssNodeIndexSelfGetter.call(this);
if (index !== -1) {
arraySplice.bind(parent.value, nextIndex, 0).apply(null, arraySplice.call(this, index, 1));
return true;
}
}
function cssNodeTypeGetter() {
return Object(objectGetPrototypeOf(this)).constructor.name;
}
// ...
var CSSNode = toCSSClass(
'Node(_',
function(value) {
this.value = value;
},
Object,
{
value: asValue(''),
parent: asAccessor(cssNodeParentAccessor, cssNodeParentAccessor),
indexSelf: asAccessor(cssNodeIndexSelfGetter, cssNodeIndexSelfSetter),
removeSelf: asValue(function removeSelf() {
var index = this.indexSelf;
if (index !== -1) {
arraySplice.call(this.parent.value, index, 1);
}
}),
replaceSelf: asValue(function replaceSelf() {
var index = cssNodeIndexSelfGetter.call(this);
if (index !== -1) {
arraySplice.bind(this.parent.value, index, 1).apply(null, arrayFilter.call(arguments, isCSSNode));
}
}),
visit: asValue(function visit(visitors) {
visiting(this);
return this;
function visiting(node) {
var visitor = visitors[cssNodeTypeGetter.call(node)];
if (typeof visitor === 'function') {
visitor.call(visitors, node);
} else if (typeof Object(visitor).enter === 'function') {
visitor.enter.call(visitors, node);
}
if (node.value === Object(node.value)) {
arrayForEach.call(node.value, visiting);
}
if (typeof Object(visitor).exit === 'function') {
visitor.exit.call(visitors, node);
}
}
}),
source: asAccessor(cssNodeSourceSetter, cssNodeSourceSetter),
toString: asValue(function toString() {
return '' + this.value;
}),
type: asAccessor(cssNodeTypeGetter, noop)
}
);
var CSSMeta = toCSSClass('Meta(_', CSSNode, CSSNode);
var AtKeyword = toCSSClass('AtKeyword(_', CSSNode, CSSNode, {
toString: asValue(function toString() {
return '@' + this.value;
})
});
var cssBlockIndexOf = function indexOf(node) {
return isCSSBlock(this) && isCSSNode(node) ? arrayIndexOf.call(this.value, node) : -1;
};
var cssBlockReplace = function replace(replacee) {
var index = cssBlockIndexOf.call(this, replacee);
if (index !== -1) {
cssNodeParentAccessor.call(replacee, null, true);
arraySplice.bind(this.value, index, 1).apply(
null,
arraySlice.call(arguments, 1).filter(function(replacer) {
return cssNodeParentAccessor.call(replacer, this, true);
}, this)
);
}
return this;
};
var cssBlockRemove = function remove(removee) {
return cssBlockReplace.call(this, removee);
};
var cssBlockAppend = function append() {
if (isCSSBlock(this)) {
arrayPush.apply(
this.value,
arrayFilter.call(
arguments,
function(appender) {
return cssBlockValueAccessor.call(appender, this, true);
},
this
)
);
}
return this;
};
var cssBlockPrepend = function prepend() {
if (isCSSBlock(this)) {
arrayUnshift.apply(
this.value,
arrayFilter.call(
arguments,
function(prepender) {
return cssBlockValueAccessor.call(prepender, this, true);
},
this
)
);
}
return this;
};
// ...
var CSSBlock = toCSSClass(
'Block(',
function() {
this.name = '';
this.bracket = '';
this.mirror = '';
},
CSSNode,
{
name: asValue(''),
bracket: asValue(''),
mirror: asValue(''),
value: asAccessor(cssBlockValueAccessor, cssBlockValueAccessor),
toString: asValue(function toString() {
return '' + this.name + this.bracket + arrayJoin.call(this.value, '') + this.mirror;
}),
first: asAccessor(
function() {
return isCSSBlock(this) && this.value[0];
},
function(first) {
return cssNodeParentAccessor.call(first, this, true) && (this.value[0] = first);
}
),
last: asAccessor(
function() {
return isCSSBlock(this) && this.value[this.value.length - 1];
},
function(last) {
return cssNodeParentAccessor.call(last, this, true) && (this.value[this.value.length - 1] = last);
}
),
append: asValue(cssBlockAppend),
indexOf: asValue(cssBlockIndexOf),
prepend: asValue(cssBlockPrepend),
remove: asValue(cssBlockRemove),
replace: asValue(cssBlockReplace)
}
);
function isCSSBlock(value) {
return value instanceof CSSBlock;
}
function isCSSNode(value) {
return value instanceof CSSNode;
}
var CSSRoot = toCSSClass('Root(', CSSBlock, CSSBlock);
var CSSFunction = toCSSClass('Function(_', CSSBlock, CSSBlock);
var CSSList = [
null,
toCSSClass('Comment(_', CSSNode, CSSMeta),
toCSSClass('Whitespace(_', CSSNode, CSSMeta),
toCSSClass('String(_', CSSNode, CSSNode),
toCSSClass('Hash(_', CSSNode, CSSNode),
toCSSClass(
'Unit(_',
function(value, unit) {
CSSNode.call(this, value);
this.unit = arguments.length < 2 ? '' : String(unit);
},
CSSNode,
{
toString: asValue(function toString() {
return '' + this.value + this.unit;
})
}
),
toCSSClass('CDC(_', CSSNode, CSSNode),
toCSSClass('CDO(_', CSSNode, CSSNode),
toCSSClass('Ident(_', CSSNode, CSSNode),
toCSSClass('Delim(_', CSSNode, CSSNode)
];
return function parse(cssText) {
var root = new CSSRoot();
root.source.start = 0;
var parent = root;
var lastStart = 0;
var lastEnd = 0;
var result, thisTokenIndex, lastTokenIndex, thisValue, lastValue;
while ((result = tokenRegExp.exec(cssText))) {
for (thisTokenIndex = 0; result[++thisTokenIndex] === undefined; ) {}
thisValue = result[0];
if (lastTokenIndex === 5 && thisTokenIndex === 8) {
// unit = number + ident
parent.last.unit = thisValue;
} else if (lastTokenIndex === 5 && thisTokenIndex === 9 && thisValue === '%') {
// unit = number + "%"
parent.last.unit = thisValue;
} else if (lastTokenIndex === 9 && thisTokenIndex === 8 && lastValue === '@') {
// at-keyword = "@" + ident
parent.last = new AtKeyword(thisValue);
parent.last.source.start = lastStart;
} else if (lastTokenIndex === 8 && thisTokenIndex === 9 && thisValue === '(') {
// enter function = ident + "("
parent.last = new CSSFunction();
parent.last.name = lastValue;
parent.last.source.start = lastStart;
} else {
lastStart = parent.append(new CSSList[thisTokenIndex](thisValue)).last.source.start = lastEnd;
}
lastTokenIndex = thisTokenIndex;
lastValue = thisValue;
lastEnd = parent.last.source.end = tokenRegExp.lastIndex;
if (thisTokenIndex === 9 && bracketOpeningRegExp.test(thisValue)) {
if (!(parent.last instanceof CSSBlock)) {
parent.last = new CSSBlock();
parent.last.source.start = lastStart;
}
parent.last.bracket = thisValue;
parent = parent.last;
} else if (thisTokenIndex === 9 && bracketClosingRegExp.test(thisValue) && parent !== root) {
parent.mirror = thisValue;
parent.source.end = parent.value.pop().source.end;
parent = parent.parent;
}
}
root.source.end = root.value[root.value.length - 1].source.end;
return root;
};
})(
// regex
/(\x2f\x2a[^\x2a]*\x2a+(?:[^\x2f\x2a][^\x2a]*\x2a+)*\x2f)|([\x20\x09\x0a\x0c\x0d]+)|((?:(?:\x22(?:[^\x22\x5c]|\x5c.)*\x22)|(?:\x27(?:[^\x27\x5c]|\x5c.)*\x27)))|(\x23(?:[\x41-\x46\x61-\x66\x30-\x39\x5f\x2d\x80-\xff]|\x5c[^\x0a\x0c\x0d])+)|([\x2b\x2d]?(?:[\x30-\x39]+(?:\x2e[\x30-\x39]*)?|\x2e[\x30-\x39]+)(?:[\x45\x65]\x2d?[\x30-\x39]+)?)|(\x2d\x2d\x3e)|(\x3c\x21\x2d\x2d)|((?:-(?:[\x41-\x5a\x61-\x7a\x80-\xff\x5a]|\x2d|\[^\x0a\x0c\x0d])|[\x41-\x5a\x61-\x7a\x80-\xff\x5a]|\[^\x0a\x0c\x0d])(?:[\x41-\x5a\x61-\x7a\x80-\xff\x5a\x30-\x39\x2d]|\[^\x0a\x0c\x0d])*)|(\x5c[^\x0a\x0c\x0d]|.)/g,
/[\x28\x5b\x7b]/,
/[\x29\x5d\x7d]/
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment