443 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
			
		
		
	
	
			443 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
// @ts-check
 | 
						|
/** @typedef {import('./index').Visitor} Visitor */
 | 
						|
 | 
						|
/**
 | 
						|
 * Composes multiple visitor objects into a single one.
 | 
						|
 * @param {Visitor[]} visitors 
 | 
						|
 * @return {Visitor}
 | 
						|
 */
 | 
						|
function composeVisitors(visitors) {
 | 
						|
  if (visitors.length === 1) {
 | 
						|
    return visitors[0];
 | 
						|
  }
 | 
						|
 | 
						|
  /** @type Visitor */
 | 
						|
  let res = {};
 | 
						|
  composeSimpleVisitors(res, visitors, 'StyleSheet');
 | 
						|
  composeSimpleVisitors(res, visitors, 'StyleSheetExit');
 | 
						|
  composeObjectVisitors(res, visitors, 'Rule', ruleVisitor, wrapCustomAndUnknownAtRule);
 | 
						|
  composeObjectVisitors(res, visitors, 'RuleExit', ruleVisitor, wrapCustomAndUnknownAtRule);
 | 
						|
  composeObjectVisitors(res, visitors, 'Declaration', declarationVisitor, wrapCustomProperty);
 | 
						|
  composeObjectVisitors(res, visitors, 'DeclarationExit', declarationVisitor, wrapCustomProperty);
 | 
						|
  composeSimpleVisitors(res, visitors, 'Url');
 | 
						|
  composeSimpleVisitors(res, visitors, 'Color');
 | 
						|
  composeSimpleVisitors(res, visitors, 'Image');
 | 
						|
  composeSimpleVisitors(res, visitors, 'ImageExit');
 | 
						|
  composeSimpleVisitors(res, visitors, 'Length');
 | 
						|
  composeSimpleVisitors(res, visitors, 'Angle');
 | 
						|
  composeSimpleVisitors(res, visitors, 'Ratio');
 | 
						|
  composeSimpleVisitors(res, visitors, 'Resolution');
 | 
						|
  composeSimpleVisitors(res, visitors, 'Time');
 | 
						|
  composeSimpleVisitors(res, visitors, 'CustomIdent');
 | 
						|
  composeSimpleVisitors(res, visitors, 'DashedIdent');
 | 
						|
  composeArrayFunctions(res, visitors, 'MediaQuery');
 | 
						|
  composeArrayFunctions(res, visitors, 'MediaQueryExit');
 | 
						|
  composeSimpleVisitors(res, visitors, 'SupportsCondition');
 | 
						|
  composeSimpleVisitors(res, visitors, 'SupportsConditionExit');
 | 
						|
  composeArrayFunctions(res, visitors, 'Selector');
 | 
						|
  composeTokenVisitors(res, visitors, 'Token', 'token', false);
 | 
						|
  composeTokenVisitors(res, visitors, 'Function', 'function', false);
 | 
						|
  composeTokenVisitors(res, visitors, 'FunctionExit', 'function', true);
 | 
						|
  composeTokenVisitors(res, visitors, 'Variable', 'var', false);
 | 
						|
  composeTokenVisitors(res, visitors, 'VariableExit', 'var', true);
 | 
						|
  composeTokenVisitors(res, visitors, 'EnvironmentVariable', 'env', false);
 | 
						|
  composeTokenVisitors(res, visitors, 'EnvironmentVariableExit', 'env', true);
 | 
						|
  return res;
 | 
						|
}
 | 
						|
 | 
						|
module.exports = composeVisitors;
 | 
						|
 | 
						|
function wrapCustomAndUnknownAtRule(k, f) {
 | 
						|
  if (k === 'unknown') {
 | 
						|
    return (value => f({ type: 'unknown', value }));
 | 
						|
  }
 | 
						|
  if (k === 'custom') {
 | 
						|
    return (value => f({ type: 'custom', value }));
 | 
						|
  }
 | 
						|
  return f;
 | 
						|
}
 | 
						|
 | 
						|
