Files
2025-07-08 14:47:27 +02:00

602 lines
15 KiB
JavaScript

import CssParseError from '../CssParseError';
import Position from '../CssPosition';
import { CssTypes, } from '../type';
// http://www.w3.org/TR/CSS21/grammar.html
// https://github.com/visionmedia/css-parse/pull/49#issuecomment-30088027
const commentre = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//g;
export const parse = (css, options) => {
options = options || {};
/**
* Positional.
*/
let lineno = 1;
let column = 1;
/**
* Update lineno and column based on `str`.
*/
function updatePosition(str) {
const lines = str.match(/\n/g);
if (lines)
lineno += lines.length;
const i = str.lastIndexOf('\n');
column = ~i ? str.length - i : column + str.length;
}
/**
* Mark position and patch `node.position`.
*/
function position() {
const start = { line: lineno, column: column };
return function (node) {
node.position = new Position(start, { line: lineno, column: column }, options?.source || '');
whitespace();
return node;
};
}
/**
* Error `msg`.
*/
const errorsList = [];
function error(msg) {
const err = new CssParseError(options?.source || '', msg, lineno, column, css);
if (options?.silent) {
errorsList.push(err);
}
else {
throw err;
}
}
/**
* Parse stylesheet.
*/
function stylesheet() {
const rulesList = rules();
const result = {
type: CssTypes.stylesheet,
stylesheet: {
source: options?.source,
rules: rulesList,
parsingErrors: errorsList,
},
};
return result;
}
/**
* Opening brace.
*/
function open() {
return match(/^{\s*/);
}
/**
* Closing brace.
*/
function close() {
return match(/^}/);
}
/**
* Parse ruleset.
*/
function rules() {
let node;
const rules = [];
whitespace();
comments(rules);
while (css.length && css.charAt(0) !== '}' && (node = atrule() || rule())) {
if (node) {
rules.push(node);
comments(rules);
}
}
return rules;
}
/**
* Match `re` and return captures.
*/
function match(re) {
const m = re.exec(css);
if (!m) {
return;
}
const str = m[0];
updatePosition(str);
css = css.slice(str.length);
return m;
}
/**
* Parse whitespace.
*/
function whitespace() {
match(/^\s*/);
}
/**
* Parse comments;
*/
function comments(rules) {
let c;
rules = rules || [];
while ((c = comment())) {
if (c) {
rules.push(c);
}
}
return rules;
}
/**
* Parse comment.
*/
function comment() {
const pos = position();
if ('/' !== css.charAt(0) || '*' !== css.charAt(1)) {
return;
}
const m = match(/^\/\*[^]*?\*\//);
if (!m) {
return error('End of comment missing');
}
return pos({
type: CssTypes.comment,
comment: m[0].slice(2, -2),
});
}
/**
* Parse selector.
*/
function selector() {
const m = match(/^([^{]+)/);
if (!m) {
return;
}
// remove comment in selector; [^] is equivalent to [.\n\r]
const res = trim(m[0]).replace(/\/\*[^]*?\*\//gm, '');
// Optimisation: If there is no ',' no need to split or post-process (this is less costly)
if (res.indexOf(',') === -1) {
return [res];
}
return (res
/**
* replace ',' by \u200C for data selector (div[data-lang="fr,de,us"])
* replace ',' by \u200C for nthChild and other selector (div:nth-child(2,3,4))
*
* Examples:
* div[data-lang="fr,\"de,us"]
* div[data-lang='fr,\'de,us']
* div:matches(.toto, .titi:matches(.toto, .titi))
*
* Regex logic:
* ("|')(?:\\\1|.)*?\1 => Handle the " and '
* \(.*?\) => Handle the ()
*
* Optimization 1:
* No greedy capture (see docs about the difference between .* and .*?)
*
* Optimization 2:
* ("|')(?:\\\1|.)*?\1 this use reference to capture group, it work faster.
*/
.replace(/("|')(?:\\\1|.)*?\1|\(.*?\)/g, m => m.replace(/,/g, '\u200C'))
// Split the selector by ','
.split(',')
// Replace back \u200C by ','
.map(s => {
return trim(s.replace(/\u200C/g, ','));
}));
}
/**
* Parse declaration.
*/
function declaration() {
const pos = position();
// prop
const propMatch = match(/^(\*?[-#/*\\\w]+(\[[0-9a-z_-]+\])?)\s*/);
if (!propMatch) {
return;
}
const propValue = trim(propMatch[0]);
// :
if (!match(/^:\s*/)) {
return error("property missing ':'");
}
// val
const val = match(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^)]*?\)|[^};])+)/);
const ret = pos({
type: CssTypes.declaration,
property: propValue.replace(commentre, ''),
value: val ? trim(val[0]).replace(commentre, '') : '',
});
// ;
match(/^[;\s]*/);
return ret;
}
/**
* Parse declarations.
*/
function declarations() {
const decls = [];
if (!open()) {
return error("missing '{'");
}
comments(decls);
// declarations
let decl;
while ((decl = declaration())) {
if (decl) {
decls.push(decl);
comments(decls);
}
}
if (!close()) {
return error("missing '}'");
}
return decls;
}
/**
* Parse keyframe.
*/
function keyframe() {
let m;
const vals = [];
const pos = position();
while ((m = match(/^((\d+\.\d+|\.\d+|\d+)%?|[a-z]+)\s*/))) {
vals.push(m[1]);
match(/^,\s*/);
}
if (!vals.length) {
return;
}
return pos({
type: CssTypes.keyframe,
values: vals,
declarations: declarations() || [],
});
}
/**
* Parse keyframes.
*/
function atkeyframes() {
const pos = position();
const m1 = match(/^@([-\w]+)?keyframes\s*/);
if (!m1) {
return;
}
const vendor = m1[1];
// identifier
const m2 = match(/^([-\w]+)\s*/);
if (!m2) {
return error('@keyframes missing name');
}
const name = m2[1];
if (!open()) {
return error("@keyframes missing '{'");
}
let frame;
let frames = comments();
while ((frame = keyframe())) {
frames.push(frame);
frames = frames.concat(comments());
}
if (!close()) {
return error("@keyframes missing '}'");
}
return pos({
type: CssTypes.keyframes,
name: name,
vendor: vendor,
keyframes: frames,
});
}
/**
* Parse supports.
*/
function atsupports() {
const pos = position();
const m = match(/^@supports *([^{]+)/);
if (!m) {
return;
}
const supports = trim(m[1]);
if (!open()) {
return error("@supports missing '{'");
}
const style = comments().concat(rules());
if (!close()) {
return error("@supports missing '}'");
}
return pos({
type: CssTypes.supports,
supports: supports,
rules: style,
});
}
/**
* Parse host.
*/
function athost() {
const pos = position();
const m = match(/^@host\s*/);
if (!m) {
return;
}
if (!open()) {
return error("@host missing '{'");
}
const style = comments().concat(rules());
if (!close()) {
return error("@host missing '}'");
}
return pos({
type: CssTypes.host,
rules: style,
});
}
/**
* Parse container.
*/
function atcontainer() {
const pos = position();
const m = match(/^@container *([^{]+)/);
if (!m) {
return;
}
const container = trim(m[1]);
if (!open()) {
return error("@container missing '{'");
}
const style = comments().concat(rules());
if (!close()) {
return error("@container missing '}'");
}
return pos({
type: CssTypes.container,
container: container,
rules: style,
});
}
/**
* Parse container.
*/
function atlayer() {
const pos = position();
const m = match(/^@layer *([^{;@]+)/);
if (!m) {
return;
}
const layer = trim(m[1]);
if (!open()) {
match(/^[;\s]*/);
return pos({
type: CssTypes.layer,
layer: layer,
});
}
const style = comments().concat(rules());
if (!close()) {
return error("@layer missing '}'");
}
return pos({
type: CssTypes.layer,
layer: layer,
rules: style,
});
}
/**
* Parse media.
*/
function atmedia() {
const pos = position();
const m = match(/^@media *([^{]+)/);
if (!m) {
return;
}
const media = trim(m[1]);
if (!open()) {
return error("@media missing '{'");
}
const style = comments().concat(rules());
if (!close()) {
return error("@media missing '}'");
}
return pos({
type: CssTypes.media,
media: media,
rules: style,
});
}
/**
* Parse custom-media.
*/
function atcustommedia() {
const pos = position();
const m = match(/^@custom-media\s+(--[^\s]+)\s*([^{;]+);/);
if (!m) {
return;
}
return pos({
type: CssTypes.customMedia,
name: trim(m[1]),
media: trim(m[2]),
});
}
/**
* Parse paged media.
*/
function atpage() {
const pos = position();
const m = match(/^@page */);
if (!m) {
return;
}
const sel = selector() || [];
if (!open()) {
return error("@page missing '{'");
}
let decls = comments();
// declarations
let decl;
while ((decl = declaration())) {
decls.push(decl);
decls = decls.concat(comments());
}
if (!close()) {
return error("@page missing '}'");
}
return pos({
type: CssTypes.page,
selectors: sel,
declarations: decls,
});
}
/**
* Parse document.
*/
function atdocument() {
const pos = position();
const m = match(/^@([-\w]+)?document *([^{]+)/);
if (!m) {
return;
}
const vendor = trim(m[1]);
const doc = trim(m[2]);
if (!open()) {
return error("@document missing '{'");
}
const style = comments().concat(rules());
if (!close()) {
return error("@document missing '}'");
}
return pos({
type: CssTypes.document,
document: doc,
vendor: vendor,
rules: style,
});
}
/**
* Parse font-face.
*/
function atfontface() {
const pos = position();
const m = match(/^@font-face\s*/);
if (!m) {
return;
}
if (!open()) {
return error("@font-face missing '{'");
}
let decls = comments();
// declarations
let decl;
while ((decl = declaration())) {
decls.push(decl);
decls = decls.concat(comments());
}
if (!close()) {
return error("@font-face missing '}'");
}
return pos({
type: CssTypes.fontFace,
declarations: decls,
});
}
/**
* Parse import
*/
const atimport = _compileAtrule('import');
/**
* Parse charset
*/
const atcharset = _compileAtrule('charset');
/**
* Parse namespace
*/
const atnamespace = _compileAtrule('namespace');
/**
* Parse non-block at-rules
*/
function _compileAtrule(name) {
const re = new RegExp('^@' +
name +
'\\s*((:?[^;\'"]|"(?:\\\\"|[^"])*?"|\'(?:\\\\\'|[^\'])*?\')+);');
// ^@import\s*([^;"']|("|')(?:\\\2|.)*?\2)+;
return function () {
const pos = position();
const m = match(re);
if (!m) {
return;
}
const ret = { type: name };
ret[name] = m[1].trim();
return pos(ret);
};
}
/**
* Parse at rule.
*/
function atrule() {
if (css[0] !== '@') {
return;
}
return (atkeyframes() ||
atmedia() ||
atcustommedia() ||
atsupports() ||
atimport() ||
atcharset() ||
atnamespace() ||
atdocument() ||
atpage() ||
athost() ||
atfontface() ||
atcontainer() ||
atlayer());
}
/**
* Parse rule.
*/
function rule() {
const pos = position();
const sel = selector();
if (!sel) {
return error('selector missing');
}
comments();
return pos({
type: CssTypes.rule,
selectors: sel,
declarations: declarations() || [],
});
}
return addParent(stylesheet());
};
/**
* Trim `str`.
*/
function trim(str) {
return str ? str.trim() : '';
}
/**
* Adds non-enumerable parent node reference to each node.
*/
function addParent(obj, parent) {
const isNode = obj && typeof obj.type === 'string';
const childParent = isNode ? obj : parent;
for (const k in obj) {
const value = obj[k];
if (Array.isArray(value)) {
value.forEach(v => {
addParent(v, childParent);
});
}
else if (value && typeof value === 'object') {
addParent(value, childParent);
}
}
if (isNode) {
Object.defineProperty(obj, 'parent', {
configurable: true,
writable: true,
enumerable: false,
value: parent || null,
});
}
return obj;
}
export default parse;