Previously we use a regexp to match all the CSS selectors and add our hover class name to it, which has been proved not solid and may be very slow in some situation. Using a production ready css parser can handle this better and also provide ability's to do more accurate things to the recorded stylesheets.
910 lines
18 KiB
TypeScript
910 lines
18 KiB
TypeScript
/**
|
|
* This file is a fork of https://github.com/reworkcss/css/blob/master/lib/parse/index.js
|
|
* I fork it because:
|
|
* 1. The css library was built for node.js which does not have tree-shaking supports.
|
|
* 2. Rewrites into typescript give us a better type interface.
|
|
*/
|
|
|
|
/* tslint:disable no-conditional-assignment interface-name no-shadowed-variable */
|
|
|
|
export interface ParserOptions {
|
|
/** Silently fail on parse errors */
|
|
silent?: boolean;
|
|
/**
|
|
* The path to the file containing css.
|
|
* Makes errors and source maps more helpful, by letting them know where code comes from.
|
|
*/
|
|
source?: string;
|
|
}
|
|
|
|
/**
|
|
* Error thrown during parsing.
|
|
*/
|
|
export interface ParserError {
|
|
/** The full error message with the source position. */
|
|
message?: string;
|
|
/** The error message without position. */
|
|
reason?: string;
|
|
/** The value of options.source if passed to css.parse. Otherwise undefined. */
|
|
filename?: string;
|
|
line?: number;
|
|
column?: number;
|
|
/** The portion of code that couldn't be parsed. */
|
|
source?: string;
|
|
}
|
|
|
|
export interface Loc {
|
|
line?: number;
|
|
column?: number;
|
|
}
|
|
|
|
/**
|
|
* Base AST Tree Node.
|
|
*/
|
|
export interface Node {
|
|
/** The possible values are the ones listed in the Types section on https://github.com/reworkcss/css page. */
|
|
type?: string;
|
|
/** A reference to the parent node, or null if the node has no parent. */
|
|
parent?: Node;
|
|
/** Information about the position in the source string that corresponds to the node. */
|
|
position?: {
|
|
start?: Loc;
|
|
end?: Loc;
|
|
/** The value of options.source if passed to css.parse. Otherwise undefined. */
|
|
source?: string;
|
|
/** The full source string passed to css.parse. */
|
|
content?: string;
|
|
};
|
|
}
|
|
|
|
export interface Rule extends Node {
|
|
/** The list of selectors of the rule, split on commas. Each selector is trimmed from whitespace and comments. */
|
|
selectors?: string[];
|
|
/** Array of nodes with the types declaration and comment. */
|
|
declarations?: Array<Declaration | Comment>;
|
|
}
|
|
|
|
export interface Declaration extends Node {
|
|
/** The property name, trimmed from whitespace and comments. May not be empty. */
|
|
property?: string;
|
|
/** The value of the property, trimmed from whitespace and comments. Empty values are allowed. */
|
|
value?: string;
|
|
}
|
|
|
|
/**
|
|
* A rule-level or declaration-level comment. Comments inside selectors, properties and values etc. are lost.
|
|
*/
|
|
export interface Comment extends Node {
|
|
comment?: string;
|
|
}
|
|
|
|
/**
|
|
* The @charset at-rule.
|
|
*/
|
|
export interface Charset extends Node {
|
|
/** The part following @charset. */
|
|
charset?: string;
|
|
}
|
|
|
|
/**
|
|
* The @custom-media at-rule
|
|
*/
|
|
export interface CustomMedia extends Node {
|
|
/** The ---prefixed name. */
|
|
name?: string;
|
|
/** The part following the name. */
|
|
media?: string;
|
|
}
|
|
|
|
/**
|
|
* The @document at-rule.
|
|
*/
|
|
export interface Document extends Node {
|
|
/** The part following @document. */
|
|
document?: string;
|
|
/** The vendor prefix in @document, or undefined if there is none. */
|
|
vendor?: string;
|
|
/** Array of nodes with the types rule, comment and any of the at-rule types. */
|
|
rules?: Array<Rule | Comment | AtRule>;
|
|
}
|
|
|
|
/**
|
|
* The @font-face at-rule.
|
|
*/
|
|
export interface FontFace extends Node {
|
|
/** Array of nodes with the types declaration and comment. */
|
|
declarations?: Array<Declaration | Comment>;
|
|
}
|
|
|
|
/**
|
|
* The @host at-rule.
|
|
*/
|
|
export interface Host extends Node {
|
|
/** Array of nodes with the types rule, comment and any of the at-rule types. */
|
|
rules?: Array<Rule | Comment | AtRule>;
|
|
}
|
|
|
|
/**
|
|
* The @import at-rule.
|
|
*/
|
|
export interface Import extends Node {
|
|
/** The part following @import. */
|
|
import?: string;
|
|
}
|
|
|
|
/**
|
|
* The @keyframes at-rule.
|
|
*/
|
|
export interface KeyFrames extends Node {
|
|
/** The name of the keyframes rule. */
|
|
name?: string;
|
|
/** The vendor prefix in @keyframes, or undefined if there is none. */
|
|
vendor?: string;
|
|
/** Array of nodes with the types keyframe and comment. */
|
|
keyframes?: Array<KeyFrame | Comment>;
|
|
}
|
|
|
|
export interface KeyFrame extends Node {
|
|
/** The list of "selectors" of the keyframe rule, split on commas. Each “selector” is trimmed from whitespace. */
|
|
values?: string[];
|
|
/** Array of nodes with the types declaration and comment. */
|
|
declarations?: Array<Declaration | Comment>;
|
|
}
|
|
|
|
/**
|
|
* The @media at-rule.
|
|
*/
|
|
export interface Media extends Node {
|
|
/** The part following @media. */
|
|
media?: string;
|
|
/** Array of nodes with the types rule, comment and any of the at-rule types. */
|
|
rules?: Array<Rule | Comment | AtRule>;
|
|
}
|
|
|
|
/**
|
|
* The @namespace at-rule.
|
|
*/
|
|
export interface Namespace extends Node {
|
|
/** The part following @namespace. */
|
|
namespace?: string;
|
|
}
|
|
|
|
/**
|
|
* The @page at-rule.
|
|
*/
|
|
export interface Page extends Node {
|
|
/** The list of selectors of the rule, split on commas. Each selector is trimmed from whitespace and comments. */
|
|
selectors?: string[];
|
|
/** Array of nodes with the types declaration and comment. */
|
|
declarations?: Array<Declaration | Comment>;
|
|
}
|
|
|
|
/**
|
|
* The @supports at-rule.
|
|
*/
|
|
export interface Supports extends Node {
|
|
/** The part following @supports. */
|
|
supports?: string;
|
|
/** Array of nodes with the types rule, comment and any of the at-rule types. */
|
|
rules?: Array<Rule | Comment | AtRule>;
|
|
}
|
|
|
|
/** All at-rules. */
|
|
export type AtRule =
|
|
| Charset
|
|
| CustomMedia
|
|
| Document
|
|
| FontFace
|
|
| Host
|
|
| Import
|
|
| KeyFrames
|
|
| Media
|
|
| Namespace
|
|
| Page
|
|
| Supports;
|
|
|
|
/**
|
|
* A collection of rules
|
|
*/
|
|
export interface StyleRules {
|
|
source?: string;
|
|
/** Array of nodes with the types rule, comment and any of the at-rule types. */
|
|
rules: Array<Rule | Comment | AtRule>;
|
|
/** Array of Errors. Errors collected during parsing when option silent is true. */
|
|
parsingErrors?: ParserError[];
|
|
}
|
|
|
|
/**
|
|
* The root node returned by css.parse.
|
|
*/
|
|
export interface Stylesheet extends Node {
|
|
stylesheet?: StyleRules;
|
|
}
|
|
|
|
// http://www.w3.org/TR/CSS21/grammar.html
|
|
// https://github.com/visionmedia/css-parse/pull/49#issuecomment-30088027
|
|
const commentre = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//g;
|
|
|
|
export function parse(css: string, options: ParserOptions = {}) {
|
|
/**
|
|
* Positional.
|
|
*/
|
|
|
|
let lineno = 1;
|
|
let column = 1;
|
|
|
|
/**
|
|
* Update lineno and column based on `str`.
|
|
*/
|
|
|
|
function updatePosition(str: string) {
|
|
const lines = str.match(/\n/g);
|
|
if (lines) {
|
|
lineno += lines.length;
|
|
}
|
|
let i = str.lastIndexOf('\n');
|
|
column = i === -1 ? column + str.length : str.length - i;
|
|
}
|
|
|
|
/**
|
|
* Mark position and patch `node.position`.
|
|
*/
|
|
|
|
function position() {
|
|
const start = { line: lineno, column };
|
|
return (
|
|
node: Rule | Declaration | Comment | AtRule | Stylesheet | KeyFrame,
|
|
) => {
|
|
node.position = new Position(start);
|
|
whitespace();
|
|
return node;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Store position information for a node
|
|
*/
|
|
|
|
class Position {
|
|
public content!: string;
|
|
public start!: Loc;
|
|
public end!: Loc;
|
|
public source?: string;
|
|
|
|
constructor(start: Loc) {
|
|
this.start = start;
|
|
this.end = { line: lineno, column };
|
|
this.source = options.source;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Non-enumerable source string
|
|
*/
|
|
|
|
Position.prototype.content = css;
|
|
|
|
const errorsList: ParserError[] = [];
|
|
|
|
function error(msg: string) {
|
|
const err = new Error(
|
|
options.source + ':' + lineno + ':' + column + ': ' + msg,
|
|
) as ParserError;
|
|
err.reason = msg;
|
|
err.filename = options.source;
|
|
err.line = lineno;
|
|
err.column = column;
|
|
err.source = css;
|
|
|
|
if (options.silent) {
|
|
errorsList.push(err);
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse stylesheet.
|
|
*/
|
|
|
|
function stylesheet(): Stylesheet {
|
|
const rulesList = rules();
|
|
|
|
return {
|
|
type: 'stylesheet',
|
|
stylesheet: {
|
|
source: options.source,
|
|
rules: rulesList,
|
|
parsingErrors: errorsList,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Opening brace.
|
|
*/
|
|
|
|
function open() {
|
|
return match(/^{\s*/);
|
|
}
|
|
|
|
/**
|
|
* Closing brace.
|
|
*/
|
|
|
|
function close() {
|
|
return match(/^}/);
|
|
}
|
|
|
|
/**
|
|
* Parse ruleset.
|
|
*/
|
|
|
|
function rules() {
|
|
let node: Rule | void;
|
|
const rules: Rule[] = [];
|
|
whitespace();
|
|
comments(rules);
|
|
while (css.length && css.charAt(0) !== '}' && (node = atrule() || rule())) {
|
|
if (node !== false) {
|
|
rules.push(node);
|
|
comments(rules);
|
|
}
|
|
}
|
|
return rules;
|
|
}
|
|
|
|
/**
|
|
* Match `re` and return captures.
|
|
*/
|
|
|
|
function match(re: RegExp) {
|
|
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: Rule[] = []) {
|
|
let c: Comment | void;
|
|
while ((c = comment())) {
|
|
if (c !== false) {
|
|
rules.push(c);
|
|
}
|
|
c = comment();
|
|
}
|
|
return rules;
|
|
}
|
|
|
|
/**
|
|
* Parse comment.
|
|
*/
|
|
|
|
function comment() {
|
|
const pos = position();
|
|
if ('/' !== css.charAt(0) || '*' !== css.charAt(1)) {
|
|
return;
|
|
}
|
|
|
|
let i = 2;
|
|
while (
|
|
'' !== css.charAt(i) &&
|
|
('*' !== css.charAt(i) || '/' !== css.charAt(i + 1))
|
|
) {
|
|
++i;
|
|
}
|
|
i += 2;
|
|
|
|
if ('' === css.charAt(i - 1)) {
|
|
return error('End of comment missing');
|
|
}
|
|
|
|
const str = css.slice(2, i - 2);
|
|
column += 2;
|
|
updatePosition(str);
|
|
css = css.slice(i);
|
|
column += 2;
|
|
|
|
return pos({
|
|
type: 'comment',
|
|
comment: str,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Parse selector.
|
|
*/
|
|
|
|
function selector() {
|
|
const m = match(/^([^{]+)/);
|
|
if (!m) {
|
|
return;
|
|
}
|
|
/* @fix Remove all comments from selectors
|
|
* http://ostermiller.org/findcomment.html */
|
|
return trim(m[0])
|
|
.replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/g, '')
|
|
.replace(/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'/g, m => {
|
|
return m.replace(/,/g, '\u200C');
|
|
})
|
|
.split(/\s*(?![^(]*\)),\s*/)
|
|
.map(s => {
|
|
return s.replace(/\u200C/g, ',');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Parse declaration.
|
|
*/
|
|
|
|
function declaration(): Declaration | void | never {
|
|
const pos = position();
|
|
|
|
// prop
|
|
let propMatch = match(/^(\*?[-#\/\*\\\w]+(\[[0-9a-z_-]+\])?)\s*/);
|
|
if (!propMatch) {
|
|
return;
|
|
}
|
|
const prop = trim(propMatch[0]);
|
|
|
|
// :
|
|
if (!match(/^:\s*/)) {
|
|
return error(`property missing ':'`);
|
|
}
|
|
|
|
// val
|
|
const val = match(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+)/);
|
|
|
|
const ret = pos({
|
|
type: 'declaration',
|
|
property: prop.replace(commentre, ''),
|
|
value: val ? trim(val[0]).replace(commentre, '') : '',
|
|
});
|
|
|
|
// ;
|
|
match(/^[;\s]*/);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Parse declarations.
|
|
*/
|
|
|
|
function declarations() {
|
|
const decls: Array<object> = [];
|
|
|
|
if (!open()) {
|
|
return error(`missing '{'`);
|
|
}
|
|
comments(decls);
|
|
|
|
// declarations
|
|
let decl;
|
|
while ((decl = declaration())) {
|
|
if ((decl as unknown) !== false) {
|
|
decls.push(decl);
|
|
comments(decls);
|
|
}
|
|
decl = declaration();
|
|
}
|
|
|
|
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: 'keyframe',
|
|
values: vals,
|
|
declarations: declarations() as Declaration[],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Parse keyframes.
|
|
*/
|
|
|
|
function atkeyframes() {
|
|
const pos = position();
|
|
let m = match(/^@([-\w]+)?keyframes\s*/);
|
|
|
|
if (!m) {
|
|
return;
|
|
}
|
|
const vendor = m[1];
|
|
|
|
// identifier
|
|
m = match(/^([-\w]+)\s*/);
|
|
if (!m) {
|
|
return error('@keyframes missing name');
|
|
}
|
|
const name = m[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: 'keyframes',
|
|
name,
|
|
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: '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: 'host',
|
|
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: '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: 'custom-media',
|
|
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: '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: 'document',
|
|
document: doc,
|
|
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: 'font-face',
|
|
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: string) {
|
|
const re = new RegExp('^@' + name + '\\s*([^;]+);');
|
|
return () => {
|
|
const pos = position();
|
|
const m = match(re);
|
|
if (!m) {
|
|
return;
|
|
}
|
|
const ret: Record<string, string> = { 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()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Parse rule.
|
|
*/
|
|
|
|
function rule() {
|
|
const pos = position();
|
|
const sel = selector();
|
|
|
|
if (!sel) {
|
|
return error('selector missing');
|
|
}
|
|
comments();
|
|
|
|
return pos({
|
|
type: 'rule',
|
|
selectors: sel,
|
|
declarations: declarations() as Declaration[],
|
|
});
|
|
}
|
|
|
|
return addParent(stylesheet());
|
|
}
|
|
|
|
/**
|
|
* Trim `str`.
|
|
*/
|
|
|
|
function trim(str: string) {
|
|
return str ? str.replace(/^\s+|\s+$/g, '') : '';
|
|
}
|
|
|
|
/**
|
|
* Adds non-enumerable parent node reference to each node.
|
|
*/
|
|
|
|
function addParent(obj: Stylesheet, parent?: Stylesheet) {
|
|
const isNode = obj && typeof obj.type === 'string';
|
|
const childParent = isNode ? obj : parent;
|
|
|
|
for (const k of Object.keys(obj)) {
|
|
const value = obj[k as keyof Stylesheet];
|
|
if (Array.isArray(value)) {
|
|
value.forEach(v => {
|
|
addParent(v, childParent);
|
|
});
|
|
} else if (value && typeof value === 'object') {
|
|
addParent((value as unknown) as Stylesheet, childParent);
|
|
}
|
|
}
|
|
|
|
if (isNode) {
|
|
Object.defineProperty(obj, 'parent', {
|
|
configurable: true,
|
|
writable: true,
|
|
enumerable: false,
|
|
value: parent || null,
|
|
});
|
|
}
|
|
|
|
return obj;
|
|
}
|