function wrapCustomProperty(k, f) {
 | 
						|
  return k === 'custom' ? (value => f({ property: 'custom', value })) : f;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @param {import('./index').Visitor['Rule']} f 
 | 
						|
 * @param {import('./ast').Rule} item 
 | 
						|
 */
 | 
						|
function ruleVisitor(f, item) {
 | 
						|
  if (typeof f === 'object') {
 | 
						|
    if (item.type === 'unknown') {
 | 
						|
      let v = f.unknown;
 | 
						|
      if (typeof v === 'object') {
 | 
						|
        v = v[item.value.name];
 | 
						|
      }
 | 
						|
      return v?.(item.value);
 | 
						|
    }
 | 
						|
    if (item.type === 'custom') {
 | 
						|
      let v = f.custom;
 | 
						|
      if (typeof v === 'object') {
 | 
						|
        v = v[item.value.name];
 | 
						|
      }
 | 
						|
      return v?.(item.value);
 | 
						|
    }
 | 
						|
    return f[item.type]?.(item);
 | 
						|
  }
 | 
						|
  return f?.(item);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @param {import('./index').Visitor['Declaration']} f 
 | 
						|
 * @param {import('./ast').Declaration} item 
 | 
						|
 */
 | 
						|
function declarationVisitor(f, item) {
 | 
						|
  if (typeof f === 'object') {
 | 
						|
    /** @type {string} */
 | 
						|
    let name = item.property;
 | 
						|
    if (item.property === 'unparsed') {
 | 
						|
      name = item.value.propertyId.property;
 | 
						|
    } else if (item.property === 'custom') {
 | 
						|
      let v = f.custom;
 | 
						|
      if (typeof v === 'object') {
 | 
						|
        v = v[item.value.name];
 | 
						|
      }
 | 
						|
      return v?.(item.value);
 | 
						|
    }
 | 
						|
    return f[name]?.(item);
 | 
						|
  }
 | 
						|
  return f?.(item);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * 
 | 
						|
 * @param {Visitor[]} visitors 
 | 
						|
 * @param {string} key 
 | 
						|
 * @returns {[any[], boolean, Set<string>]}
 | 
						|
 */
 | 
						|
function extractObjectsOrFunctions(visitors, key) {
 | 
						|
  let values = [];
 | 
						|
  let hasFunction = false;
 | 
						|
  let allKeys = new Set();
 | 
						|
  for (let visitor of visitors) {
 | 
						|
    let v = visitor[key];
 | 
						|
    if (v) {
 | 
						|
      if (typeof v === 'function') {
 | 
						|
        hasFunction = true;
 | 
						|
      } else {
 | 
						|
        for (let key in v) {
 | 
						|
          allKeys.add(key);
 | 
						|
        }
 | 
						|
      }
 | 
						|
      values.push(v);
 | 
						|
    }
 | 
						|
  }
 | 
						|
  return [values, hasFunction, allKeys];
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @template {keyof Visitor} K
 | 
						|
 * @param {Visitor} res
 | 
						|
 * @param {Visitor[]} visitors
 | 
						|
 * @param {K} key
 | 
						|
 * @param {(visitor: Visitor[K], item: any) => any | any[] | void} apply 
 | 
						|
 * @param {(k: string, f: any) => any} wrapKey 
 | 
						|
 */
 | 
						|
function composeObjectVisitors(res, visitors, key, apply, wrapKey) {
 | 
						|
  let [values, hasFunction, allKeys] = extractObjectsOrFunctions(visitors, key);
 | 
						|
  if (values.length === 0) {
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  if (values.length === 1) {
 | 
						|
    res[key] = values[0];
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  let f = createArrayVisitor(visitors, (visitor, item) => apply(visitor[key], item));
 | 
						|
  if (hasFunction) {
 | 
						|
    res[key] = f;
 | 
						|
  } else {
 | 
						|
    /** @type {any} */
 | 
						|
    let v = {};
 | 
						|
    for (let k of allKeys) {
 | 
						|
      v[k] = wrapKey(k, f);
 | 
						|
    }
 | 
						|
    res[key] = v;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @param {Visitor} res 
 | 
						|
 * @param {Visitor[]} visitors 
 | 
						|
 * @param {string} key 
 | 
						|
 * @param {import('./ast').TokenOrValue['type']} type 
 | 
						|
 * @param {boolean} isExit 
 | 
						|
 */
 | 
						|
function composeTokenVisitors(res, visitors, key, type, isExit) {
 | 
						|
  let [values, hasFunction, allKeys] = extractObjectsOrFunctions(visitors, key);
 | 
						|
  if (values.length === 0) {
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  if (values.length === 1) {
 | 
						|
    res[key] = values[0];
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  let f = createTokenVisitor(visitors, type, isExit);
 | 
						|
  if (hasFunction) {
 | 
						|
    res[key] = f;
 | 
						|
  } else {
 | 
						|
    let v = {};
 | 
						|
    for (let key of allKeys) {
 | 
						|
      v[key] = f;
 | 
						|
    }
 | 
						|
    res[key] = v;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @param {Visitor[]} visitors 
 | 
						|
 * @param {import('./ast').TokenOrValue['type']} type 
 | 
						|
 */
 | 
						|
function createTokenVisitor(visitors, type, isExit) {
 | 
						|
  let v = createArrayVisitor(visitors, (visitor, /** @type {import('./ast').TokenOrValue} */ item) => {
 | 
						|
    let f;
 | 
						|
    switch (item.type) {
 | 
						|
      case 'token':
 | 
						|
        f = visitor.Token;
 | 
						|
        if (typeof f === 'object') {
 | 
						|
          f = f[item.value.type];
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case 'function':
 | 
						|
        f = isExit ? visitor.FunctionExit : visitor.Function;
 | 
						|
        if (typeof f === 'object') {
 | 
						|
          f = f[item.value.name];
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case 'var':
 | 
						|
        f = isExit ? visitor.VariableExit : visitor.Variable;
 | 
						|
        break;
 | 
						|
      case 'env':
 | 
						|
        f = isExit ? visitor.EnvironmentVariableExit : visitor.EnvironmentVariable;
 | 
						|
        if (typeof f === 'object') {
 | 
						|
          let name;
 | 
						|
          switch (item.value.name.type) {
 | 
						|
            case 'ua':
 | 
						|
            case 'unknown':
 | 
						|
              name = item.value.name.value;
 | 
						|
              break;
 | 
						|
            case 'custom':
 | 
						|
              name = item.value.name.ident;
 | 
						|
              break;
 | 
						|
          }
 | 
						|
          f = f[name];
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case 'color':
 | 
						|
        f = visitor.Color;
 | 
						|
        break;
 | 
						|
      case 'url':
 | 
						|
        f = visitor.Url;
 | 
						|
        break;
 | 
						|
      case 'length':
 | 
						|
        f = visitor.Length;
 | 
						|
        break;
 | 
						|
      case 'angle':
 | 
						|
        f = visitor.Angle;
 | 
						|
        break;
 | 
						|
      case 'time':
 | 
						|
        f = visitor.Time;
 | 
						|
        break;
 | 
						|
      case 'resolution':
 | 
						|
        f = visitor.Resolution;
 | 
						|
        break;
 | 
						|
      case 'dashed-ident':
 | 
						|
        f = visitor.DashedIdent;
 | 
						|
        break;
 | 
						|
    }
 | 
						|
 | 
						|
    if (!f) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let res = f(item.value);
 | 
						|
    switch (item.type) {
 | 
						|
      case 'color':
 | 
						|
      case 'url':
 | 
						|
      case 'length':
 | 
						|
      case 'angle':
 | 
						|
      case 'time':
 | 
						|
      case 'resolution':
 | 
						|
      case 'dashed-ident':
 | 
						|
        if (Array.isArray(res)) {
 | 
						|
          res = res.map(value => ({ type: item.type, value }))
 | 
						|
        } else if (res) {
 | 
						|
          res = { type: item.type, value: res };
 | 
						|
        }
 | 
						|
        break;
 | 
						|
    }
 | 
						|
 | 
						|
    return res;
 | 
						|
  });
 | 
						|
 | 
						|
  return value => v({ type, value });
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @param {Visitor[]} visitors 
 | 
						|
 * @param {string} key 
 | 
						|
 */
 | 
						|
function extractFunctions(visitors, key) {
 | 
						|
  let functions = [];
 | 
						|
  for (let visitor of visitors) {
 | 
						|
    let f = visitor[key];
 | 
						|
    if (f) {
 | 
						|
      functions.push(f);
 | 
						|
    }
 | 
						|
  }
 | 
						|
  return functions;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @param {Visitor} res 
 | 
						|
 * @param {Visitor[]} visitors 
 | 
						|
 * @param {string} key 
 | 
						|
 */
 | 
						|
function composeSimpleVisitors(res, visitors, key) {
 | 
						|
  let functions = extractFunctions(visitors, key);
 | 
						|
  if (functions.length === 0) {
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  if (functions.length === 1) {
 | 
						|
    res[key] = functions[0];
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  res[key] = arg => {
 | 
						|
    let mutated = false;
 | 
						|
    for (let f of functions) {
 | 
						|
      let res = f(arg);
 | 
						|
      if (res) {
 | 
						|
        arg = res;
 | 
						|
        mutated = true;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return mutated ? arg : undefined;
 | 
						|
  };
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @param {Visitor} res 
 | 
						|
 * @param {Visitor[]} visitors 
 | 
						|
 * @param {string} key 
 | 
						|
 */
 | 
						|
function composeArrayFunctions(res, visitors, key) {
 | 
						|
  let functions = extractFunctions(visitors, key);
 | 
						|
  if (functions.length === 0) {
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  if (functions.length === 1) {
 | 
						|
    res[key] = functions[0];
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  res[key] = createArrayVisitor(functions, (f, item) => f(item));
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @template T
 | 
						|
 * @template V
 | 
						|
 * @param {T[]} visitors 
 | 
						|
 * @param {(visitor: T, item: V) => V | V[] | void} apply 
 | 
						|
 * @returns {(item: V) => V | V[] | void}
 | 
						|
 */
 | 
						|
function createArrayVisitor(visitors, apply) {
 | 
						|
  let seen = new Bitset(visitors.length);
 | 
						|
  return arg => {
 | 
						|
    let arr = [arg];
 | 
						|
    let mutated = false;
 | 
						|
    seen.clear();
 | 
						|
    for (let i = 0; i < arr.length; i++) {
 | 
						|
      // For each value, call all visitors. If a visitor returns a new value,
 | 
						|
      // we start over, but skip the visitor that generated the value or saw
 | 
						|
      // it before (to avoid cycles). This way, visitors can be composed in any order. 
 | 
						|
      for (let v = 0; v < visitors.length;) {
 | 
						|
        if (seen.get(v)) {
 | 
						|
          v++;
 | 
						|
          continue;
 | 
						|
        }
 | 
						|
 | 
						|
        let item = arr[i];
 | 
						|
        let visitor = visitors[v];
 | 
						|
        let res = apply(visitor, item);
 | 
						|
        if (Array.isArray(res)) {
 | 
						|
          if (res.length === 0) {
 | 
						|
            arr.splice(i, 1);
 | 
						|
          } else if (res.length === 1) {
 | 
						|
            arr[i] = res[0];
 | 
						|
          } else {
 | 
						|
            arr.splice(i, 1, ...res);
 | 
						|
          }
 | 
						|
          mutated = true;
 | 
						|
          seen.set(v);
 | 
						|
          v = 0;
 | 
						|
        } else if (res) {
 | 
						|
          arr[i] = res;
 | 
						|
          mutated = true;
 | 
						|
          seen.set(v);
 | 
						|
          v = 0;
 | 
						|
        } else {
 | 
						|
          v++;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (!mutated) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    return arr.length === 1 ? arr[0] : arr;
 | 
						|
  };
 | 
						|
}
 | 
						|
 | 
						|
class Bitset {
 | 
						|
  constructor(maxBits = 32) {
 | 
						|
    this.bits = 0;
 | 
						|
    this.more = maxBits > 32 ? new Uint32Array(Math.ceil((maxBits - 32) / 32)) : null;
 | 
						|
  }
 | 
						|
 | 
						|
  /** @param {number} bit */
 | 
						|
  get(bit) {
 | 
						|
    if (bit >= 32 && this.more) {
 | 
						|
      let i = Math.floor((bit - 32) / 32);
 | 
						|
      let b = bit % 32;
 | 
						|
      return Boolean(this.more[i] & (1 << b));
 | 
						|
    } else {
 | 
						|
      return Boolean(this.bits & (1 << bit));
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /** @param {number} bit */
 | 
						|
  set(bit) {
 | 
						|
    if (bit >= 32 && this.more) {
 | 
						|
      let i = Math.floor((bit - 32) / 32);
 | 
						|
      let b = bit % 32;
 | 
						|
      this.more[i] |= 1 << b;
 | 
						|
    } else {
 | 
						|
      this.bits |= 1 << bit;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  clear() {
 | 
						|
    this.bits = 0;
 | 
						|
    if (this.more) {
 | 
						|
      this.more.fill(0);
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 |