Initial commit

This commit is contained in:
mats committed 2026-10-19 16:04:17 +02:00
commit afed023152
8152 files changed
+2086461

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 asamuzaK (Kazz)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+128
View File
@@ -0,0 +1,128 @@
# CSS color
[![build](https://github.com/asamuzaK/cssColor/actions/workflows/node.js.yml/badge.svg)](https://github.com/asamuzaK/cssColor/actions/workflows/node.js.yml)
[![CodeQL](https://github.com/asamuzaK/cssColor/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/asamuzaK/cssColor/actions/workflows/github-code-scanning/codeql)
[![npm (scoped)](https://img.shields.io/npm/v/@asamuzakjp/css-color)](https://www.npmjs.com/package/@asamuzakjp/css-color)
A robust and modern library to resolve, parse, and convert CSS colors.
Supports the latest CSS Color Module Level 4 & 5 specifications.
## Features
- **Modern CSS Color Support:** Accurately resolves `color-mix()`, `color()`, modern color spaces (`oklch`, `oklab`, `lch`, `lab`, `hwb`, etc.), and relative colors (`lab(from red l a b)`, `color(from red xyz-d50 x y z)`).
- **Deep Resolution:** Deeply resolves `var()` and `calc()` functions embedded within color values.
- **Gradient Parsing:** Supports parsing and validation for `linear-gradient`, `radial-gradient`, and `conic-gradient`.
- **Comprehensive Color Conversion:** Highly accurate converters between HEX, HSL, HWB, LAB, LCH, Oklab, Oklch, RGB, and XYZ color spaces.
- **Bonus Utilities:** Includes convenient functions to validate colors or gradients, extract CSS variables, and safely split CSS values.
- **Used in jsdom:** Adopted as the CSS color parser and resolver for `jsdom`.
- **Pure ESM with TypeScript Ready:** Native ESM (`type: "module"`) with comprehensive TypeScript definitions.
## Install
```console
npm i @asamuzakjp/css-color
```
## Quick Start
```javascript
import { convert, resolve, utils } from '@asamuzakjp/css-color';
```
### Samples
1. Resolve complex modern CSS colors:
```javascript
const resolvedMix = resolve(
'color-mix(in oklab, lch(67.5345 42.5 258.2), color(srgb 0 0.5 0))'
);
// => 'oklab(0.620754 -0.0931934 -0.00374881)'
```
2. Resolve with Custom Properties and calc():
```javascript
const resolvedVar = resolve('hsl(calc(var(--base-hue) * 3) 100% 50% / .5)', {
customProperty: { '--base-hue': '210deg' }
});
// => 'rgba(128, 0, 255, 0.5)'
```
3. Convert between color spaces:
```javascript
const hex = convert.colorToHex('lab(46.2775% -47.5621 48.5837)');
// => '#008000'
```
4. Validate colors and gradients:
```javascript
const isColor = utils.isColor('light-dark(red, blue)');
// => true
const isGradient = utils.isGradient(
'conic-gradient(from 0.5turn at 50% 50%, red, blue)'
);
// => true
```
## API Reference
### `resolve(color, opt?)`
Resolves a CSS color string into its computed or specified value. System colors are not supported.
- **`color`** `<string>`: The CSS color value to resolve.
- **`opt`** `<object>` _(optional)_:
- `opt.currentColor`: Color to use for the `currentcolor` keyword.
- `opt.customProperty`: Object containing `--` prefixed keys and their values, or a `callback(propertyName)` function to dynamically resolve CSS variables.
- `opt.dimension`: Object mapping units (e.g., `em`, `rem`, `vw`) to pixel numbers, or a `callback(unit)` function for dynamic length resolution.
- `opt.format`: Output format. Options: `computedValue` (default), `specifiedValue`, `hex`, `hexAlpha`.
- `opt.colorScheme`: `normal` (default), `light`, or `dark` (useful for `light-dark()` resolution).
### `convert`
A collection of color conversion utilities.
<!-- prettier-ignore -->
| Function | Returns | Options | Description |
| :-------------- | :-------------- | :-------------- | :-------------- |
| `convert.colorToHex(value, opt?)` | `string \| null` | `opt.alpha` `<boolean>`<br>_(+ see `resolve` options)_ | Returns `#rrggbb` or `#rrggbbaa` (if `opt.alpha` is true). |
| `convert.colorToHsl(value, opt?)` | `number[]` | _See `resolve` options_ | Converts to HSL channels: `[h, s, l, alpha]` |
| `convert.colorToHwb(value, opt?)` | `number[]` | _See `resolve` options_ | Converts to HWB channels: `[h, w, b, alpha]` |
| `convert.colorToLab(value, opt?)` | `number[]` | _See `resolve` options_ | Converts to CIE LAB channels: `[l, a, b, alpha]` |
| `convert.colorToLch(value, opt?)` | `number[]` | _See `resolve` options_ | Converts to CIE LCH channels: `[l, c, h, alpha]` |
| `convert.colorToOklab(value, opt?)` | `number[]` | _See `resolve` options_ | Converts to Oklab channels: `[l, a, b, alpha]` |
| `convert.colorToOklch(value, opt?)` | `number[]` | _See `resolve` options_ | Converts to Oklch channels: `[l, c, h, alpha]` |
| `convert.colorToRgb(value, opt?)` | `number[]` | _See `resolve` options_ | Converts to sRGB channels: `[r, g, b, alpha]` |
| `convert.colorToXyz(value, opt?)` | `number[]` | _See `resolve` options_ | Converts to CIE XYZ channels (defaults to D65): `[x, y, z, alpha]` |
| `convert.colorToXyzD50(value, opt?)` | `number[]` | _See `resolve` options_ | Converts to CIE XYZ channels with D50 white point. |
### `utils`
Helpful internal tools exposed for advanced usage, parsing, and validation.
<!-- prettier-ignore -->
| Function | Returns | Options (`opt`) | Description |
| :-------------- | :-------------- | :-------------- | :-------------- |
| `utils.cssCalc(value, opt?)` | `string` | `opt.dimension`<br>_(+ see `resolve` options)_ | Resolves CSS `calc()` expressions. |
| `utils.cssVar(value, opt?)` | `string` | `opt.customProperty`<br>_(+ see `resolve` options)_ | Resolves CSS `var()` expressions. |
| `utils.extractDashedIdent(value)` | `string[]` | _None_ | Extracts custom property names (dashed-ident tokens) from a value. |
| `utils.isColor(value, opt?)` | `boolean` | _See `resolve` options_ | Returns `true` if the string is a valid CSS color. |
| `utils.isGradient(value, opt?)` | `boolean` | _See `resolve` options_ | Returns `true` if the string is a valid CSS gradient. |
| `utils.resolveGradient(value, opt?)` | `string` | _See `resolve` options_ | Resolves CSS gradient strings. |
| `utils.resolveLengthInPixels(value, unit, opt?)` | `number` | `opt.dimension`<br>_(+ see `resolve` options)_ | Converts an absolute or relative CSS length to pixels. |
| `utils.splitValue(value, opt?)` | `string[]` | `opt.delimiter` `<string>`<br>`opt.preserveComment` `<boolean>` | Safely splits a CSS value by a specified delimiter (` `, `,`, `/`). |
## Acknowledgments
The following resources have been of great help in the development of this library:
- [csstools/postcss-plugins](https://github.com/csstools/postcss-plugins)
- [lru-cache](https://github.com/isaacs/node-lru-cache)
---
Copyright (c) 2024 [asamuzaK (Kazz)](https://github.com/asamuzaK/)
+18
View File
@@ -0,0 +1,18 @@
/*!
* CSS color - Resolve, parse, convert CSS color.
* @license MIT
* @copyright asamuzaK (Kazz)
* @see {@link https://github.com/asamuzaK/cssColor/blob/main/LICENSE}
*/
export { convert } from './js/convert.js';
export { resolve } from './js/resolve.js';
export declare const utils: {
cssCalc: (value: string, opt?: import('./js/typedef.js').Options) => string;
cssVar: (value: string, opt?: import('./js/typedef.js').Options) => string;
extractDashedIdent: (value: string) => string[];
isColor: (value: unknown, opt?: import('./js/typedef.js').Options) => boolean;
isGradient: (value: string, opt?: import('./js/typedef.js').Options) => boolean;
resolveGradient: (value: string, opt?: import('./js/typedef.js').Options) => string;
resolveLengthInPixels: (value: number | string, unit: string | undefined, opt?: import('./js/typedef.js').Options) => number;
splitValue: (value: string, opt?: import('./js/typedef.js').Options) => string[];
};
+26
View File
@@ -0,0 +1,26 @@
import { extractDashedIdent, resolveLengthInPixels, splitValue } from "./js/util.js";
import { isColor, resolve } from "./js/resolve.js";
import { cssCalc, cssVar } from "./js/css-calc-var.js";
import { isGradient, resolveGradient } from "./js/css-gradient.js";
import { convert } from "./js/convert.js";
//#region src/index.ts
/*!
* CSS color - Resolve, parse, convert CSS color.
* @license MIT
* @copyright asamuzaK (Kazz)
* @see {@link https://github.com/asamuzaK/cssColor/blob/main/LICENSE}
*/
var utils = {
cssCalc,
cssVar,
extractDashedIdent,
isColor,
isGradient,
resolveGradient,
resolveLengthInPixels,
splitValue
};
//#endregion
export { convert, resolve, utils };
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","names":[],"sources":["../../src/index.ts"],"sourcesContent":["/*!\n * CSS color - Resolve, parse, convert CSS color.\n * @license MIT\n * @copyright asamuzaK (Kazz)\n * @see {@link https://github.com/asamuzaK/cssColor/blob/main/LICENSE}\n */\n\nimport { cssCalc, cssVar } from './js/css-calc-var';\nimport { isGradient, resolveGradient } from './js/css-gradient';\nimport { isColor } from './js/resolve';\nimport {\n extractDashedIdent,\n resolveLengthInPixels,\n splitValue\n} from './js/util';\n\nexport { convert } from './js/convert';\nexport { resolve } from './js/resolve';\n/* utils */\nexport const utils = {\n cssCalc,\n cssVar,\n extractDashedIdent,\n isColor,\n isGradient,\n resolveGradient,\n resolveLengthInPixels,\n splitValue\n};\n"],"mappings":";;;;;;;;;;;;AAmBA,IAAa,QAAQ;CACnB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF"}
+31
View File
@@ -0,0 +1,31 @@
import { LRUCache } from 'lru-cache';
import { Options } from './typedef.js';
/**
* CacheItem
*/
export declare class CacheItem {
#private;
constructor(item: unknown);
get item(): unknown;
}
export declare const lruCache: LRUCache<string, CacheItem, unknown>;
/**
* set cache
* @param key - cache key
* @param value - value to cache
* @returns void
*/
export declare const setCache: (key: string, value: unknown) => void;
/**
* get cache
* @param key - cache key
* @returns cached item or false otherwise
*/
export declare const getCache: (key: string) => CacheItem | false;
/**
* create cache key
* @param keyData - key data
* @param [opt] - options
* @returns cache key
*/
export declare const createCacheKey: (keyData: Record<string, string>, opt?: Options) => string;
+72
View File
@@ -0,0 +1,72 @@
import { LRUCache } from "lru-cache";
//#region src/js/cache.ts
/**
* cache
*/
var CACHE_SIZE = 4096;
/**
* CacheItem
*/
var CacheItem = class {
#item;
constructor(item) {
this.#item = item;
}
get item() {
return this.#item;
}
};
var lruCache = new LRUCache({ max: CACHE_SIZE });
/**
* set cache
* @param key - cache key
* @param value - value to cache
* @returns void
*/
var setCache = (key, value) => {
if (!key) return;
if (value instanceof CacheItem) lruCache.set(key, value);
else lruCache.set(key, new CacheItem(value));
};
/**
* get cache
* @param key - cache key
* @returns cached item or false otherwise
*/
var getCache = (key) => {
if (!key) return false;
const item = lruCache.get(key);
if (item !== void 0) return item;
return false;
};
/**
* helper function to sort object keys alphabetically
* @param obj - Object
* @returns stringified JSON
*/
var stringifySorted = (obj) => {
const keys = Object.keys(obj);
if (keys.length === 0) return "";
keys.sort();
let result = "";
for (const key of keys) result += `${key}:${JSON.stringify(obj[key])};`;
return result;
};
/**
* create cache key
* @param keyData - key data
* @param [opt] - options
* @returns cache key
*/
var createCacheKey = (keyData, opt = {}) => {
if (!keyData || opt.customProperty && typeof opt.customProperty.callback === "function" || opt.dimension && typeof opt.dimension.callback === "function") return "";
const namespace = keyData.namespace || "";
const name = keyData.name || "";
const value = keyData.value || "";
if (!namespace && !name && !value) return "";
return `${`${namespace}:${name}:${value}`}::${`${opt.format || ""}|${opt.colorSpace || ""}|${opt.colorScheme || ""}|${opt.currentColor || ""}|${opt.d50 ? "1" : "0"}|${opt.nullable ? "1" : "0"}|${opt.preserveComment ? "1" : "0"}|${opt.delimiter || ""}`}::${opt.customProperty ? stringifySorted(opt.customProperty) : ""}::${opt.dimension ? stringifySorted(opt.dimension) : ""}`;
};
//#endregion
export { CacheItem, createCacheKey, getCache, lruCache, setCache };
//# sourceMappingURL=cache.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"cache.js","names":["#item"],"sources":["../../../src/js/cache.ts"],"sourcesContent":["/**\n * cache\n */\n\nimport { LRUCache } from 'lru-cache';\nimport { Options } from './typedef';\n\n/* constants */\nconst CACHE_SIZE = 4096;\n\n/**\n * CacheItem\n */\nexport class CacheItem {\n /* private */\n #item: unknown;\n\n constructor(item: unknown) {\n this.#item = item;\n }\n\n get item() {\n return this.#item;\n }\n}\n\n/*\n * lru cache instance\n */\nexport const lruCache = new LRUCache<string, CacheItem>({\n max: CACHE_SIZE\n});\n\n/**\n * set cache\n * @param key - cache key\n * @param value - value to cache\n * @returns void\n */\nexport const setCache = (key: string, value: unknown): void => {\n if (!key) {\n return;\n }\n if (value instanceof CacheItem) {\n lruCache.set(key, value);\n } else {\n lruCache.set(key, new CacheItem(value));\n }\n};\n\n/**\n * get cache\n * @param key - cache key\n * @returns cached item or false otherwise\n */\nexport const getCache = (key: string): CacheItem | false => {\n if (!key) {\n return false;\n }\n const item = lruCache.get(key);\n if (item !== undefined) {\n return item as CacheItem;\n }\n return false;\n};\n\n/**\n * helper function to sort object keys alphabetically\n * @param obj - Object\n * @returns stringified JSON\n */\nconst stringifySorted = (obj: Record<string, unknown>): string => {\n const keys = Object.keys(obj);\n if (keys.length === 0) {\n return '';\n }\n keys.sort();\n let result = '';\n for (const key of keys) {\n result += `${key}:${JSON.stringify(obj[key])};`;\n }\n return result;\n};\n\n/**\n * create cache key\n * @param keyData - key data\n * @param [opt] - options\n * @returns cache key\n */\nexport const createCacheKey = (\n keyData: Record<string, string>,\n opt: Options = {}\n): string => {\n if (\n !keyData ||\n (opt.customProperty && typeof opt.customProperty.callback === 'function') ||\n (opt.dimension && typeof opt.dimension.callback === 'function')\n ) {\n return '';\n }\n const namespace = keyData.namespace || '';\n const name = keyData.name || '';\n const value = keyData.value || '';\n if (!namespace && !name && !value) {\n return '';\n }\n const baseKey = `${namespace}:${name}:${value}`;\n const optStr = `${opt.format || ''}|${opt.colorSpace || ''}|${opt.colorScheme || ''}|${opt.currentColor || ''}|${opt.d50 ? '1' : '0'}|${opt.nullable ? '1' : '0'}|${opt.preserveComment ? '1' : '0'}|${opt.delimiter || ''}`;\n const customPropStr = opt.customProperty\n ? stringifySorted(opt.customProperty as Record<string, unknown>)\n : '';\n const dimStr = opt.dimension\n ? stringifySorted(opt.dimension as Record<string, unknown>)\n : '';\n return `${baseKey}::${optStr}::${customPropStr}::${dimStr}`;\n};\n"],"mappings":";;;;;AAQA,IAAM,aAAa;;;;AAKnB,IAAa,YAAb,MAAuB;CAErB;CAEA,YAAY,MAAe;EACzB,KAAKA,QAAQ;CACf;CAEA,IAAI,OAAO;EACT,OAAO,KAAKA;CACd;AACF;AAKA,IAAa,WAAW,IAAI,SAA4B,EACtD,KAAK,WACP,CAAC;;;;;;;AAQD,IAAa,YAAY,KAAa,UAAyB;CAC7D,IAAI,CAAC,KACH;CAEF,IAAI,iBAAiB,WACnB,SAAS,IAAI,KAAK,KAAK;MAEvB,SAAS,IAAI,KAAK,IAAI,UAAU,KAAK,CAAC;AAE1C;;;;;;AAOA,IAAa,YAAY,QAAmC;CAC1D,IAAI,CAAC,KACH,OAAO;CAET,MAAM,OAAO,SAAS,IAAI,GAAG;CAC7B,IAAI,SAAS,KAAA,GACX,OAAO;CAET,OAAO;AACT;;;;;;AAOA,IAAM,mBAAmB,QAAyC;CAChE,MAAM,OAAO,OAAO,KAAK,GAAG;CAC5B,IAAI,KAAK,WAAW,GAClB,OAAO;CAET,KAAK,KAAK;CACV,IAAI,SAAS;CACb,KAAK,MAAM,OAAO,MAChB,UAAU,GAAG,IAAI,GAAG,KAAK,UAAU,IAAI,IAAI,EAAE;CAE/C,OAAO;AACT;;;;;;;AAQA,IAAa,kBACX,SACA,MAAe,CAAC,MACL;CACX,IACE,CAAC,WACA,IAAI,kBAAkB,OAAO,IAAI,eAAe,aAAa,cAC7D,IAAI,aAAa,OAAO,IAAI,UAAU,aAAa,YAEpD,OAAO;CAET,MAAM,YAAY,QAAQ,aAAa;CACvC,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,QAAQ,QAAQ,SAAS;CAC/B,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,OAC1B,OAAO;CAUT,OAAO,GAAG,GARS,UAAU,GAAG,KAAK,GAAG,QAQtB,IAAI,GAPJ,IAAI,UAAU,GAAG,GAAG,IAAI,cAAc,GAAG,GAAG,IAAI,eAAe,GAAG,GAAG,IAAI,gBAAgB,GAAG,GAAG,IAAI,MAAM,MAAM,IAAI,GAAG,IAAI,WAAW,MAAM,IAAI,GAAG,IAAI,kBAAkB,MAAM,IAAI,GAAG,IAAI,aAAa,KAO3L,IANP,IAAI,iBACtB,gBAAgB,IAAI,cAAyC,IAC7D,GAI2C,IAHhC,IAAI,YACf,gBAAgB,IAAI,SAAoC,IACxD;AAEN"}
+532
View File
@@ -0,0 +1,532 @@
import { ColorChannels, Options, SpecifiedColorChannels } from './typedef.js';
/**
* @type TriColorChannels - color channels without alpha
*/
type TriColorChannels = [x: number, y: number, z: number];
/**
* @type ColorMatrix - color matrix
*/
type ColorMatrix = [
r1: TriColorChannels,
r2: TriColorChannels,
r3: TriColorChannels
];
/**
* named colors
*/
export declare const NAMED_COLORS: {
readonly aliceblue: [240, 248, 255];
readonly antiquewhite: [250, 235, 215];
readonly aqua: [0, 255, 255];
readonly aquamarine: [127, 255, 212];
readonly azure: [240, 255, 255];
readonly beige: [245, 245, 220];
readonly bisque: [255, 228, 196];
readonly black: [0, 0, 0];
readonly blanchedalmond: [255, 235, 205];
readonly blue: [0, 0, 255];
readonly blueviolet: [138, 43, 226];
readonly brown: [165, 42, 42];
readonly burlywood: [222, 184, 135];
readonly cadetblue: [95, 158, 160];
readonly chartreuse: [127, 255, 0];
readonly chocolate: [210, 105, 30];
readonly coral: [255, 127, 80];
readonly cornflowerblue: [100, 149, 237];
readonly cornsilk: [255, 248, 220];
readonly crimson: [220, 20, 60];
readonly cyan: [0, 255, 255];
readonly darkblue: [0, 0, 139];
readonly darkcyan: [0, 139, 139];
readonly darkgoldenrod: [184, 134, 11];
readonly darkgray: [169, 169, 169];
readonly darkgreen: [0, 100, 0];
readonly darkgrey: [169, 169, 169];
readonly darkkhaki: [189, 183, 107];
readonly darkmagenta: [139, 0, 139];
readonly darkolivegreen: [85, 107, 47];
readonly darkorange: [255, 140, 0];
readonly darkorchid: [153, 50, 204];
readonly darkred: [139, 0, 0];
readonly darksalmon: [233, 150, 122];
readonly darkseagreen: [143, 188, 143];
readonly darkslateblue: [72, 61, 139];
readonly darkslategray: [47, 79, 79];
readonly darkslategrey: [47, 79, 79];
readonly darkturquoise: [0, 206, 209];
readonly darkviolet: [148, 0, 211];
readonly deeppink: [255, 20, 147];
readonly deepskyblue: [0, 191, 255];
readonly dimgray: [105, 105, 105];
readonly dimgrey: [105, 105, 105];
readonly dodgerblue: [30, 144, 255];
readonly firebrick: [178, 34, 34];
readonly floralwhite: [255, 250, 240];
readonly forestgreen: [34, 139, 34];
readonly fuchsia: [255, 0, 255];
readonly gainsboro: [220, 220, 220];
readonly ghostwhite: [248, 248, 255];
readonly gold: [255, 215, 0];
readonly goldenrod: [218, 165, 32];
readonly gray: [128, 128, 128];
readonly green: [0, 128, 0];
readonly greenyellow: [173, 255, 47];
readonly grey: [128, 128, 128];
readonly honeydew: [240, 255, 240];
readonly hotpink: [255, 105, 180];
readonly indianred: [205, 92, 92];
readonly indigo: [75, 0, 130];
readonly ivory: [255, 255, 240];
readonly khaki: [240, 230, 140];
readonly lavender: [230, 230, 250];
readonly lavenderblush: [255, 240, 245];
readonly lawngreen: [124, 252, 0];
readonly lemonchiffon: [255, 250, 205];
readonly lightblue: [173, 216, 230];
readonly lightcoral: [240, 128, 128];
readonly lightcyan: [224, 255, 255];
readonly lightgoldenrodyellow: [250, 250, 210];
readonly lightgray: [211, 211, 211];
readonly lightgreen: [144, 238, 144];
readonly lightgrey: [211, 211, 211];
readonly lightpink: [255, 182, 193];
readonly lightsalmon: [255, 160, 122];
readonly lightseagreen: [32, 178, 170];
readonly lightskyblue: [135, 206, 250];
readonly lightslategray: [119, 136, 153];
readonly lightslategrey: [119, 136, 153];
readonly lightsteelblue: [176, 196, 222];
readonly lightyellow: [255, 255, 224];
readonly lime: [0, 255, 0];
readonly limegreen: [50, 205, 50];
readonly linen: [250, 240, 230];
readonly magenta: [255, 0, 255];
readonly maroon: [128, 0, 0];
readonly mediumaquamarine: [102, 205, 170];
readonly mediumblue: [0, 0, 205];
readonly mediumorchid: [186, 85, 211];
readonly mediumpurple: [147, 112, 219];
readonly mediumseagreen: [60, 179, 113];
readonly mediumslateblue: [123, 104, 238];
readonly mediumspringgreen: [0, 250, 154];
readonly mediumturquoise: [72, 209, 204];
readonly mediumvioletred: [199, 21, 133];
readonly midnightblue: [25, 25, 112];
readonly mintcream: [245, 255, 250];
readonly mistyrose: [255, 228, 225];
readonly moccasin: [255, 228, 181];
readonly navajowhite: [255, 222, 173];
readonly navy: [0, 0, 128];
readonly oldlace: [253, 245, 230];
readonly olive: [128, 128, 0];
readonly olivedrab: [107, 142, 35];
readonly orange: [255, 165, 0];
readonly orangered: [255, 69, 0];
readonly orchid: [218, 112, 214];
readonly palegoldenrod: [238, 232, 170];
readonly palegreen: [152, 251, 152];
readonly paleturquoise: [175, 238, 238];
readonly palevioletred: [219, 112, 147];
readonly papayawhip: [255, 239, 213];
readonly peachpuff: [255, 218, 185];
readonly peru: [205, 133, 63];
readonly pink: [255, 192, 203];
readonly plum: [221, 160, 221];
readonly powderblue: [176, 224, 230];
readonly purple: [128, 0, 128];
readonly rebeccapurple: [102, 51, 153];
readonly red: [255, 0, 0];
readonly rosybrown: [188, 143, 143];
readonly royalblue: [65, 105, 225];
readonly saddlebrown: [139, 69, 19];
readonly salmon: [250, 128, 114];
readonly sandybrown: [244, 164, 96];
readonly seagreen: [46, 139, 87];
readonly seashell: [255, 245, 238];
readonly sienna: [160, 82, 45];
readonly silver: [192, 192, 192];
readonly skyblue: [135, 206, 235];
readonly slateblue: [106, 90, 205];
readonly slategray: [112, 128, 144];
readonly slategrey: [112, 128, 144];
readonly snow: [255, 250, 250];
readonly springgreen: [0, 255, 127];
readonly steelblue: [70, 130, 180];
readonly tan: [210, 180, 140];
readonly teal: [0, 128, 128];
readonly thistle: [216, 191, 216];
readonly tomato: [255, 99, 71];
readonly turquoise: [64, 224, 208];
readonly violet: [238, 130, 238];
readonly wheat: [245, 222, 179];
readonly white: [255, 255, 255];
readonly whitesmoke: [245, 245, 245];
readonly yellow: [255, 255, 0];
readonly yellowgreen: [154, 205, 50];
};
/**
* cache invalid color value
* @param key - cache key
* @param nullable - is nullable
* @returns cached value
*/
export declare const cacheInvalidColorValue: (cacheKey: string, format: string, nullable?: boolean) => SpecifiedColorChannels | string | null;
/**
* resolve invalid color value
* @param format - output format
* @param nullable - is nullable
* @returns resolved value
*/
export declare const resolveInvalidColorValue: (format: string, nullable?: boolean) => SpecifiedColorChannels | string | null;
/**
* validate color components
* @param arr - color components
* @param opt - options
* @param opt.alpha - alpha channel
* @param opt.minLength - min length
* @param opt.maxLength - max length
* @param opt.minRange - min range
* @param opt.maxRange - max range
* @param opt.validateRange - validate range
* @returns result - validated color components
*/
export declare const validateColorComponents: (arr: ColorChannels | TriColorChannels, opt?: {
alpha?: boolean;
minLength?: number;
maxLength?: number;
minRange?: number;
maxRange?: number;
validateRange?: boolean;
}) => ColorChannels | TriColorChannels;
/**
* transform matrix
* @param mtx - 3 * 3 matrix
* @param vct - vector
* @param [skip] - skip validate
* @returns TriColorChannels - [p1, p2, p3]
*/
export declare const transformMatrix: (mtx: ColorMatrix, vct: TriColorChannels, skip?: boolean) => TriColorChannels;
/**
* normalize color components
* @param colorA - color components [v1, v2, v3, v4]
* @param colorB - color components [v1, v2, v3, v4]
* @param [skip] - skip validate
* @returns result - [colorA, colorB]
*/
export declare const normalizeColorComponents: (colorA: [number | string, number | string, number | string, number | string], colorB: [number | string, number | string, number | string, number | string], skip?: boolean) => [ColorChannels, ColorChannels];
/**
* number to hex string
* @param value - numeric value
* @returns hex string
*/
export declare const numberToHexString: (value: number) => string;
/**
* angle to deg
* @param angle
* @returns deg: 0..360
*/
export declare const angleToDeg: (angle: string) => number;
/**
* parse alpha
* @param [alpha] - alpha value
* @returns alpha: 0..1
*/
export declare const parseAlpha: (alpha?: string) => number;
/**
* parse hex alpha
* @param value - alpha value in hex string
* @returns alpha: 0..1
*/
export declare const parseHexAlpha: (value: string) => number;
/**
* transform rgb to linear rgb
* @param rgb - [r, g, b] r|g|b: 0..255
* @param [skip] - skip validate
* @returns TriColorChannels - [r, g, b] r|g|b: 0..1
*/
export declare const transformRgbToLinearRgb: (rgb: TriColorChannels, skip?: boolean) => TriColorChannels;
/**
* transform rgb to xyz
* @param rgb - [r, g, b] r|g|b: 0..255
* @param [skip] - skip validate
* @returns TriColorChannels - [x, y, z]
*/
export declare const transformRgbToXyz: (rgb: TriColorChannels, skip?: boolean) => TriColorChannels;
/**
* transform rgb to xyz-d50
* @param rgb - [r, g, b] r|g|b: 0..255 alpha: 0..1
* @returns TriColorChannels - [x, y, z]
*/
export declare const transformRgbToXyzD50: (rgb: TriColorChannels) => TriColorChannels;
/**
* transform linear rgb to rgb
* @param rgb - [r, g, b] r|g|b: 0..1
* @param [round] - round result
* @returns TriColorChannels - [r, g, b] r|g|b: 0..255
*/
export declare const transformLinearRgbToRgb: (rgb: TriColorChannels, round?: boolean) => TriColorChannels;
/**
* transform xyz to rgb
* @param xyz - [x, y, z]
* @param [skip] - skip validate
* @returns TriColorChannels - [r, g, b] r|g|b: 0..255
*/
export declare const transformXyzToRgb: (xyz: TriColorChannels, skip?: boolean) => TriColorChannels;
/**
* transform xyz to xyz-d50
* @param xyz - [x, y, z]
* @returns TriColorChannels - [x, y, z]
*/
export declare const transformXyzToXyzD50: (xyz: TriColorChannels) => TriColorChannels;
/**
* transform xyz to hsl
* @param xyz - [x, y, z]
* @param [skip] - skip validate
* @returns TriColorChannels - [h, s, l]
*/
export declare const transformXyzToHsl: (xyz: TriColorChannels, skip?: boolean) => TriColorChannels;
/**
* transform xyz to hwb
* @param xyz - [x, y, z]
* @param [skip] - skip validate
* @returns TriColorChannels - [h, w, b]
*/
export declare const transformXyzToHwb: (xyz: TriColorChannels, skip?: boolean) => TriColorChannels;
/**
* transform xyz to oklab
* @param xyz - [x, y, z]
* @param [skip] - skip validate
* @returns TriColorChannels - [l, a, b]
*/
export declare const transformXyzToOklab: (xyz: TriColorChannels, skip?: boolean) => TriColorChannels;
/**
* transform xyz to oklch
* @param xyz - [x, y, z]
* @param [skip] - skip validate
* @returns TriColorChannels - [l, c, h]
*/
export declare const transformXyzToOklch: (xyz: TriColorChannels, skip?: boolean) => TriColorChannels;
/**
* transform xyz D50 to rgb
* @param xyz - [x, y, z]
* @param [skip] - skip validate
* @returns TriColorChannels - [r, g, b] r|g|b: 0..255
*/
export declare const transformXyzD50ToRgb: (xyz: TriColorChannels, skip?: boolean) => TriColorChannels;
/**
* transform xyz-d50 to lab
* @param xyz - [x, y, z]
* @param [skip] - skip validate
* @returns TriColorChannels - [l, a, b]
*/
export declare const transformXyzD50ToLab: (xyz: TriColorChannels, skip?: boolean) => TriColorChannels;
/**
* transform xyz-d50 to lch
* @param xyz - [x, y, z]
* @param [skip] - skip validate
* @returns TriColorChannels - [l, c, h]
*/
export declare const transformXyzD50ToLch: (xyz: TriColorChannels, skip?: boolean) => TriColorChannels;
/**
* convert rgb to hex color
* @param rgb - [r, g, b, alpha] r|g|b: 0..255 alpha: 0..1
* @returns hex color
*/
export declare const convertRgbToHex: (rgb: ColorChannels) => string;
/**
* convert linear rgb to hex color
* @param rgb - [r, g, b, alpha] r|g|b|alpha: 0..1
* @param [skip] - skip validate
* @returns hex color
*/
export declare const convertLinearRgbToHex: (rgb: ColorChannels, skip?: boolean) => string;
/**
* convert xyz to hex color
* @param xyz - [x, y, z, alpha]
* @returns hex color
*/
export declare const convertXyzToHex: (xyz: ColorChannels) => string;
/**
* convert xyz D50 to hex color
* @param xyz - [x, y, z, alpha]
* @returns hex color
*/
export declare const convertXyzD50ToHex: (xyz: ColorChannels) => string;
/**
* convert hex color to rgb
* @param value - hex color value
* @returns ColorChannels - [r, g, b, alpha] r|g|b: 0..255 alpha: 0..1
*/
export declare const convertHexToRgb: (value: string) => ColorChannels;
/**
* convert hex color to linear rgb
* @param value - hex color value
* @returns ColorChannels - [r, g, b, alpha] r|g|b|alpha: 0..1
*/
export declare const convertHexToLinearRgb: (value: string) => ColorChannels;
/**
* convert hex color to xyz
* @param value - hex color value
* @returns ColorChannels - [x, y, z, alpha]
*/
export declare const convertHexToXyz: (value: string) => ColorChannels;
/**
* parse rgb()
* @param value - rgb color value
* @param opt - options
* @returns parsed color - ['rgb', r, g, b, alpha], '(empty)', null
*/
export declare const parseRgb: (value: string, opt?: Options) => SpecifiedColorChannels | string | null;
/**
* parse hsl()
* @param value - hsl color value
* @param opt - options
* @returns parsed color - ['rgb', r, g, b, alpha], '(empty)', null
*/
export declare const parseHsl: (value: string, opt?: Options) => SpecifiedColorChannels | string | null;
/**
* parse hwb()
* @param value - hwb color value
* @param opt - options
* @returns parsed color - ['rgb', r, g, b, alpha], '(empty)', null
*/
export declare const parseHwb: (value: string, opt?: Options) => SpecifiedColorChannels | string | null;
/**
* parse lab()
* @param value - lab color value
* @param opt - options
* @returns parsed color
* - [xyz-d50, x, y, z, alpha], ['lab', l, a, b, alpha], '(empty)', null
*/
export declare const parseLab: (value: string, opt?: Options) => SpecifiedColorChannels | string | null;
/**
* parse lch()
* @param value - lch color value
* @param opt - options
* @returns parsed color
* - ['xyz-d50', x, y, z, alpha], ['lch', l, c, h, alpha], '(empty)', null
*/
export declare const parseLch: (value: string, opt?: Options) => SpecifiedColorChannels | string | null;
/**
* parse oklab()
* @param value - oklab color value
* @param opt - options
* @returns parsed color
* - ['xyz-d65', x, y, z, alpha], ['oklab', l, a, b, alpha], '(empty)', null
*/
export declare const parseOklab: (value: string, opt?: Options) => SpecifiedColorChannels | string | null;
/**
* parse oklch()
* @param value - oklch color value
* @param opt - options
* @returns parsed color
* - ['xyz-d65', x, y, z, alpha], ['oklch', l, c, h, alpha], '(empty)', null
*/
export declare const parseOklch: (value: string, opt?: Options) => SpecifiedColorChannels | string | null;
/**
* parse color()
* @param value - color function value
* @param opt - options
* @returns parsed color
* - ['xyz-(d50|d65)', x, y, z, alpha], [cs, r, g, b, alpha], '(empty)', null
*/
export declare const parseColorFunc: (value: string, opt?: Options) => SpecifiedColorChannels | string | null;
/**
* parse color value
* @param value - CSS color value
* @param opt - options
* @returns parsed color
* - ['xyz-(d50|d65)', x, y, z, alpha], ['rgb', r, g, b, alpha]
* - value, '(empty)', null
*/
export declare const parseColorValue: (value: string, opt?: Options) => SpecifiedColorChannels | string | null;
/**
* resolve color value
* @param value - CSS color value
* @param opt - options
* @returns resolved color
* - [cs, v1, v2, v3, alpha], value, '(empty)', null
*/
export declare const resolveColorValue: (value: string, opt?: Options) => SpecifiedColorChannels | string | null;
/**
* resolve color()
* @param value - color function value
* @param opt - options
* @returns resolved color - [cs, v1, v2, v3, alpha], '(empty)', null
*/
export declare const resolveColorFunc: (value: string, opt?: Options) => SpecifiedColorChannels | string | null;
/**
* convert color value to linear rgb
* @param value - CSS color value
* @param opt - options
* @returns ColorChannels | null - [r, g, b, alpha] r|g|b|alpha: 0..1
*/
export declare const convertColorToLinearRgb: (value: string, opt?: {
colorSpace?: string;
format?: string;
}) => ColorChannels | null;
/**
* convert color value to rgb
* @param value - CSS color value
* @param opt - options
* @returns ColorChannels | null - [r, g, b, alpha] r|g|b: 0..255 alpha: 0..1
*/
export declare const convertColorToRgb: (value: string, opt?: Options) => ColorChannels | null;
/**
* convert color value to xyz
* @param value - CSS color value
* @param opt - options
* @returns ColorChannels | null - [x, y, z, alpha]
*/
export declare const convertColorToXyz: (value: string, opt?: Options) => ColorChannels | null;
/**
* convert color value to hsl
* @param value - CSS color value
* @param opt - options
* @returns ColorChannels | null - [h, s, l, alpha], hue may be powerless
*/
export declare const convertColorToHsl: (value: string, opt?: Options) => ColorChannels | [number | string, number, number, number] | null;
/**
* convert color value to hwb
* @param value - CSS color value
* @param opt - options
* @returns ColorChannels | null - [h, w, b, alpha], hue may be powerless
*/
export declare const convertColorToHwb: (value: string, opt?: Options) => ColorChannels | [number | string, number, number, number] | null;
/**
* convert color value to lab
* @param value - CSS color value
* @param opt - options
* @returns ColorChannels | null - [l, a, b, alpha]
*/
export declare const convertColorToLab: (value: string, opt?: Options) => ColorChannels | null;
/**
* convert color value to lch
* @param value - CSS color value
* @param opt - options
* @returns ColorChannels | null - [l, c, h, alpha], hue may be powerless
*/
export declare const convertColorToLch: (value: string, opt?: Options) => ColorChannels | [number, number, number | string, number] | null;
/**
* convert color value to oklab
* @param value - CSS color value
* @param opt - options
* @returns ColorChannels | null - [l, a, b, alpha]
*/
export declare const convertColorToOklab: (value: string, opt?: Options) => ColorChannels | null;
/**
* convert color value to oklch
* @param value - CSS color value
* @param opt - options
* @returns ColorChannels | null - [l, c, h, alpha], hue may be powerless
*/
export declare const convertColorToOklch: (value: string, opt?: Options) => ColorChannels | [number, number, number | string, number] | null;
/**
* resolve color-mix()
* @param value - color-mix color value
* @param opt - options
* @param resolver - resolver function
* @returns resolved color - [cs, v1, v2, v3, alpha], '(empty)', null
*/
export declare const resolveColorMix: (value: string, opt?: Options, resolver?: (v: string, o?: Options) => string | null) => SpecifiedColorChannels | string | null;
export {};
File diff suppressed because it is too large. Load diff
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"color.js","names":[],"sources":["../../../src/js/color.ts"],"sourcesContent":["/**\n * color\n *\n * Ref: CSS Color Module Level 4\n * Sample code for Color Conversions\n * https://w3c.github.io/csswg-drafts/css-color-4/#color-conversion-code\n */\n\nimport { createCacheKey, getCache, setCache } from './cache';\nimport { isString } from './common';\nimport { interpolateHue, roundToPrecision, splitValue } from './util';\nimport {\n ColorChannels,\n ComputedColorChannels,\n Options,\n MatchedRegExp,\n SpecifiedColorChannels,\n StringColorChannels,\n StringColorSpacedChannels\n} from './typedef';\n\n/* constants */\nimport {\n ANGLE,\n CS_HUE_CAPT,\n CS_MIX,\n CS_RGB,\n CS_XYZ,\n FN_COLOR,\n FN_LIGHT_DARK,\n FN_MIX,\n NONE,\n NUM,\n PCT,\n SYN_COLOR_TYPE,\n SYN_FN_COLOR,\n SYN_HSL,\n SYN_HSL_LV3,\n SYN_LCH,\n SYN_MIX,\n SYN_MIX_CAPT,\n SYN_MIX_PART,\n SYN_MOD,\n SYN_RGB_LV3,\n VAL_COMP,\n VAL_MIX,\n VAL_SPEC\n} from './constant';\nconst NAMESPACE = 'color';\n\n/* numeric constants */\nconst PPTH = 0.001;\nconst HALF = 0.5;\nconst DUO = 2;\nconst TRIA = 3;\nconst QUAD = 4;\nconst OCT = 8;\nconst DEC = 10;\nconst DOZ = 12;\nconst HEX = 16;\nconst SEXA = 60;\nconst DEG_HALF = 180;\nconst DEG = 360;\nconst MAX_PCT = 100;\nconst MAX_RGB = 255;\nconst POW_SQR = 2;\nconst POW_CUBE = 3;\nconst POW_LINEAR = 2.4;\nconst LINEAR_COEF = 12.92;\nconst LINEAR_OFFSET = 0.055;\nconst LAB_L = 116;\nconst LAB_A = 500;\nconst LAB_B = 200;\nconst LAB_EPSILON = 216 / 24389;\nconst LAB_KAPPA = 24389 / 27;\n\n/* type definitions */\n/**\n * @type NumStrColorChannels - string or numeric color channels\n */\ntype NumStrColorChannels = [\n x: number | string,\n y: number | string,\n z: number | string,\n alpha: number | string\n];\n\n/**\n * @type TriColorChannels - color channels without alpha\n */\ntype TriColorChannels = [x: number, y: number, z: number];\n\n/**\n * @type ColorMatrix - color matrix\n */\ntype ColorMatrix = [\n r1: TriColorChannels,\n r2: TriColorChannels,\n r3: TriColorChannels\n];\n\n/* white point */\nconst D50: TriColorChannels = [\n 0.3457 / 0.3585,\n 1.0,\n (1.0 - 0.3457 - 0.3585) / 0.3585\n];\nconst MATRIX_D50_TO_D65: ColorMatrix = [\n [0.955473421488075, -0.02309845494876471, 0.06325924320057072],\n [-0.0283697093338637, 1.0099953980813041, 0.021041441191917323],\n [0.012314014864481998, -0.020507649298898964, 1.330365926242124]\n];\nconst MATRIX_D65_TO_D50: ColorMatrix = [\n [1.0479297925449969, 0.022946870601609652, -0.05019226628920524],\n [0.02962780877005599, 0.9904344267538799, -0.017073799063418826],\n [-0.009243040646204504, 0.015055191490298152, 0.7518742814281371]\n];\n\n/* color space */\nconst MATRIX_L_RGB_TO_XYZ: ColorMatrix = [\n [506752 / 1228815, 87881 / 245763, 12673 / 70218],\n [87098 / 409605, 175762 / 245763, 12673 / 175545],\n [7918 / 409605, 87881 / 737289, 1001167 / 1053270]\n];\nconst MATRIX_XYZ_TO_L_RGB: ColorMatrix = [\n [12831 / 3959, -329 / 214, -1974 / 3959],\n [-851781 / 878810, 1648619 / 878810, 36519 / 878810],\n [705 / 12673, -2585 / 12673, 705 / 667]\n];\nconst MATRIX_XYZ_TO_LMS: ColorMatrix = [\n [0.819022437996703, 0.3619062600528904, -0.1288737815209879],\n [0.0329836539323885, 0.9292868615863434, 0.0361446663506424],\n [0.0481771893596242, 0.2642395317527308, 0.6335478284694309]\n];\nconst MATRIX_LMS_TO_XYZ: ColorMatrix = [\n [1.2268798758459243, -0.5578149944602171, 0.2813910456659647],\n [-0.0405757452148008, 1.112286803280317, -0.0717110580655164],\n [-0.0763729366746601, -0.4214933324022432, 1.5869240198367816]\n];\nconst MATRIX_OKLAB_TO_LMS: ColorMatrix = [\n [1.0, 0.3963377773761749, 0.2158037573099136],\n [1.0, -0.1055613458156586, -0.0638541728258133],\n [1.0, -0.0894841775298119, -1.2914855480194092]\n];\nconst MATRIX_LMS_TO_OKLAB: ColorMatrix = [\n [0.210454268309314, 0.7936177747023054, -0.0040720430116193],\n [1.9779985324311684, -2.4285922420485799, 0.450593709617411],\n [0.0259040424655478, 0.7827717124575296, -0.8086757549230774]\n];\nconst MATRIX_P3_TO_XYZ: ColorMatrix = [\n [608311 / 1250200, 189793 / 714400, 198249 / 1000160],\n [35783 / 156275, 247089 / 357200, 198249 / 2500400],\n [0 / 1, 32229 / 714400, 5220557 / 5000800]\n];\nconst MATRIX_REC2020_TO_XYZ: ColorMatrix = [\n [63426534 / 99577255, 20160776 / 139408157, 47086771 / 278816314],\n [26158966 / 99577255, 472592308 / 697040785, 8267143 / 139408157],\n [0 / 1, 19567812 / 697040785, 295819943 / 278816314]\n];\nconst MATRIX_A98_TO_XYZ: ColorMatrix = [\n [573536 / 994567, 263643 / 1420810, 187206 / 994567],\n [591459 / 1989134, 6239551 / 9945670, 374412 / 4972835],\n [53769 / 1989134, 351524 / 4972835, 4929758 / 4972835]\n];\nconst MATRIX_PROPHOTO_TO_XYZ_D50: ColorMatrix = [\n [0.7977666449006423, 0.13518129740053308, 0.0313477341283922],\n [0.2880748288194013, 0.711835234241873, 0.00008993693872564],\n [0.0, 0.0, 0.8251046025104602]\n];\n\n/* regexp */\nconst REG_COLOR = new RegExp(`^(?:${SYLine truncated
+21
View File
@@ -0,0 +1,21 @@
/**
* common
*/
/**
* get type
* @param o - object to check
* @returns type of object
*/
export declare const getType: (o: unknown) => string;
/**
* is string
* @param o - object to check
* @returns result
*/
export declare const isString: (o: unknown) => o is string;
/**
* is string or number
* @param o - object to check
* @returns result
*/
export declare const isStringOrNumber: (o: unknown) => boolean;
+17
View File
@@ -0,0 +1,17 @@
//#region src/js/common.ts
/**
* is string
* @param o - object to check
* @returns result
*/
var isString = (o) => typeof o === "string" || o instanceof String;
/**
* is string or number
* @param o - object to check
* @returns result
*/
var isStringOrNumber = (o) => isString(o) || typeof o === "number";
//#endregion
export { isString, isStringOrNumber };
//# sourceMappingURL=common.js.map
@@ -0,0 +1 @@
{"version":3,"file":"common.js","names":[],"sources":["../../../src/js/common.ts"],"sourcesContent":["/**\n * common\n */\n\n/* numeric constants */\nconst TYPE_FROM = 8;\nconst TYPE_TO = -1;\n\n/**\n * get type\n * @param o - object to check\n * @returns type of object\n */\nexport const getType = (o: unknown): string =>\n Object.prototype.toString.call(o).slice(TYPE_FROM, TYPE_TO);\n\n/**\n * is string\n * @param o - object to check\n * @returns result\n */\nexport const isString = (o: unknown): o is string =>\n typeof o === 'string' || o instanceof String;\n\n/**\n * is string or number\n * @param o - object to check\n * @returns result\n */\nexport const isStringOrNumber = (o: unknown): boolean =>\n isString(o) || typeof o === 'number';\n"],"mappings":";;;;;;AAqBA,IAAa,YAAY,MACvB,OAAO,MAAM,YAAY,aAAa;;;;;;AAOxC,IAAa,oBAAoB,MAC/B,SAAS,CAAC,KAAK,OAAO,MAAM"}
+43
View File
@@ -0,0 +1,43 @@
/**
* constant
*/
export declare const ANGLE = "deg|g?rad|turn";
export declare const LENGTH = "[cm]m|[dls]?v(?:[bhiw]|max|min)|in|p[ctx]|q|r?(?:[cl]h|cap|e[mx]|ic)";
export declare const NUM = "[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?";
export declare const NUM_POSITIVE = "\\+?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?";
export declare const NONE = "none";
export declare const PCT = "[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%";
export declare const SYN_FN_CALC = "^(?:calc|clamp|max|min|exp|hypot|log|pow|sqrt|abs|sign|mod|rem|round|a?(?:cos|sin|tan)|atan2)\\(|(?<=[*\\/\\s\\(])(?:calc|clamp|max|min|exp|hypot|log|pow|sqrt|abs|sign|mod|rem|round|a?(?:cos|sin|tan)|atan2)\\(";
export declare const SYN_FN_MATH_START = "^(?:clamp|max|min|exp|hypot|log|pow|sqrt|abs|sign|mod|rem|round|a?(?:cos|sin|tan)|atan2)\\($";
export declare const SYN_FN_VAR = "^var\\(|(?<=[*\\/\\s\\(])var\\(";
export declare const SYN_FN_VAR_START = "^(?:var|calc|clamp|max|min|exp|hypot|log|pow|sqrt|abs|sign|mod|rem|round|a?(?:cos|sin|tan)|atan2)\\(";
export declare const CS_HUE = "(?:(?:ok)?lch|hsl|hwb)(?:\\s(?:(?:de|in)creasing|longer|shorter)\\shue)?";
export declare const CS_HUE_CAPT = "((?:ok)?lch|hsl|hwb)(?:\\s((?:de|in)creasing|longer|shorter)\\shue)?";
export declare const CS_LAB = "(?:ok)?lab";
export declare const CS_LCH = "(?:ok)?lch";
export declare const CS_SRGB = "srgb(?:-linear)?";
export declare const CS_RGB = "(?:a98|prophoto)-rgb|display-p3|rec2020|srgb(?:-linear)?";
export declare const CS_XYZ = "xyz(?:-d(?:50|65))?";
export declare const CS_RECT = "(?:ok)?lab|(?:a98|prophoto)-rgb|display-p3|rec2020|srgb(?:-linear)?|xyz(?:-d(?:50|65))?";
export declare const CS_MIX = "(?:(?:ok)?lch|hsl|hwb)(?:\\s(?:(?:de|in)creasing|longer|shorter)\\shue)?|(?:ok)?lab|(?:a98|prophoto)-rgb|display-p3|rec2020|srgb(?:-linear)?|xyz(?:-d(?:50|65))?";
export declare const FN_COLOR = "color(";
export declare const FN_LIGHT_DARK = "light-dark(";
export declare const FN_MIX = "color-mix(";
export declare const FN_REL = "(?:(?:ok)?l(?:ab|ch)|color|hsla?|hwb|rgba?)\\(\\s*from\\s+";
export declare const FN_REL_CAPT = "((?:ok)?l(?:ab|ch)|color|hsla?|hwb|rgba?)\\(\\s*from\\s+";
export declare const FN_VAR = "var(";
export declare const SYN_FN_COLOR = "(?:(?:a98|prophoto)-rgb|display-p3|rec2020|srgb(?:-linear)?|xyz(?:-d(?:50|65))?)(?:\\s+(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)){3}(?:\\s*\\/\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none))?";
export declare const SYN_FN_LIGHT_DARK = "^light-dark\\(";
export declare const SYN_FN_REL = "^(?:(?:ok)?l(?:ab|ch)|color|hsla?|hwb|rgba?)\\(\\s*from\\s+|(?<=[\\s])(?:(?:ok)?l(?:ab|ch)|color|hsla?|hwb|rgba?)\\(\\s*from\\s+";
export declare const SYN_HSL = "(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:deg|g?rad|turn)?|none)(?:\\s+(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)){2}(?:\\s*\\/\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none))?";
export declare const SYN_HSL_LV3 = "[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:deg|g?rad|turn)?(?:\\s*,\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%){2}(?:\\s*,\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%))?";
export declare const SYN_LCH = "(?:(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)\\s+){2}(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:deg|g?rad|turn)?|none)(?:\\s*\\/\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none))?";
export declare const SYN_MOD = "(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)(?:\\s+(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)){2}(?:\\s*\\/\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none))?";
export declare const SYN_RGB_LV3 = "(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:\\s*,\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?){2}|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%(?:\\s*,\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%){2})(?:\\s*,\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%))?";
export declare const SYN_COLOR_TYPE = "[a-z]+|#[\\da-f]{3}|#[\\da-f]{4}|#[\\da-f]{6}|#[\\da-f]{8}|hsla?\\(\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:deg|g?rad|turn)?(?:\\s*,\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%){2}(?:\\s*,\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%))?\\s*\\)|rgba?\\(\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:\\s*,\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?){2}|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%(?:\\s*,\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%){2})(?:\\s*,\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%))?\\s*\\)|(?:hsla?|hwb)\\(\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:deg|g?rad|turn)?|none)(?:\\s+(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)){2}(?:\\s*\\/\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none))?\\s*\\)|(?:(?:ok)?lab|rgba?)\\(\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)(?:\\s+(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)){2}(?:\\s*\\/\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none))?\\s*\\)|(?:ok)?lch\\(\\s*(?:(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)\\s+){2}(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:deg|g?rad|turn)?|none)(?:\\s*\\/\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none))?\\s*\\)|color\\(\\s*(?:(?:a98|prophoto)-rgb|display-p3|rec2020|srgb(?:-linear)?|xyz(?:-d(?:50|65))?)(?:\\s+(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)){3}(?:\\s*\\/\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none))?\\s*\\)";
export declare const SYN_MIX_PART = "(?:[a-z]+|#[\\da-f]{3}|#[\\da-f]{4}|#[\\da-f]{6}|#[\\da-f]{8}|hsla?\\(\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:deg|g?rad|turn)?(?:\\s*,\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%){2}(?:\\s*,\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%))?\\s*\\)|rgba?\\(\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:\\s*,\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?){2}|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%(?:\\s*,\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%){2})(?:\\s*,\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%))?\\s*\\)|(?:hsla?|hwb)\\(\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:deg|g?rad|turn)?|none)(?:\\s+(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)){2}(?:\\s*\\/\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none))?\\s*\\)|(?:(?:ok)?lab|rgba?)\\(\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)(?:\\s+(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)){2}(?:\\s*\\/\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none))?\\s*\\)|(?:ok)?lch\\(\\s*(?:(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)\\s+){2}(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:deg|g?rad|turn)?|none)(?:\\s*\\/\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none))?\\s*\\)|color\\(\\s*(?:(?:a98|prophoto)-rgb|display-p3|rec2020|srgb(?:-linear)?|xyz(?:-d(?:50|65))?)(?:\\s+(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)){3}(?:\\s*\\/\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none))?\\s*\\))(?:\\s+[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%)?";
export declare const SYN_MIX = "color-mix\\(\\s*in\\s+(?:(?:(?:ok)?lch|hsl|hwb)(?:\\s(?:(?:de|in)creasing|longer|shorter)\\shue)?|(?:ok)?lab|(?:a98|prophoto)-rgb|display-p3|rec2020|srgb(?:-linear)?|xyz(?:-d(?:50|65))?)\\s*,\\s*(?:[a-z]+|#[\\da-f]{3}|#[\\da-f]{4}|#[\\da-f]{6}|#[\\da-f]{8}|hsla?\\(\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:deg|g?rad|turn)?(?:\\s*,\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%){2}(?:\\s*,\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%))?\\s*\\)|rgba?\\(\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:\\s*,\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?){2}|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%(?:\\s*,\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%){2})(?:\\s*,\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%))?\\s*\\)|(?:hsla?|hwb)\\(\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:deg|g?rad|turn)?|none)(?:\\s+(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)){2}(?:\\s*\\/\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none))?\\s*\\)|(?:(?:ok)?lab|rgba?)\\(\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)(?:\\s+(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)){2}(?:\\s*\\/\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none))?\\s*\\)|(?:ok)?lch\\(\\s*(?:(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)\\s+){2}(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:deg|g?rad|turn)?|none)(?:\\s*\\/\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none))?\\s*\\)|color\\(\\s*(?:(?:a98|prophoto)-rgb|display-p3|rec2020|srgb(?:-linear)?|xyz(?:-d(?:50|65))?)(?:\\s+(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)){3}(?:\\s*\\/\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none))?\\s*\\))(?:\\s+[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%)?\\s*,\\s*(?:[a-z]+|#[\\da-f]{3}|#[\\da-f]{4}|#[\\da-f]{6}|#[\\da-f]{8}|hsla?\\(\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:deg|g?rad|turn)?(?:\\s*,\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%){2}(?:\\s*,\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%))?\\s*\\)|rgba?\\(\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:\\s*,\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?){2}|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%(?:\\s*,\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%){2})(?:\\s*,\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%))?\\s*\\)|(?:hsla?|hwb)\\(\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:deg|g?rad|turn)?|none)(?:\\s+(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)){2}(?:\\s*\\/\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none))?\\s*\\)|(?:(?:ok)?lab|rgba?)\\(\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)(?:\\s+(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)){2}(?:\\s*\\/\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none))?\\s*\\)|(?:ok)?lch\\(\\s*(?:(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)\\s+){2}(?:[+-]?(?:Line truncated
export declare const SYN_MIX_CAPT = "color-mix\\(\\s*in\\s+((?:(?:ok)?lch|hsl|hwb)(?:\\s(?:(?:de|in)creasing|longer|shorter)\\shue)?|(?:ok)?lab|(?:a98|prophoto)-rgb|display-p3|rec2020|srgb(?:-linear)?|xyz(?:-d(?:50|65))?)\\s*,\\s*((?:[a-z]+|#[\\da-f]{3}|#[\\da-f]{4}|#[\\da-f]{6}|#[\\da-f]{8}|hsla?\\(\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:deg|g?rad|turn)?(?:\\s*,\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%){2}(?:\\s*,\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%))?\\s*\\)|rgba?\\(\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:\\s*,\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?){2}|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%(?:\\s*,\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%){2})(?:\\s*,\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%))?\\s*\\)|(?:hsla?|hwb)\\(\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:deg|g?rad|turn)?|none)(?:\\s+(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)){2}(?:\\s*\\/\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none))?\\s*\\)|(?:(?:ok)?lab|rgba?)\\(\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)(?:\\s+(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)){2}(?:\\s*\\/\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none))?\\s*\\)|(?:ok)?lch\\(\\s*(?:(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)\\s+){2}(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:deg|g?rad|turn)?|none)(?:\\s*\\/\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none))?\\s*\\)|color\\(\\s*(?:(?:a98|prophoto)-rgb|display-p3|rec2020|srgb(?:-linear)?|xyz(?:-d(?:50|65))?)(?:\\s+(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)){3}(?:\\s*\\/\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none))?\\s*\\))(?:\\s+[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%)?)\\s*,\\s*((?:[a-z]+|#[\\da-f]{3}|#[\\da-f]{4}|#[\\da-f]{6}|#[\\da-f]{8}|hsla?\\(\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:deg|g?rad|turn)?(?:\\s*,\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%){2}(?:\\s*,\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%))?\\s*\\)|rgba?\\(\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:\\s*,\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?){2}|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%(?:\\s*,\\s*[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%){2})(?:\\s*,\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%))?\\s*\\)|(?:hsla?|hwb)\\(\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?(?:deg|g?rad|turn)?|none)(?:\\s+(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)){2}(?:\\s*\\/\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none))?\\s*\\)|(?:(?:ok)?lab|rgba?)\\(\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)(?:\\s+(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)){2}(?:\\s*\\/\\s*(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none))?\\s*\\)|(?:ok)?lch\\(\\s*(?:(?:[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?|[+-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)(?:e-?(?:0|[1-9]\\d*))?%|none)\\s+){2}(?:[+Line truncated
export declare const VAL_COMP = "computedValue";
export declare const VAL_MIX = "mixValue";
export declare const VAL_SPEC = "specifiedValue";
+61
View File
@@ -0,0 +1,61 @@
//#region src/js/constant.ts
/**
* constant
*/
var _DIGIT = "(?:0|[1-9]\\d*)";
var _MATH = `clamp|max|min|exp|hypot|log|pow|sqrt|abs|sign|mod|rem|round|a?(?:cos|sin|tan)|atan2`;
var _CALC = `calc|${_MATH}`;
var _VAR = `var|${_CALC}`;
var ANGLE = "deg|g?rad|turn";
var LENGTH = "[cm]m|[dls]?v(?:[bhiw]|max|min)|in|p[ctx]|q|r?(?:[cl]h|cap|e[mx]|ic)";
var NUM = `[+-]?(?:${_DIGIT}(?:\\.\\d*)?|\\.\\d+)(?:e-?${_DIGIT})?`;
var NUM_POSITIVE = `\\+?(?:${_DIGIT}(?:\\.\\d*)?|\\.\\d+)(?:e-?${_DIGIT})?`;
var NONE = "none";
var PCT = `${NUM}%`;
var SYN_FN_CALC = `^(?:${_CALC})\\(|(?<=[*\\/\\s\\(])(?:${_CALC})\\(`;
var SYN_FN_MATH_START = `^(?:${_MATH})\\($`;
var SYN_FN_VAR = "^var\\(|(?<=[*\\/\\s\\(])var\\(";
var SYN_FN_VAR_START = `^(?:${_VAR})\\(`;
var _ALPHA = `(?:\\s*\\/\\s*(?:${NUM}|${PCT}|${NONE}))?`;
var _ALPHA_LV3 = `(?:\\s*,\\s*(?:${NUM}|${PCT}))?`;
var _COLOR_FUNC = "(?:ok)?l(?:ab|ch)|color|hsla?|hwb|rgba?";
var _COLOR_KEY = "[a-z]+|#[\\da-f]{3}|#[\\da-f]{4}|#[\\da-f]{6}|#[\\da-f]{8}";
var _CS_HUE = "(?:ok)?lch|hsl|hwb";
var _CS_HUE_ARC = "(?:de|in)creasing|longer|shorter";
var _NUM_ANGLE = `${NUM}(?:${ANGLE})?`;
var _NUM_ANGLE_NONE = `(?:${NUM}(?:${ANGLE})?|${NONE})`;
var _NUM_PCT_NONE = `(?:${NUM}|${PCT}|${NONE})`;
var CS_HUE = `(?:${_CS_HUE})(?:\\s(?:${_CS_HUE_ARC})\\shue)?`;
var CS_HUE_CAPT = `(${_CS_HUE})(?:\\s(${_CS_HUE_ARC})\\shue)?`;
var CS_LAB = "(?:ok)?lab";
var CS_LCH = "(?:ok)?lch";
var CS_SRGB = "srgb(?:-linear)?";
var CS_RGB = `(?:a98|prophoto)-rgb|display-p3|rec2020|${CS_SRGB}`;
var CS_XYZ = "xyz(?:-d(?:50|65))?";
var CS_RECT = `${CS_LAB}|${CS_RGB}|${CS_XYZ}`;
var CS_MIX = `${CS_HUE}|${CS_RECT}`;
var FN_COLOR = "color(";
var FN_LIGHT_DARK = "light-dark(";
var FN_MIX = "color-mix(";
var FN_REL = `(?:${_COLOR_FUNC})\\(\\s*from\\s+`;
var FN_REL_CAPT = `(${_COLOR_FUNC})\\(\\s*from\\s+`;
var FN_VAR = "var(";
var SYN_FN_COLOR = `(?:${CS_RGB}|${CS_XYZ})(?:\\s+${_NUM_PCT_NONE}){3}${_ALPHA}`;
var SYN_FN_LIGHT_DARK = "^light-dark\\(";
var SYN_FN_REL = `^${FN_REL}|(?<=[\\s])${FN_REL}`;
var SYN_HSL = `${_NUM_ANGLE_NONE}(?:\\s+${_NUM_PCT_NONE}){2}${_ALPHA}`;
var SYN_HSL_LV3 = `${_NUM_ANGLE}(?:\\s*,\\s*${PCT}){2}${_ALPHA_LV3}`;
var SYN_LCH = `(?:${_NUM_PCT_NONE}\\s+){2}${_NUM_ANGLE_NONE}${_ALPHA}`;
var SYN_MOD = `${_NUM_PCT_NONE}(?:\\s+${_NUM_PCT_NONE}){2}${_ALPHA}`;
var SYN_RGB_LV3 = `(?:${NUM}(?:\\s*,\\s*${NUM}){2}|${PCT}(?:\\s*,\\s*${PCT}){2})${_ALPHA_LV3}`;
var SYN_COLOR_TYPE = `${_COLOR_KEY}|hsla?\\(\\s*${SYN_HSL_LV3}\\s*\\)|rgba?\\(\\s*${SYN_RGB_LV3}\\s*\\)|(?:hsla?|hwb)\\(\\s*${SYN_HSL}\\s*\\)|(?:(?:ok)?lab|rgba?)\\(\\s*${SYN_MOD}\\s*\\)|(?:ok)?lch\\(\\s*${SYN_LCH}\\s*\\)|color\\(\\s*${SYN_FN_COLOR}\\s*\\)`;
var SYN_MIX_PART = `(?:${SYN_COLOR_TYPE})(?:\\s+${PCT})?`;
var SYN_MIX = `color-mix\\(\\s*in\\s+(?:${CS_MIX})\\s*,\\s*${SYN_MIX_PART}\\s*,\\s*${SYN_MIX_PART}\\s*\\)`;
var SYN_MIX_CAPT = `color-mix\\(\\s*in\\s+(${CS_MIX})\\s*,\\s*(${SYN_MIX_PART})\\s*,\\s*(${SYN_MIX_PART})\\s*\\)`;
var VAL_COMP = "computedValue";
var VAL_MIX = "mixValue";
var VAL_SPEC = "specifiedValue";
//#endregion
export { ANGLE, CS_HUE, CS_HUE_CAPT, CS_LAB, CS_LCH, CS_MIX, CS_RECT, CS_RGB, CS_SRGB, CS_XYZ, FN_COLOR, FN_LIGHT_DARK, FN_MIX, FN_REL, FN_REL_CAPT, FN_VAR, LENGTH, NONE, NUM, NUM_POSITIVE, PCT, SYN_COLOR_TYPE, SYN_FN_CALC, SYN_FN_COLOR, SYN_FN_LIGHT_DARK, SYN_FN_MATH_START, SYN_FN_REL, SYN_FN_VAR, SYN_FN_VAR_START, SYN_HSL, SYN_HSL_LV3, SYN_LCH, SYN_MIX, SYN_MIX_CAPT, SYN_MIX_PART, SYN_MOD, SYN_RGB_LV3, VAL_COMP, VAL_MIX, VAL_SPEC };
//# sourceMappingURL=constant.js.map
@@ -0,0 +1 @@
{"version":3,"file":"constant.js","names":[],"sources":["../../../src/js/constant.ts"],"sourcesContent":["/**\n * constant\n */\n\n/* values and units */\nconst _DIGIT = '(?:0|[1-9]\\\\d*)';\nconst _COMPARE = 'clamp|max|min';\nconst _EXPO = 'exp|hypot|log|pow|sqrt';\nconst _SIGN = 'abs|sign';\nconst _STEP = 'mod|rem|round';\nconst _TRIG = 'a?(?:cos|sin|tan)|atan2';\nconst _MATH = `${_COMPARE}|${_EXPO}|${_SIGN}|${_STEP}|${_TRIG}`;\nconst _CALC = `calc|${_MATH}`;\nconst _VAR = `var|${_CALC}`;\nexport const ANGLE = 'deg|g?rad|turn';\nexport const LENGTH =\n '[cm]m|[dls]?v(?:[bhiw]|max|min)|in|p[ctx]|q|r?(?:[cl]h|cap|e[mx]|ic)';\nexport const NUM = `[+-]?(?:${_DIGIT}(?:\\\\.\\\\d*)?|\\\\.\\\\d+)(?:e-?${_DIGIT})?`;\nexport const NUM_POSITIVE = `\\\\+?(?:${_DIGIT}(?:\\\\.\\\\d*)?|\\\\.\\\\d+)(?:e-?${_DIGIT})?`;\nexport const NONE = 'none';\nexport const PCT = `${NUM}%`;\nexport const SYN_FN_CALC = `^(?:${_CALC})\\\\(|(?<=[*\\\\/\\\\s\\\\(])(?:${_CALC})\\\\(`;\nexport const SYN_FN_MATH_START = `^(?:${_MATH})\\\\($`;\nexport const SYN_FN_VAR = '^var\\\\(|(?<=[*\\\\/\\\\s\\\\(])var\\\\(';\nexport const SYN_FN_VAR_START = `^(?:${_VAR})\\\\(`;\n\n/* colors */\nconst _ALPHA = `(?:\\\\s*\\\\/\\\\s*(?:${NUM}|${PCT}|${NONE}))?`;\nconst _ALPHA_LV3 = `(?:\\\\s*,\\\\s*(?:${NUM}|${PCT}))?`;\nconst _COLOR_FUNC = '(?:ok)?l(?:ab|ch)|color|hsla?|hwb|rgba?';\nconst _COLOR_KEY = '[a-z]+|#[\\\\da-f]{3}|#[\\\\da-f]{4}|#[\\\\da-f]{6}|#[\\\\da-f]{8}';\nconst _CS_HUE = '(?:ok)?lch|hsl|hwb';\nconst _CS_HUE_ARC = '(?:de|in)creasing|longer|shorter';\nconst _NUM_ANGLE = `${NUM}(?:${ANGLE})?`;\nconst _NUM_ANGLE_NONE = `(?:${NUM}(?:${ANGLE})?|${NONE})`;\nconst _NUM_PCT_NONE = `(?:${NUM}|${PCT}|${NONE})`;\nexport const CS_HUE = `(?:${_CS_HUE})(?:\\\\s(?:${_CS_HUE_ARC})\\\\shue)?`;\nexport const CS_HUE_CAPT = `(${_CS_HUE})(?:\\\\s(${_CS_HUE_ARC})\\\\shue)?`;\nexport const CS_LAB = '(?:ok)?lab';\nexport const CS_LCH = '(?:ok)?lch';\nexport const CS_SRGB = 'srgb(?:-linear)?';\nexport const CS_RGB = `(?:a98|prophoto)-rgb|display-p3|rec2020|${CS_SRGB}`;\nexport const CS_XYZ = 'xyz(?:-d(?:50|65))?';\nexport const CS_RECT = `${CS_LAB}|${CS_RGB}|${CS_XYZ}`;\nexport const CS_MIX = `${CS_HUE}|${CS_RECT}`;\nexport const FN_COLOR = 'color(';\nexport const FN_LIGHT_DARK = 'light-dark(';\nexport const FN_MIX = 'color-mix(';\nexport const FN_REL = `(?:${_COLOR_FUNC})\\\\(\\\\s*from\\\\s+`;\nexport const FN_REL_CAPT = `(${_COLOR_FUNC})\\\\(\\\\s*from\\\\s+`;\nexport const FN_VAR = 'var(';\nexport const SYN_FN_COLOR = `(?:${CS_RGB}|${CS_XYZ})(?:\\\\s+${_NUM_PCT_NONE}){3}${_ALPHA}`;\nexport const SYN_FN_LIGHT_DARK = '^light-dark\\\\(';\nexport const SYN_FN_REL = `^${FN_REL}|(?<=[\\\\s])${FN_REL}`;\nexport const SYN_HSL = `${_NUM_ANGLE_NONE}(?:\\\\s+${_NUM_PCT_NONE}){2}${_ALPHA}`;\nexport const SYN_HSL_LV3 = `${_NUM_ANGLE}(?:\\\\s*,\\\\s*${PCT}){2}${_ALPHA_LV3}`;\nexport const SYN_LCH = `(?:${_NUM_PCT_NONE}\\\\s+){2}${_NUM_ANGLE_NONE}${_ALPHA}`;\nexport const SYN_MOD = `${_NUM_PCT_NONE}(?:\\\\s+${_NUM_PCT_NONE}){2}${_ALPHA}`;\nexport const SYN_RGB_LV3 = `(?:${NUM}(?:\\\\s*,\\\\s*${NUM}){2}|${PCT}(?:\\\\s*,\\\\s*${PCT}){2})${_ALPHA_LV3}`;\nexport const SYN_COLOR_TYPE = `${_COLOR_KEY}|hsla?\\\\(\\\\s*${SYN_HSL_LV3}\\\\s*\\\\)|rgba?\\\\(\\\\s*${SYN_RGB_LV3}\\\\s*\\\\)|(?:hsla?|hwb)\\\\(\\\\s*${SYN_HSL}\\\\s*\\\\)|(?:(?:ok)?lab|rgba?)\\\\(\\\\s*${SYN_MOD}\\\\s*\\\\)|(?:ok)?lch\\\\(\\\\s*${SYN_LCH}\\\\s*\\\\)|color\\\\(\\\\s*${SYN_FN_COLOR}\\\\s*\\\\)`;\nexport const SYN_MIX_PART = `(?:${SYN_COLOR_TYPE})(?:\\\\s+${PCT})?`;\nexport const SYN_MIX = `color-mix\\\\(\\\\s*in\\\\s+(?:${CS_MIX})\\\\s*,\\\\s*${SYN_MIX_PART}\\\\s*,\\\\s*${SYN_MIX_PART}\\\\s*\\\\)`;\nexport const SYN_MIX_CAPT = `color-mix\\\\(\\\\s*in\\\\s+(${CS_MIX})\\\\s*,\\\\s*(${SYN_MIX_PART})\\\\s*,\\\\s*(${SYN_MIX_PART})\\\\s*\\\\)`;\n\n/* formats */\nexport const VAL_COMP = 'computedValue';\nexport const VAL_MIX = 'mixValue';\nexport const VAL_SPEC = 'specifiedValue';\n"],"mappings":";;;;AAKA,IAAM,SAAS;AAMf,IAAM,QAAQ;AACd,IAAM,QAAQ,QAAQ;AACtB,IAAM,OAAO,OAAO;AACpB,IAAa,QAAQ;AACrB,IAAa,SACX;AACF,IAAa,MAAM,WAAW,OAAO,6BAA6B,OAAO;AACzE,IAAa,eAAe,UAAU,OAAO,6BAA6B,OAAO;AACjF,IAAa,OAAO;AACpB,IAAa,MAAM,GAAG,IAAI;AAC1B,IAAa,cAAc,OAAO,MAAM,2BAA2B,MAAM;AACzE,IAAa,oBAAoB,OAAO,MAAM;AAC9C,IAAa,aAAa;AAC1B,IAAa,mBAAmB,OAAO,KAAK;AAG5C,IAAM,SAAS,oBAAoB,IAAI,GAAG,IAAI,GAAG,KAAK;AACtD,IAAM,aAAa,kBAAkB,IAAI,GAAG,IAAI;AAChD,IAAM,cAAc;AACpB,IAAM,aAAa;AACnB,IAAM,UAAU;AAChB,IAAM,cAAc;AACpB,IAAM,aAAa,GAAG,IAAI,KAAK,MAAM;AACrC,IAAM,kBAAkB,MAAM,IAAI,KAAK,MAAM,KAAK,KAAK;AACvD,IAAM,gBAAgB,MAAM,IAAI,GAAG,IAAI,GAAG,KAAK;AAC/C,IAAa,SAAS,MAAM,QAAQ,YAAY,YAAY;AAC5D,IAAa,cAAc,IAAI,QAAQ,UAAU,YAAY;AAC7D,IAAa,SAAS;AACtB,IAAa,SAAS;AACtB,IAAa,UAAU;AACvB,IAAa,SAAS,2CAA2C;AACjE,IAAa,SAAS;AACtB,IAAa,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG;AAC9C,IAAa,SAAS,GAAG,OAAO,GAAG;AACnC,IAAa,WAAW;AACxB,IAAa,gBAAgB;AAC7B,IAAa,SAAS;AACtB,IAAa,SAAS,MAAM,YAAY;AACxC,IAAa,cAAc,IAAI,YAAY;AAC3C,IAAa,SAAS;AACtB,IAAa,eAAe,MAAM,OAAO,GAAG,OAAO,UAAU,cAAc,MAAM;AACjF,IAAa,oBAAoB;AACLine truncated
+98
View File
@@ -0,0 +1,98 @@
import { ColorChannels, Options } from './typedef.js';
/**
* pre process
* @param value - CSS color value
* @param [opt] - options
* @returns value
*/
export declare const preProcess: (value: string, opt?: Options) => string | null;
/**
* convert number to hex string
* @param value - numeric value
* @returns hex string: 00..ff
*/
export declare const numberToHex: (value: number) => string;
/**
* convert color to hex
* @param value - CSS color value
* @param [opt] - options
* @param [opt.alpha] - enable alpha channel
* @returns #rrggbb | #rrggbbaa | null
*/
export declare const colorToHex: (value: string, opt?: Options) => string | null;
/**
* convert color to hsl
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [h, s, l, alpha]
*/
export declare const colorToHsl: (value: string, opt?: Options) => ColorChannels;
/**
* convert color to hwb
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [h, w, b, alpha]
*/
export declare const colorToHwb: (value: string, opt?: Options) => ColorChannels;
/**
* convert color to lab
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [l, a, b, alpha]
*/
export declare const colorToLab: (value: string, opt?: Options) => ColorChannels;
/**
* convert color to lch
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [l, c, h, alpha]
*/
export declare const colorToLch: (value: string, opt?: Options) => ColorChannels;
/**
* convert color to oklab
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [l, a, b, alpha]
*/
export declare const colorToOklab: (value: string, opt?: Options) => ColorChannels;
/**
* convert color to oklch
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [l, c, h, alpha]
*/
export declare const colorToOklch: (value: string, opt?: Options) => ColorChannels;
/**
* convert color to rgb
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [r, g, b, alpha]
*/
export declare const colorToRgb: (value: string, opt?: Options) => ColorChannels;
/**
* convert color to xyz
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [x, y, z, alpha]
*/
export declare const colorToXyz: (value: string, opt?: Options) => ColorChannels;
/**
* convert color to xyz-d50
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [x, y, z, alpha]
*/
export declare const colorToXyzD50: (value: string, opt?: Options) => ColorChannels;
export declare const convert: {
colorToHex: (value: string, opt?: Options) => string | null;
colorToHsl: (value: string, opt?: Options) => ColorChannels;
colorToHwb: (value: string, opt?: Options) => ColorChannels;
colorToLab: (value: string, opt?: Options) => ColorChannels;
colorToLch: (value: string, opt?: Options) => ColorChannels;
colorToOklab: (value: string, opt?: Options) => ColorChannels;
colorToOklch: (value: string, opt?: Options) => ColorChannels;
colorToRgb: (value: string, opt?: Options) => ColorChannels;
colorToXyz: (value: string, opt?: Options) => ColorChannels;
colorToXyzD50: (value: string, opt?: Options) => ColorChannels;
numberToHex: (value: number) => string;
};
+241
View File
@@ -0,0 +1,241 @@
import { createCacheKey, getCache, setCache } from "./cache.js";
import { isString } from "./common.js";
import { SYN_FN_CALC, SYN_FN_REL, SYN_FN_VAR, VAL_COMP } from "./constant.js";
import { convertColorToHsl, convertColorToHwb, convertColorToLab, convertColorToLch, convertColorToOklab, convertColorToOklch, convertColorToRgb, numberToHexString, parseColorFunc, parseColorValue } from "./color.js";
import { resolveRelativeColor } from "./relative-color.js";
import { resolveColor } from "./resolve.js";
import { cssCalc, resolveVar } from "./css-calc-var.js";
//#region src/js/convert.ts
/**
* convert
*/
var NAMESPACE = "convert";
var REG_FN_CALC = new RegExp(SYN_FN_CALC);
var REG_FN_REL = new RegExp(SYN_FN_REL);
var REG_FN_VAR = new RegExp(SYN_FN_VAR);
/**
* pre process
* @param value - CSS color value
* @param [opt] - options
* @returns value
*/
var preProcess = (value, opt = {}) => {
if (!isString(value)) return null;
value = value.trim();
if (!value) return null;
const cacheKey = createCacheKey({
namespace: NAMESPACE,
name: "preProcess",
value
}, opt);
const cachedResult = getCache(cacheKey);
if (cachedResult !== false) return cachedResult.item;
let res = value;
if (REG_FN_VAR.test(value)) {
const resolved = resolveVar(value, opt);
if (isString(resolved)) res = resolved;
else {
setCache(cacheKey, null);
return null;
}
}
if (isString(res)) {
if (REG_FN_REL.test(res)) {
const resolved = resolveRelativeColor(res, opt);
if (isString(resolved)) res = resolved;
else {
setCache(cacheKey, null);
return null;
}
} else if (REG_FN_CALC.test(res)) res = cssCalc(res, opt);
}
if (isString(res)) {
if (res.startsWith("color-mix")) {
const resolvedColorMix = resolveColor(res, {
...opt,
format: VAL_COMP,
nullable: true
});
res = typeof resolvedColorMix === "string" ? resolvedColorMix : null;
}
}
setCache(cacheKey, res);
return res;
};
/**
* converter factory to reduce boilerplate
* @param name - function name for cache
* @param format - color format
* @param convertFn - conversion function
* @returns color converter function
*/
var createColorConverter = (name, format, convertFn) => {
const colorConverterFn = (value, opt = {}) => {
if (!isString(value)) throw new TypeError(`${value} is not a string.`);
const resolved = preProcess(value, opt);
if (resolved === null) return [
0,
0,
0,
0
];
const val = resolved.toLowerCase();
const cacheKey = createCacheKey({
namespace: NAMESPACE,
name,
value: val
}, opt);
const cached = getCache(cacheKey);
if (cached !== false) return cached.item;
const result = convertFn(val, {
...opt,
format
});
setCache(cacheKey, result);
return result;
};
return colorConverterFn;
};
/**
* convert number to hex string
* @param value - numeric value
* @returns hex string: 00..ff
*/
var numberToHex = (value) => numberToHexString(value);
/**
* convert color to hex
* @param value - CSS color value
* @param [opt] - options
* @param [opt.alpha] - enable alpha channel
* @returns #rrggbb | #rrggbbaa | null
*/
var colorToHex = (value, opt = {}) => {
if (!isString(value)) throw new TypeError(`${value} is not a string.`);
const resolved = preProcess(value, opt);
if (resolved === null) return null;
const val = resolved.toLowerCase();
const cacheKey = createCacheKey({
namespace: NAMESPACE,
name: "colorToHex",
value: val
}, opt);
const cached = getCache(cacheKey);
if (cached !== false) return cached.item;
const hex = resolveColor(val, {
...opt,
nullable: true,
format: opt.alpha ? "hexAlpha" : "hex"
});
if (isString(hex)) {
setCache(cacheKey, hex);
return hex;
}
setCache(cacheKey, null);
return null;
};
/**
* convert color to hsl
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [h, s, l, alpha]
*/
var colorToHsl = createColorConverter("colorToHsl", "hsl", convertColorToHsl);
/**
* convert color to hwb
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [h, w, b, alpha]
*/
var colorToHwb = createColorConverter("colorToHwb", "hwb", convertColorToHwb);
/**
* convert color to lab
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [l, a, b, alpha]
*/
var colorToLab = createColorConverter("colorToLab", "lab", convertColorToLab);
/**
* convert color to lch
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [l, c, h, alpha]
*/
var colorToLch = createColorConverter("colorToLch", "lch", convertColorToLch);
/**
* convert color to oklab
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [l, a, b, alpha]
*/
var colorToOklab = createColorConverter("colorToOklab", "oklab", convertColorToOklab);
/**
* convert color to oklch
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [l, c, h, alpha]
*/
var colorToOklch = createColorConverter("colorToOklch", "oklch", convertColorToOklch);
/**
* convert color to rgb
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [r, g, b, alpha]
*/
var colorToRgb = createColorConverter("colorToRgb", "rgb", convertColorToRgb);
/**
* convert color to xyz
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [x, y, z, alpha]
*/
var colorToXyz = (value, opt = {}) => {
if (!isString(value)) throw new TypeError(`${value} is not a string.`);
const resolved = preProcess(value, opt);
if (resolved === null) return [
0,
0,
0,
0
];
const val = resolved.toLowerCase();
const cacheKey = createCacheKey({
namespace: NAMESPACE,
name: "colorToXyz",
value: val
}, opt);
const cached = getCache(cacheKey);
if (cached !== false) return cached.item;
let parsed;
if (val.startsWith("color(")) parsed = parseColorFunc(val, opt);
else parsed = parseColorValue(val, opt);
const [, ...xyz] = parsed;
setCache(cacheKey, xyz);
return xyz;
};
/**
* convert color to xyz-d50
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [x, y, z, alpha]
*/
var colorToXyzD50 = (value, opt = {}) => {
opt.d50 = true;
return colorToXyz(value, opt);
};
var convert = {
colorToHex,
colorToHsl,
colorToHwb,
colorToLab,
colorToLch,
colorToOklab,
colorToOklch,
colorToRgb,
colorToXyz,
colorToXyzD50,
numberToHex
};
//#endregion
export { colorToHex, colorToHsl, colorToHwb, colorToLab, colorToLch, colorToOklab, colorToOklch, colorToRgb, colorToXyz, colorToXyzD50, convert, numberToHex, preProcess };
//# sourceMappingURL=convert.js.map
@@ -0,0 +1 @@
{"version":3,"file":"convert.js","names":[],"sources":["../../../src/js/convert.ts"],"sourcesContent":["/**\n * convert\n */\n\nimport { createCacheKey, getCache, setCache } from './cache';\nimport {\n convertColorToHsl,\n convertColorToHwb,\n convertColorToLab,\n convertColorToLch,\n convertColorToOklab,\n convertColorToOklch,\n convertColorToRgb,\n numberToHexString,\n parseColorFunc,\n parseColorValue\n} from './color';\nimport { isString } from './common';\nimport { cssCalc, resolveVar } from './css-calc-var';\nimport { resolveRelativeColor } from './relative-color';\nimport { resolveColor } from './resolve';\nimport { ColorChannels, ComputedColorChannels, Options } from './typedef';\n\n/* constants */\nimport { SYN_FN_CALC, SYN_FN_REL, SYN_FN_VAR, VAL_COMP } from './constant';\nconst NAMESPACE = 'convert';\n\n/* regexp */\nconst REG_FN_CALC = new RegExp(SYN_FN_CALC);\nconst REG_FN_REL = new RegExp(SYN_FN_REL);\nconst REG_FN_VAR = new RegExp(SYN_FN_VAR);\n\n/**\n * pre process\n * @param value - CSS color value\n * @param [opt] - options\n * @returns value\n */\nexport const preProcess = (value: string, opt: Options = {}): string | null => {\n if (!isString(value)) {\n return null;\n }\n value = value.trim();\n if (!value) {\n return null;\n }\n const cacheKey: string = createCacheKey(\n { namespace: NAMESPACE, name: 'preProcess', value },\n opt\n );\n const cachedResult = getCache(cacheKey);\n if (cachedResult !== false) {\n return cachedResult.item as string | null;\n }\n let res: string | null = value;\n if (REG_FN_VAR.test(value)) {\n const resolved = resolveVar(value, opt);\n if (isString(resolved)) {\n res = resolved;\n } else {\n setCache(cacheKey, null);\n return null;\n }\n }\n if (isString(res)) {\n if (REG_FN_REL.test(res)) {\n const resolved = resolveRelativeColor(res, opt);\n if (isString(resolved)) {\n res = resolved;\n } else {\n setCache(cacheKey, null);\n return null;\n }\n } else if (REG_FN_CALC.test(res)) {\n res = cssCalc(res, opt);\n }\n }\n if (isString(res)) {\n if (res.startsWith('color-mix')) {\n const resolvedColorMix = resolveColor(res, {\n ...opt,\n format: VAL_COMP,\n nullable: true\n });\n res = typeof resolvedColorMix === 'string' ? resolvedColorMix : null;\n }\n }\n setCache(cacheKey, res);\n return res;\n};\n\n/**\n * converter factory to reduce boilerplate\n * @param name - function name for cache\n * @param format - color format\n * @param convertFn - conversion function\n * @returns color converter function\n */\nconst createColorConverter = (\n name: string,\n format: string,\n convertFn: Function\n) => {\n const colorConverterFn = (\n value: string,\n opt: Options = {}\n ): ColorChannels => {\n if (!isString(value)) {\n throw new TypeError(`${value} is not a string.`);\n }\n const resolved = preProcess(value, opt);\n if (resolved === null) {\n return [0, 0, 0, 0];\n }\n const val = resolved.toLowerCase();\n const cacheKey = createCacheKey(\n { namespace: NAMESPACE, name, value: val },\n opt\n );\n const cached = getCache(cacheKey);\n if (cached !== false) {\n return cached.item as ColorChannels;\n }\n const result = convertFn(val, { ...opt, format }) as ColorChannels;\n setCache(cacheKey, result);\n return result;\n };\n return colorConverterFn;\n};\n\n/**\n * convert number to hex string\n * @param value - numeric value\n * @returns hex string: 00..ff\n */\nexport const numberToHex = (value: number): string => numberToHexString(value);\n\n/**\n * convert color to hex\n * @param value - CSS color value\n * @param [opt] - options\n * @param [opt.alpha] - enable alpha channel\n * @returns #rrggbb | #rrggbbaa | null\n */\nexport const colorToHex = (value: string, opt: Options = {}): string | null => {\n if (!isString(value)) {\n throw new TypeError(`${value} is not a string.`);\n }\n const resolved = preProcess(value, opt);\n if (resolved === null) {\n return null;\n }\n const val = resolved.toLowerCase();\n const cacheKey = createCacheKey(\n { namespace: NAMESPACE, name: 'colorToHex', value: val },\n opt\n );\n const cached = getCache(cacheKey);\n if (cached !== false) {\n return cached.item as string | null;\n }\n const hex = resolveColor(val, {\n ...opt,\n nullable: true,\n format: opt.alpha ? 'hexAlpha' : 'hex'\n });\n if (isString(hex)) {\n setCache(cacheKey, hex);\n return hex;\n }\n setCache(cacheKey, null);\n return null;\n};\n\n/**\n * convert color to hsl\n * @param value - CSS color value\n * @param [opt] - options\n * @returns ColorChannels - [h, s, l, alpha]\n */\nexport const colorToHsl = createColorConverter(\n 'colorToHsl',\n 'hsl',\n convertColorToHsl\n);\n\n/**\n * convert color to hwb\n * @param value - CSS color value\n * @param [opt] - options\n * @returnLine truncated
@@ -0,0 +1,122 @@
import { CSSToken } from '@csstools/css-tokenizer';
import { Options } from './typedef.js';
/**
* Calclator
*/
export declare class Calculator {
#private;
/**
* constructor
*/
constructor();
get hasNum(): boolean;
set hasNum(value: boolean);
get numSum(): number[];
get numMul(): number[];
get hasPct(): boolean;
set hasPct(value: boolean);
get pctSum(): number[];
get pctMul(): number[];
get hasDim(): boolean;
set hasDim(value: boolean);
get dimSum(): string[];
get dimSub(): string[];
get dimMul(): string[];
get dimDiv(): string[];
get hasEtc(): boolean;
set hasEtc(value: boolean);
get etcSum(): string[];
get etcSub(): string[];
get etcMul(): string[];
get etcDiv(): string[];
/**
* clear values
* @returns void
*/
clear(): void;
/**
* sort values
* @param values - values
* @returns sorted values
*/
sort(values?: string[]): string[];
/**
* multiply values
* @returns resolved value
*/
multiply(): string;
/**
* sum values
* @returns resolved value
*/
sum(): string;
}
/**
* sort calc values
* @param values - values to sort
* @param [finalize] - finalize values
* @returns sorted values
*/
export declare const sortCalcValues: (values?: (number | string)[], finalize?: boolean) => string;
/**
* sort math function terms
* @param expr - unwrapped calc expression
* @returns sorted expression
*/
export declare const sortMathFnTerms: (expr: string) => string;
/**
* serialize calc
* @param value - CSS value
* @param [opt] - options
* @returns serialized value
*/
export declare const serializeCalc: (value: string, opt?: Options) => string;
/**
* resolve dimension
* @param token - CSS token
* @param [opt] - options
* @returns resolved value
*/
export declare const resolveDimension: (token: CSSToken, opt?: Options) => string | null;
/**
* parse CSS calc() tokens
* @param tokens - CSS tokens
* @param [opt] - options
* @returns parsed tokens
*/
export declare const parseCalcTokens: (tokens: CSSToken[], opt?: Options) => string[];
/**
* CSS calc()
* @param value - CSS value including calc()
* @param [opt] - options
* @returns resolved value
*/
export declare const cssCalc: (value: string, opt?: Options) => string;
/**
* resolve custom property
* @param tokens - CSS tokens
* @param [opt] - options
* @returns result - [tokens, resolvedValue]
*/
export declare function resolveCustomProperty(tokens: CSSToken[], opt?: Options): [CSSToken[], string];
/**
* parse CSS var() tokens
* @param tokens - CSS tokens
* @param [opt] - options
* @returns parsed tokens
*/
export declare function parseVarTokens(tokens: CSSToken[], opt?: Options): string[] | null;
/**
* resolve CSS var()
* @param value - CSS value including var()
* @param [opt] - options
* @returns resolved value
*/
export declare function resolveVar(value: string, opt?: Options): string | null;
/**
* CSS var()
* @param value - CSS value including var()
* @param [opt] - options
* @returns resolved value
*/
export declare const cssVar: (value: string, opt?: Options) => string;
+859
View File
@@ -0,0 +1,859 @@
import { createCacheKey, getCache, setCache } from "./cache.js";
import { isString, isStringOrNumber } from "./common.js";
import { resolveLengthInPixels, roundToPrecision } from "./util.js";
import { ANGLE, LENGTH, NUM, SYN_FN_CALC, SYN_FN_MATH_START, SYN_FN_VAR, SYN_FN_VAR_START } from "./constant.js";
import { isColor } from "./resolve.js";
import { calc } from "@csstools/css-calc";
import { TokenType, tokenize } from "@csstools/css-tokenizer";
//#region src/js/css-calc-var.ts
/**
* css-calc-var
*/
var { CloseParen: PAREN_CLOSE, Comment: COMMENT, Dimension: DIM, EOF, Function: FUNC, Ident: IDENT, OpenParen: PAREN_OPEN, Whitespace: W_SPACE } = TokenType;
var NAMESPACE = "css-calc-var";
var TRIA = 3;
var HEX = 16;
var REG_FN_CALC = new RegExp(SYN_FN_CALC);
var REG_FN_CALC_NUM = new RegExp(`^calc\\((${NUM})\\)$`);
var REG_FN_MATH_START = new RegExp(SYN_FN_MATH_START);
var REG_FN_VAR = new RegExp(SYN_FN_VAR);
var REG_FN_VAR_START = new RegExp(SYN_FN_VAR_START);
var REG_OPERATOR = /\s[*+/-]\s/;
var REG_PAREN_OPEN = /\($/;
var REG_TYPE_DIM = new RegExp(`^(${NUM})(${ANGLE}|${LENGTH})$`);
var REG_TYPE_DIM_PCT = new RegExp(`^(${NUM})(${ANGLE}|${LENGTH}|%)$`);
var REG_TYPE_PCT = new RegExp(`^(${NUM})%$`);
var REG_CSS_WIDE_KEYWORD = /^(?:inherit|initial|revert(?:-layer)?|unset)$/;
/**
* Calclator
*/
var Calculator = class {
#hasNum;
#numSum;
#numMul;
#hasPct;
#pctSum;
#pctMul;
#hasDim;
#dimSum;
#dimSub;
#dimMul;
#dimDiv;
#hasEtc;
#etcSum;
#etcSub;
#etcMul;
#etcDiv;
#calcOpts;
/**
* constructor
*/
constructor() {
this.#hasNum = false;
this.#numSum = [];
this.#numMul = [];
this.#hasPct = false;
this.#pctSum = [];
this.#pctMul = [];
this.#hasDim = false;
this.#dimSum = [];
this.#dimSub = [];
this.#dimMul = [];
this.#dimDiv = [];
this.#hasEtc = false;
this.#etcSum = [];
this.#etcSub = [];
this.#etcMul = [];
this.#etcDiv = [];
this.#calcOpts = { toCanonicalUnits: true };
}
get hasNum() {
return this.#hasNum;
}
set hasNum(value) {
this.#hasNum = !!value;
}
get numSum() {
return this.#numSum;
}
get numMul() {
return this.#numMul;
}
get hasPct() {
return this.#hasPct;
}
set hasPct(value) {
this.#hasPct = !!value;
}
get pctSum() {
return this.#pctSum;
}
get pctMul() {
return this.#pctMul;
}
get hasDim() {
return this.#hasDim;
}
set hasDim(value) {
this.#hasDim = !!value;
}
get dimSum() {
return this.#dimSum;
}
get dimSub() {
return this.#dimSub;
}
get dimMul() {
return this.#dimMul;
}
get dimDiv() {
return this.#dimDiv;
}
get hasEtc() {
return this.#hasEtc;
}
set hasEtc(value) {
this.#hasEtc = !!value;
}
get etcSum() {
return this.#etcSum;
}
get etcSub() {
return this.#etcSub;
}
get etcMul() {
return this.#etcMul;
}
get etcDiv() {
return this.#etcDiv;
}
/**
* clear values
* @returns void
*/
clear() {
this.#hasNum = false;
this.#numSum.length = 0;
this.#numMul.length = 0;
this.#hasPct = false;
this.#pctSum.length = 0;
this.#pctMul.length = 0;
this.#hasDim = false;
this.#dimSum.length = 0;
this.#dimSub.length = 0;
this.#dimMul.length = 0;
this.#dimDiv.length = 0;
this.#hasEtc = false;
this.#etcSum.length = 0;
this.#etcSub.length = 0;
this.#etcMul.length = 0;
this.#etcDiv.length = 0;
}
/**
* sort values
* @param values - values
* @returns sorted values
*/
sort(values = []) {
const arr = [...values];
if (arr.length > 1) arr.sort((a, b) => {
let res;
if (REG_TYPE_DIM_PCT.test(a) && REG_TYPE_DIM_PCT.test(b)) {
const [, valA, unitA] = a.match(REG_TYPE_DIM_PCT);
const [, valB, unitB] = b.match(REG_TYPE_DIM_PCT);
if (unitA === unitB) {
if (Number(valA) === Number(valB)) res = 0;
else if (Number(valA) > Number(valB)) res = 1;
else res = -1;
} else if (unitA > unitB) res = 1;
else res = -1;
} else if (a === b) res = 0;
else if (a > b) res = 1;
else res = -1;
return res;
});
return arr;
}
/**
* multiply values
* @returns resolved value
*/
multiply() {
const value = [];
let num;
if (this.#hasNum) {
num = 1;
for (const i of this.#numMul) {
num *= i;
if (num === 0 || !Number.isFinite(num) || Number.isNaN(num)) break;
}
if (!this.#hasPct && !this.#hasDim && !this.hasEtc) {
if (Number.isFinite(num)) num = roundToPrecision(num, HEX);
value.push(num);
}
}
if (this.#hasPct) {
if (typeof num !== "number") num = 1;
for (const i of this.#pctMul) {
num *= i;
if (num === 0 || !Number.isFinite(num) || Number.isNaN(num)) break;
}
if (Number.isFinite(num)) num = `${roundToPrecision(num, HEX)}%`;
if (!this.#hasDim && !this.hasEtc) value.push(num);
}
if (this.#hasDim) {
let dim = "";
let mul = "";
let div = "";
if (this.#dimMul.length) {
if (this.#dimMul.length === 1) [mul] = this.#dimMul;
else mul = `${this.sort(this.#dimMul).join(" * ")}`;
}
if (this.#dimDiv.length) {
if (this.#dimDiv.length === 1) [div] = this.#dimDiv;
else div = `${this.sort(this.#dimDiv).join(" * ")}`;
}
if (Number.isFinite(num)) {
if (mul) {
if (div) {
if (div.includes("*")) dim = calc(`calc(${num} * ${mul} / (${div}))`, this.#calcOpts);
else dim = calc(`calc(${num} * ${mul} / ${div})`, this.#calcOpts);
} else dim = calc(`calc(${num} * ${mul})`, this.#calcOpts);
} else if (div.includes("*")) dim = calc(`calc(${num} / (${div}))`, this.#calcOpts);
else dim = calc(`calc(${num} / ${div})`, this.#calcOpts);
value.push(dim.replace(/^calc/, ""));
} else {
if (!value.length && num !== void 0) value.push(num);
if (mul) {
if (div) {
if (div.includes("*")) dim = calc(`calc(${mul} / (${div}))`, this.#calcOpts);
else dim = calc(`calc(${mul} / ${div})`, this.#calcOpts);
} else dim = calc(`calc(${mul})`, this.#calcOpts);
if (value.length) value.push("*", dim.replace(/^calc/, ""));
else value.push(dim.replace(/^calc/, ""));
} else {
dim = calc(`calc(${div})`, this.#calcOpts);
if (value.length) value.push("/", dim.replace(/^calc/, ""));
else value.push("1", "/", dim.replace(/^calc/, ""));
}
}
}
if (this.#hasEtc) {
if (this.#etcMul.length) {
if (!value.length && num !== void 0) value.push(num);
const mul = this.sort(this.#etcMul).join(" * ");
if (value.length) value.push(`* ${mul}`);
else value.push(`${mul}`);
}
if (this.#etcDiv.length) {
const div = this.sort(this.#etcDiv).join(" * ");
if (div.includes("*")) {
if (value.length) value.push(`/ (${div})`);
else value.push(`1 / (${div})`);
} else if (value.length) value.push(`/ ${div}`);
else value.push(`1 / ${div}`);
}
}
if (value.length) return value.join(" ");
return "";
}
/**
* sum values
* @returns resolved value
*/
sum() {
const value = [];
if (this.#hasNum) {
let num = 0;
for (const i of this.#numSum) {
num += i;
if (!Number.isFinite(num) || Number.isNaN(num)) break;
}
value.push(num);
}
if (this.#hasPct) {
let num = 0;
for (const i of this.#pctSum) {
num += i;
if (!Number.isFinite(num)) break;
}
if (Number.isFinite(num)) num = `${num}%`;
if (value.length) value.push(`+ ${num}`);
else value.push(num);
}
if (this.#hasDim) {
let dim, sum, sub;
if (this.#dimSum.length) sum = this.sort(this.#dimSum).join(" + ");
if (this.#dimSub.length) sub = this.sort(this.#dimSub).join(" + ");
if (sum) {
if (sub) {
if (sub.includes("-")) dim = calc(`calc(${sum} - (${sub}))`, this.#calcOpts);
else dim = calc(`calc(${sum} - ${sub})`, this.#calcOpts);
} else dim = calc(`calc(${sum})`, this.#calcOpts);
} else dim = calc(`calc(-1 * (${sub}))`, this.#calcOpts);
if (value.length) value.push("+", dim.replace(/^calc/, ""));
else value.push(dim.replace(/^calc/, ""));
}
if (this.#hasEtc) {
if (this.#etcSum.length) {
const sum = this.sort(this.#etcSum).map((item) => {
let res;
if (REG_OPERATOR.test(item) && !item.startsWith("(") && !item.endsWith(")")) res = `(${item})`;
else res = item;
return res;
}).join(" + ");
if (value.length) {
if (this.#etcSum.length > 1) value.push(`+ (${sum})`);
else value.push(`+ ${sum}`);
} else value.push(`${sum}`);
}
if (this.#etcSub.length) {
const sub = this.sort(this.#etcSub).map((item) => {
let res;
if (REG_OPERATOR.test(item) && !item.startsWith("(") && !item.endsWith(")")) res = `(${item})`;
else res = item;
return res;
}).join(" + ");
if (value.length) {
if (this.#etcSub.length > 1) value.push(`- (${sub})`);
else value.push(`- ${sub}`);
} else if (this.#etcSub.length > 1) value.push(`-1 * (${sub})`);
else value.push(`-1 * ${sub}`);
}
}
if (value.length) return value.join(" ");
return "";
}
};
/**
* sort calc values
* @param values - values to sort
* @param [finalize] - finalize values
* @returns sorted values
*/
var sortCalcValues = (values = [], finalize = false) => {
if (values.length < TRIA) throw new Error(`Unexpected array length ${values.length}.`);
const start = values.shift();
if (!isString(start) || !start.endsWith("(")) throw new Error(`Unexpected token ${start}.`);
const end = values.pop();
if (end !== ")") throw new Error(`Unexpected token ${end}.`);
if (values.length === 1) {
const [value] = values;
if (!isStringOrNumber(value)) throw new Error(`Unexpected token ${value}.`);
return `${start}${value}${end}`;
}
const sortedValues = [];
const cal = new Calculator();
let operator = "";
const l = values.length;
let hasAddSub = false;
for (let i = 0; i < l; i++) {
const value = values[i];
if (!isStringOrNumber(value)) throw new Error(`Unexpected token ${value}.`);
if (value === "*" || value === "/") operator = value;
else if (value === "+" || value === "-") {
const sortedValue = cal.multiply();
if (sortedValue) sortedValues.push(sortedValue, value);
hasAddSub = true;
cal.clear();
operator = "";
} else {
const numValue = Number(value);
const strValue = `${value}`;
switch (operator) {
case "/":
if (Number.isFinite(numValue)) {
cal.hasNum = true;
cal.numMul.push(1 / numValue);
} else if (REG_TYPE_PCT.test(strValue)) {
const [, val] = strValue.match(REG_TYPE_PCT);
cal.hasPct = true;
cal.pctMul.push(1e4 / Number(val));
} else if (REG_TYPE_DIM.test(strValue)) {
cal.hasDim = true;
cal.dimDiv.push(strValue);
} else {
cal.hasEtc = true;
cal.etcDiv.push(strValue);
}
break;
default: if (Number.isFinite(numValue)) {
cal.hasNum = true;
cal.numMul.push(numValue);
} else if (REG_TYPE_PCT.test(strValue)) {
const [, val] = strValue.match(REG_TYPE_PCT);
cal.hasPct = true;
cal.pctMul.push(Number(val));
} else if (REG_TYPE_DIM.test(strValue)) {
cal.hasDim = true;
cal.dimMul.push(strValue);
} else {
cal.hasEtc = true;
cal.etcMul.push(strValue);
}
}
}
if (i === l - 1) {
const sortedValue = cal.multiply();
if (sortedValue) sortedValues.push(sortedValue);
cal.clear();
operator = "";
}
}
let resolvedValue = "";
if (finalize && hasAddSub) {
const finalizedValues = [];
cal.clear();
operator = "";
const l = sortedValues.length;
for (let i = 0; i < l; i++) {
const value = sortedValues[i];
if (isStringOrNumber(value)) {
if (value === "+" || value === "-") operator = value;
else {
const numValue = Number(value);
const strValue = `${value}`;
switch (operator) {
case "-":
if (Number.isFinite(numValue)) {
cal.hasNum = true;
cal.numSum.push(-1 * numValue);
} else if (REG_TYPE_PCT.test(strValue)) {
const [, val] = strValue.match(REG_TYPE_PCT);
cal.hasPct = true;
cal.pctSum.push(-1 * Number(val));
} else if (REG_TYPE_DIM.test(strValue)) {
cal.hasDim = true;
cal.dimSub.push(strValue);
} else {
cal.hasEtc = true;
cal.etcSub.push(strValue);
}
break;
default: if (Number.isFinite(numValue)) {
cal.hasNum = true;
cal.numSum.push(numValue);
} else if (REG_TYPE_PCT.test(strValue)) {
const [, val] = strValue.match(REG_TYPE_PCT);
cal.hasPct = true;
cal.pctSum.push(Number(val));
} else if (REG_TYPE_DIM.test(strValue)) {
cal.hasDim = true;
cal.dimSum.push(strValue);
} else {
cal.hasEtc = true;
cal.etcSum.push(strValue);
}
}
}
}
if (i === l - 1) {
const sortedValue = cal.sum();
if (sortedValue) finalizedValues.push(sortedValue);
cal.clear();
operator = "";
}
}
resolvedValue = finalizedValues.join(" ").replace(/\+\s-/g, "- ");
} else resolvedValue = sortedValues.join(" ").replace(/\+\s-/g, "- ");
if (resolvedValue.startsWith("(") && resolvedValue.endsWith(")") && resolvedValue.lastIndexOf("(") === 0 && resolvedValue.indexOf(")") === resolvedValue.length - 1) resolvedValue = resolvedValue.substring(1, resolvedValue.length - 1);
return `${start}${resolvedValue}${end}`;
};
/**
* sort math function terms
* @param expr - unwrapped calc expression
* @returns sorted expression
*/
var sortMathFnTerms = (expr) => {
const tokens = tokenize({ css: expr });
const terms = [];
let currentTerm = "";
let currentSign = "";
let depth = 0;
for (const token of tokens) {
const [type, value] = token;
if (type === PAREN_OPEN || type === FUNC) depth++;
else if (type === PAREN_CLOSE) depth--;
if (depth === 0 && (value === "+" || value === "-")) {
if (currentTerm.trim()) terms.push((currentSign === "-" ? "-" : "") + currentTerm.trim());
currentTerm = "";
currentSign = value;
continue;
}
currentTerm += value;
}
if (currentTerm.trim()) terms.push((currentSign === "-" ? "-" : "") + currentTerm.trim());
terms.sort((a, b) => {
const reg = /^(-?(?:\d+(?:\.\d+)?|\.\d+))([a-z%]*)$/i;
const matchA = a.match(reg);
const matchB = b.match(reg);
if (matchA && matchB) {
const numA = Number(matchA[1] || "0");
const numB = Number(matchB[1] || "0");
const unitA = matchA[2] || "";
const unitB = matchB[2] || "";
if (unitA === unitB) return numA - numB;
return unitA > unitB ? 1 : -1;
}
if (a === b) return 0;
return a > b ? 1 : -1;
});
const firstTerm = terms[0];
if (firstTerm === void 0) return expr;
let res = firstTerm;
for (let i = 1; i < terms.length; i++) {
const term = terms[i];
if (term !== void 0) {
if (term.startsWith("-")) res += " - " + term.substring(1);
else res += " + " + term;
}
}
return res;
};
/**
* resolve AST node
* @param node - AST node
* @param isRoot - is root node
* @returns resolved value
*/
var resolveNode = (node, isRoot) => {
const firstItemRaw = node[0];
const isCommaMathFnNode = isString(firstItemRaw) && REG_FN_MATH_START.test(firstItemRaw);
const flatItems = [];
for (const item of node) if (Array.isArray(item)) flatItems.push(resolveNode(item, isCommaMathFnNode));
else flatItems.push(item);
const hasComma = flatItems.includes(",");
const firstItem = flatItems[0] || "";
const isCommaMathFn = isString(firstItem) && REG_FN_MATH_START.test(firstItem);
if (hasComma && isCommaMathFn && flatItems[flatItems.length - 1] === ")") {
const fnName = flatItems.shift();
const fnEnd = flatItems.pop();
const args = [];
let currentArg = [];
for (const item of flatItems) if (item === ",") {
args.push(currentArg);
currentArg = [];
} else currentArg.push(item);
if (currentArg.length) args.push(currentArg);
return `${fnName}${args.map((argNodes) => {
if (argNodes.length >= TRIA) {
const sorted = sortCalcValues([
"calc(",
...argNodes,
")"
], true);
return sortMathFnTerms(sorted.substring(5, sorted.length - 1));
} else if (argNodes.length === 1) {
const item = argNodes[0];
if (isString(item) && item.startsWith("calc(") && item.endsWith(")")) return sortMathFnTerms(item.substring(5, item.length - 1));
}
return argNodes.join("");
}).join(", ")}${fnEnd}`;
}
if (isRoot) {
if (!hasComma) {
if (flatItems.length >= TRIA) return sortCalcValues(flatItems, true);
const first = flatItems[0] || "";
if (isString(first) && first.endsWith("(")) {
const joined = flatItems.join("");
return joined.startsWith("calc(") || /^[a-z-]+\(/.test(joined) ? joined : `calc(${joined})`;
}
return isString(first) && (first.startsWith("calc(") || /^[a-z-]+\(/.test(first)) ? first : `calc(${first})`;
}
return flatItems.join("").replace(/,\s*/g, ", ");
}
if (flatItems.length >= TRIA && !hasComma) {
let serialized = sortCalcValues(flatItems, false);
if (REG_FN_VAR_START.test(serialized)) serialized = calc(serialized, { toCanonicalUnits: true });
return serialized;
}
return flatItems.join("").replace(/,\s*/g, ", ");
};
/**
* serialize calc
* @param value - CSS value
* @param [opt] - options
* @returns serialized value
*/
var serializeCalc = (value, opt = {}) => {
const { format = "" } = opt;
if (isString(value)) {
if (!REG_FN_VAR_START.test(value) || format !== "specifiedValue") return value;
value = value.toLowerCase().trim();
} else throw new TypeError(`${value} is not a string.`);
const cacheKey = createCacheKey({
namespace: NAMESPACE,
name: "serializeCalc",
value
}, opt);
const cachedResult = getCache(cacheKey);
if (cachedResult !== false) return cachedResult.item;
const items = tokenize({ css: value }).map((token) => {
const [type, val] = token;
let res = "";
if (type !== W_SPACE && type !== COMMENT) res = val;
return res;
}).filter((v) => v);
const stack = [[]];
for (const item of items) if (REG_PAREN_OPEN.test(item)) {
const newNode = [item];
const parent = stack[stack.length - 1];
if (parent) parent.push(newNode);
stack.push(newNode);
} else if (item === ")") {
if (stack.length > 1) {
const currentLevel = stack.pop();
if (currentLevel) currentLevel.push(item);
} else {
const root = stack[0];
if (root) root.push(item);
}
} else {
const parent = stack[stack.length - 1];
if (parent) parent.push(item);
}
let serializedCalc = "";
const rootItems = stack[0];
if (rootItems) {
if (rootItems.length === 1 && Array.isArray(rootItems[0])) serializedCalc = resolveNode(rootItems[0], true);
else serializedCalc = resolveNode(rootItems, true);
}
setCache(cacheKey, serializedCalc);
return serializedCalc;
};
/**
* resolve dimension
* @param token - CSS token
* @param [opt] - options
* @returns resolved value
*/
var resolveDimension = (token, opt = {}) => {
if (!Array.isArray(token)) throw new TypeError(`${token} is not an array.`);
const [, , , , detail = {}] = token;
const { unit, value } = detail;
if (unit === "px") return `${value}${unit}`;
const pixelValue = resolveLengthInPixels(Number(value), unit, opt);
if (Number.isFinite(pixelValue)) return `${roundToPrecision(pixelValue, HEX)}px`;
return null;
};
/**
* parse CSS calc() tokens
* @param tokens - CSS tokens
* @param [opt] - options
* @returns parsed tokens
*/
var parseCalcTokens = (tokens, opt = {}) => {
if (!Array.isArray(tokens)) throw new TypeError(`${tokens} is not an array.`);
const { format = "" } = opt;
const mathFunc = /* @__PURE__ */ new Set();
let nest = 0;
const res = [];
for (const token of tokens) {
if (!Array.isArray(token)) throw new TypeError(`${token} is not an array.`);
const [type = "", value = ""] = token;
switch (type) {
case DIM:
if (format === "specifiedValue" && !mathFunc.has(nest)) res.push(value);
else {
const resolvedValue = resolveDimension(token, opt);
if (isString(resolvedValue)) res.push(resolvedValue);
else res.push(value);
}
break;
case FUNC:
case PAREN_OPEN:
res.push(value);
nest++;
if (REG_FN_MATH_START.test(value)) mathFunc.add(nest);
break;
case PAREN_CLOSE:
if (res.length) {
if (res[res.length - 1] === " ") res.splice(-1, 1, value);
else res.push(value);
} else res.push(value);
if (mathFunc.has(nest)) mathFunc.delete(nest);
nest--;
break;
case W_SPACE:
if (res.length) {
const lastValue = res[res.length - 1];
if (isString(lastValue) && !lastValue.endsWith("(") && lastValue !== " ") res.push(value);
}
break;
default: if (type !== COMMENT && type !== EOF) res.push(value);
}
}
return res;
};
/**
* CSS calc()
* @param value - CSS value including calc()
* @param [opt] - options
* @returns resolved value
*/
var cssCalc = (value, opt = {}) => {
const { format = "" } = opt;
if (isString(value)) {
if (REG_FN_VAR.test(value)) {
if (format === "specifiedValue") return value;
else {
const resolvedValue = resolveVar(value, opt);
if (isString(resolvedValue)) return resolvedValue;
else return "";
}
} else if (!REG_FN_CALC.test(value)) return value;
value = value.toLowerCase().trim();
} else throw new TypeError(`${value} is not a string.`);
const cacheKey = createCacheKey({
namespace: NAMESPACE,
name: "cssCalc",
value
}, opt);
const cachedResult = getCache(cacheKey);
if (cachedResult !== false) return cachedResult.item;
const values = parseCalcTokens(tokenize({ css: value }), opt);
let resolvedValue = calc(values.join(""), { toCanonicalUnits: true });
if (REG_FN_VAR_START.test(value)) {
if (REG_TYPE_DIM_PCT.test(resolvedValue)) {
const [, val, unit] = resolvedValue.match(REG_TYPE_DIM_PCT);
resolvedValue = `${roundToPrecision(Number(val), HEX)}${unit}`;
}
if (resolvedValue && !REG_FN_VAR_START.test(resolvedValue) && format === "specifiedValue") resolvedValue = `calc(${resolvedValue})`;
}
if (format === "specifiedValue") {
if (/\s[-+*/]\s/.test(resolvedValue) && !resolvedValue.includes("NaN")) resolvedValue = serializeCalc(resolvedValue, opt);
else if (REG_FN_CALC_NUM.test(resolvedValue)) {
const [, val] = resolvedValue.match(REG_FN_CALC_NUM);
resolvedValue = `calc(${roundToPrecision(Number(val), HEX)})`;
}
}
setCache(cacheKey, resolvedValue);
return resolvedValue;
};
/**
* resolve custom property
* @param tokens - CSS tokens
* @param [opt] - options
* @returns result - [tokens, resolvedValue]
*/
function resolveCustomProperty(tokens, opt = {}) {
if (!Array.isArray(tokens)) throw new TypeError(`${tokens} is not an array.`);
const { customProperty = {} } = opt;
const items = [];
while (tokens.length) {
const token = tokens.shift();
if (!token) break;
if (!Array.isArray(token)) throw new TypeError(`${token} is not an array.`);
const [type, value] = token;
if (type === PAREN_CLOSE) break;
if (value === "var(") {
const [, item] = resolveCustomProperty(tokens, opt);
if (item) items.push(item);
} else if (type === IDENT) {
if (value.startsWith("--")) {
let item;
if (Object.hasOwn(customProperty, value)) item = customProperty[value];
else if (typeof customProperty.callback === "function") item = customProperty.callback(value);
if (item) items.push(item);
} else if (value) items.push(value);
}
}
let resolveAsColor = false;
if (items.length > 1) resolveAsColor = isColor(items[items.length - 1]);
let resolvedValue = "";
for (let item of items) {
item = item.trim();
if (REG_FN_VAR.test(item)) {
const resolvedItem = resolveVar(item, opt);
if (isString(resolvedItem)) {
if (!resolveAsColor || isColor(resolvedItem)) resolvedValue = resolvedItem;
}
} else if (REG_FN_CALC.test(item)) {
item = cssCalc(item, opt);
if (!resolveAsColor || isColor(item)) resolvedValue = item;
} else if (item && !REG_CSS_WIDE_KEYWORD.test(item)) {
if (!resolveAsColor || isColor(item)) resolvedValue = item;
}
if (resolvedValue) break;
}
return [tokens, resolvedValue];
}
/**
* parse CSS var() tokens
* @param tokens - CSS tokens
* @param [opt] - options
* @returns parsed tokens
*/
function parseVarTokens(tokens, opt = {}) {
const res = [];
while (tokens.length) {
const token = tokens.shift();
if (!token) break;
const [type = "", value = ""] = token;
if (value === "var(") {
const [, resolvedValue] = resolveCustomProperty(tokens, opt);
if (!resolvedValue) return null;
res.push(resolvedValue);
} else switch (type) {
case PAREN_CLOSE:
if (res.length) {
if (res[res.length - 1] === " ") res[res.length - 1] = value;
else res.push(value);
} else res.push(value);
break;
case W_SPACE:
if (res.length) {
const lastValue = res[res.length - 1];
if (isString(lastValue) && !lastValue.endsWith("(") && lastValue !== " ") res.push(value);
}
break;
default: if (type !== COMMENT && type !== EOF) res.push(value);
}
}
return res;
}
/**
* resolve CSS var()
* @param value - CSS value including var()
* @param [opt] - options
* @returns resolved value
*/
function resolveVar(value, opt = {}) {
const { format = "" } = opt;
if (isString(value)) {
if (!REG_FN_VAR.test(value) || format === "specifiedValue") return value;
value = value.trim();
} else throw new TypeError(`${value} is not a string.`);
const cacheKey = createCacheKey({
namespace: NAMESPACE,
name: "resolveVar",
value
}, opt);
const cachedResult = getCache(cacheKey);
if (cachedResult !== false) return cachedResult.item;
const values = parseVarTokens(tokenize({ css: value }), opt);
if (Array.isArray(values)) {
let color = values.join("");
if (REG_FN_CALC.test(color)) color = cssCalc(color, opt);
setCache(cacheKey, color);
return color;
} else {
setCache(cacheKey, null);
return null;
}
}
/**
* CSS var()
* @param value - CSS value including var()
* @param [opt] - options
* @returns resolved value
*/
var cssVar = (value, opt = {}) => {
const resolvedValue = resolveVar(value, opt);
if (isString(resolvedValue)) return resolvedValue;
return "";
};
//#endregion
export { Calculator, cssCalc, cssVar, parseCalcTokens, parseVarTokens, resolveCustomProperty, resolveDimension, resolveVar, serializeCalc, sortCalcValues, sortMathFnTerms };
//# sourceMappingURL=css-calc-var.js.map
@@ -0,0 +1 @@
{"version":3,"file":"css-calc-var.js","names":["#hasNum","#numSum","#numMul","#hasPct","#pctSum","#pctMul","#hasDim","#dimSum","#dimSub","#dimMul","#dimDiv","#hasEtc","#etcSum","#etcSub","#etcMul","#etcDiv","#calcOpts"],"sources":["../../../src/js/css-calc-var.ts"],"sourcesContent":["/**\n * css-calc-var\n */\n\nimport { calc, conversionOptions as CalcOptions } from '@csstools/css-calc';\nimport { CSSToken, TokenType, tokenize } from '@csstools/css-tokenizer';\nimport { createCacheKey, getCache, setCache } from './cache';\nimport { isString, isStringOrNumber } from './common';\nimport { isColor } from './resolve';\nimport { resolveLengthInPixels, roundToPrecision } from './util';\nimport { MatchedRegExp, Options } from './typedef';\n\n/* constants */\nimport {\n ANGLE,\n FN_VAR,\n LENGTH,\n NUM,\n SYN_FN_CALC,\n SYN_FN_MATH_START,\n SYN_FN_VAR,\n SYN_FN_VAR_START,\n VAL_SPEC\n} from './constant';\n\nconst {\n CloseParen: PAREN_CLOSE,\n Comment: COMMENT,\n Dimension: DIM,\n EOF,\n Function: FUNC,\n Ident: IDENT,\n OpenParen: PAREN_OPEN,\n Whitespace: W_SPACE\n} = TokenType;\nconst NAMESPACE = 'css-calc-var';\n\n/* numeric constants */\nconst TRIA = 3;\nconst HEX = 16;\nconst MAX_PCT = 100;\n\n/* regexp */\nconst REG_FN_CALC = new RegExp(SYN_FN_CALC);\nconst REG_FN_CALC_NUM = new RegExp(`^calc\\\\((${NUM})\\\\)$`);\nconst REG_FN_MATH_START = new RegExp(SYN_FN_MATH_START);\nconst REG_FN_VAR = new RegExp(SYN_FN_VAR);\nconst REG_FN_VAR_START = new RegExp(SYN_FN_VAR_START);\nconst REG_OPERATOR = /\\s[*+/-]\\s/;\nconst REG_PAREN_OPEN = /\\($/;\nconst REG_TYPE_DIM = new RegExp(`^(${NUM})(${ANGLE}|${LENGTH})$`);\nconst REG_TYPE_DIM_PCT = new RegExp(`^(${NUM})(${ANGLE}|${LENGTH}|%)$`);\nconst REG_TYPE_PCT = new RegExp(`^(${NUM})%$`);\nconst REG_CSS_WIDE_KEYWORD = /^(?:inherit|initial|revert(?:-layer)?|unset)$/;\n\n/**\n * @type CalcASTNode - AST node for calc()\n */\ntype CalcASTNode = string | CalcASTNode[];\n\n/**\n * Calclator\n */\nexport class Calculator {\n /* private */\n // number\n #hasNum: boolean;\n #numSum: number[];\n #numMul: number[];\n // percentage\n #hasPct: boolean;\n #pctSum: number[];\n #pctMul: number[];\n // dimension\n #hasDim: boolean;\n #dimSum: string[];\n #dimSub: string[];\n #dimMul: string[];\n #dimDiv: string[];\n // et cetra\n #hasEtc: boolean;\n #etcSum: string[];\n #etcSub: string[];\n #etcMul: string[];\n #etcDiv: string[];\n // calc options\n #calcOpts: CalcOptions;\n\n /**\n * constructor\n */\n constructor() {\n // number\n this.#hasNum = false;\n this.#numSum = [];\n this.#numMul = [];\n // percentage\n this.#hasPct = false;\n this.#pctSum = [];\n this.#pctMul = [];\n // dimension\n this.#hasDim = false;\n this.#dimSum = [];\n this.#dimSub = [];\n this.#dimMul = [];\n this.#dimDiv = [];\n // et cetra\n this.#hasEtc = false;\n this.#etcSum = [];\n this.#etcSub = [];\n this.#etcMul = [];\n this.#etcDiv = [];\n // calc options\n this.#calcOpts = {\n toCanonicalUnits: true\n };\n }\n\n get hasNum() {\n return this.#hasNum;\n }\n\n set hasNum(value: boolean) {\n this.#hasNum = !!value;\n }\n\n get numSum() {\n return this.#numSum;\n }\n\n get numMul() {\n return this.#numMul;\n }\n\n get hasPct() {\n return this.#hasPct;\n }\n\n set hasPct(value: boolean) {\n this.#hasPct = !!value;\n }\n\n get pctSum() {\n return this.#pctSum;\n }\n\n get pctMul() {\n return this.#pctMul;\n }\n\n get hasDim() {\n return this.#hasDim;\n }\n\n set hasDim(value: boolean) {\n this.#hasDim = !!value;\n }\n\n get dimSum() {\n return this.#dimSum;\n }\n\n get dimSub() {\n return this.#dimSub;\n }\n\n get dimMul() {\n return this.#dimMul;\n }\n\n get dimDiv() {\n return this.#dimDiv;\n }\n\n get hasEtc() {\n return this.#hasEtc;\n }\n\n set hasEtc(value: boolean) {\n this.#hasEtc = !!value;\n }\n\n get etcSum() {\n return this.#etcSum;\n }\n\n get etcSub() {\n return this.#etcSub;\n }\n\n get etcMul() {\n return this.#etcMul;\n }\n\n get etcDiv() {\n return this.#etcDiv;\n }\n\n /**\n * clear values\n * @returns void\n */\n clear() {\n // number\n this.#hasNum = false;\n this.#numSum.length = 0;\n this.#numMul.length = 0;\n // percentage\n this.#hasPct = false;\n this.#pctSum.length = 0;\n this.#pctMul.length = 0;\n // dimension\n this.#hasDim = false;\n this.#dimSum.length = 0;\n this.#dimSub.length = 0;\n this.#dimMul.length = 0;\n this.#dimDiv.length = 0;\n // et cetra\n this.#hasEtc = false;\n this.#etcSum.length = 0;\n this.#etcSub.length = 0;\n this.#etcMul.length = 0;\n this.#etcDiv.length = 0;\n }\n\n /**\n * sort values\n * @param values - values\n * @returns sorted values\n */\n sort(values: string[] = []): string[] {\n const arr = [...values];\n if (arr.length > 1) {\n arr.sort((a, b) => {\Line truncated
@@ -0,0 +1,79 @@
import { Options } from './typedef.js';
/**
* @type ColorStopList - list of color stops
*/
type ColorStopList = [string, string, ...string[]];
/**
* @typedef ValidateGradientLine - validate gradient line
* @property line - gradient line
* @property valid - result
*/
interface ValidateGradientLine {
line: string;
valid: boolean;
}
/**
* @typedef ValidateColorStops - validate color stops
* @property colorStops - list of color stops
* @property valid - result
*/
interface ValidateColorStops {
colorStops: string[];
valid: boolean;
}
/**
* @typedef Gradient - parsed CSS gradient
* @property value - input value
* @property type - gradient type
* @property [gradientLine] - gradient line
* @property colorStopList - list of color stops
*/
interface Gradient {
value: string;
type: string;
gradientLine?: string;
colorStopList: ColorStopList;
}
/**
* get gradient type
* @param value - gradient value
* @returns gradient type
*/
export declare const getGradientType: (value: string) => string;
/**
* validate gradient line
* @param value - gradient line value
* @param type - gradient type
* @returns result
*/
export declare const validateGradientLine: (value: string, type: string) => ValidateGradientLine;
/**
* validate color stop list
* @param list
* @param type
* @param [opt]
* @returns result
*/
export declare const validateColorStopList: (list: string[], type: string, opt?: Options) => ValidateColorStops;
/**
* parse CSS gradient
* @param value - gradient value
* @param [opt] - options
* @returns parsed result
*/
export declare const parseGradient: (value: string, opt?: Options) => Gradient | null;
/**
* resolve CSS gradient
* @param value - CSS value
* @param [opt] - options
* @returns result
*/
export declare const resolveGradient: (value: string, opt?: Options) => string;
/**
* is CSS gradient
* @param value - CSS value
* @param [opt] - options
* @returns result
*/
export declare const isGradient: (value: string, opt?: Options) => boolean;
export {};
+289
View File
@@ -0,0 +1,289 @@
import { createCacheKey, getCache, setCache } from "./cache.js";
import { isString } from "./common.js";
import { splitValue } from "./util.js";
import { ANGLE, CS_HUE, CS_RECT, LENGTH, NUM, NUM_POSITIVE, PCT, VAL_COMP } from "./constant.js";
import { isColor, resolveColor } from "./resolve.js";
//#region src/js/css-gradient.ts
/**
* css-gradient
*/
var NAMESPACE = "css-gradient";
var DIM_ANGLE = `${NUM}(?:${ANGLE})`;
var DIM_ANGLE_PCT = `${DIM_ANGLE}|${PCT}`;
var DIM_LEN_PCT = `${`${NUM}(?:${LENGTH})|0`}|${PCT}`;
var DIM_LEN_PCT_POSI = `${NUM_POSITIVE}(?:${LENGTH}|%)|0`;
var DIM_LEN_POSI = `${NUM_POSITIVE}(?:${LENGTH})|0`;
var CTR = "center";
var L_R = "left|right";
var T_B = "top|bottom";
var S_E = "start|end";
var AXIS_X = `${L_R}|x-(?:${S_E})`;
var AXIS_Y = `${T_B}|y-(?:${S_E})`;
var BLOCK = `block-(?:${S_E})`;
var INLINE = `inline-(?:${S_E})`;
var POS_1 = `${CTR}|${AXIS_X}|${AXIS_Y}|${BLOCK}|${INLINE}|${DIM_LEN_PCT}`;
var POS_2 = [
`(?:${CTR}|${AXIS_X})\\s+(?:${CTR}|${AXIS_Y})`,
`(?:${CTR}|${AXIS_Y})\\s+(?:${CTR}|${AXIS_X})`,
`(?:${CTR}|${AXIS_X}|${DIM_LEN_PCT})\\s+(?:${CTR}|${AXIS_Y}|${DIM_LEN_PCT})`,
`(?:${CTR}|${BLOCK})\\s+(?:${CTR}|${INLINE})`,
`(?:${CTR}|${INLINE})\\s+(?:${CTR}|${BLOCK})`,
`(?:${CTR}|${S_E})\\s+(?:${CTR}|${S_E})`
].join("|");
var POS_4 = [
`(?:${AXIS_X})\\s+(?:${DIM_LEN_PCT})\\s+(?:${AXIS_Y})\\s+(?:${DIM_LEN_PCT})`,
`(?:${AXIS_Y})\\s+(?:${DIM_LEN_PCT})\\s+(?:${AXIS_X})\\s+(?:${DIM_LEN_PCT})`,
`(?:${BLOCK})\\s+(?:${DIM_LEN_PCT})\\s+(?:${INLINE})\\s+(?:${DIM_LEN_PCT})`,
`(?:${INLINE})\\s+(?:${DIM_LEN_PCT})\\s+(?:${BLOCK})\\s+(?:${DIM_LEN_PCT})`,
`(?:${S_E})\\s+(?:${DIM_LEN_PCT})\\s+(?:${S_E})\\s+(?:${DIM_LEN_PCT})`
].join("|");
var RAD_EXTENT = "(?:clos|farth)est-(?:corner|side)";
var RAD_SIZE = [
`${RAD_EXTENT}(?:\\s+${RAD_EXTENT})?`,
`${DIM_LEN_POSI}`,
`(?:${DIM_LEN_PCT_POSI})\\s+(?:${DIM_LEN_PCT_POSI})`
].join("|");
var RAD_SHAPE = "circle|ellipse";
var FROM_ANGLE = `from\\s+${DIM_ANGLE}`;
var AT_POSITION = `at\\s+(?:${POS_1}|${POS_2}|${POS_4})`;
var TO_SIDE_CORNER = `to\\s+(?:(?:${L_R})(?:\\s(?:${T_B}))?|(?:${T_B})(?:\\s(?:${L_R}))?)`;
var IN_COLOR_SPACE = `in\\s+(?:${CS_RECT}|${CS_HUE})`;
var LINE_SYNTAX_LINEAR = [`(?:${DIM_ANGLE}|${TO_SIDE_CORNER})(?:\\s+${IN_COLOR_SPACE})?`, `${IN_COLOR_SPACE}(?:\\s+(?:${DIM_ANGLE}|${TO_SIDE_CORNER}))?`].join("|");
var LINE_SYNTAX_RADIAL = [
`(?:${RAD_SHAPE})(?:\\s+(?:${RAD_SIZE}))?(?:\\s+${AT_POSITION})?(?:\\s+${IN_COLOR_SPACE})?`,
`(?:${RAD_SIZE})(?:\\s+(?:${RAD_SHAPE}))?(?:\\s+${AT_POSITION})?(?:\\s+${IN_COLOR_SPACE})?`,
`${AT_POSITION}(?:\\s+${IN_COLOR_SPACE})?`,
`${IN_COLOR_SPACE}(?:\\s+${RAD_SHAPE})(?:\\s+(?:${RAD_SIZE}))?(?:\\s+${AT_POSITION})?`,
`${IN_COLOR_SPACE}(?:\\s+${RAD_SIZE})(?:\\s+(?:${RAD_SHAPE}))?(?:\\s+${AT_POSITION})?`,
`${IN_COLOR_SPACE}(?:\\s+${AT_POSITION})?`
].join("|");
var LINE_SYNTAX_CONIC = [
`${FROM_ANGLE}(?:\\s+${AT_POSITION})?(?:\\s+${IN_COLOR_SPACE})?`,
`${AT_POSITION}(?:\\s+${IN_COLOR_SPACE})?`,
`${IN_COLOR_SPACE}(?:\\s+${FROM_ANGLE})?(?:\\s+${AT_POSITION})?`
].join("|");
var DEFAULT_LINEAR = [/to\s+bottom/];
var DEFAULT_RADIAL = [
/ellipse/,
/farthest-corner/,
/at\s+center/
];
var DEFAULT_CONIC = [/at\s+center/];
var IS_CONIC = /^(?:repeating-)?conic-gradient$/;
var IS_LINEAR = /^(?:repeating-)?linear-gradient$/;
var IS_RADIAL = /^(?:repeating-)?radial-gradient$/;
var REG_COLOR_HINT_CONIC = new RegExp(`^(?:${DIM_ANGLE_PCT})$`);
var REG_COLOR_HINT_NON_CONIC = new RegExp(`^(?:${DIM_LEN_PCT})$`);
var REG_DIM_CONIC = new RegExp(`(?:\\s+(?:${DIM_ANGLE_PCT})){1,2}$`);
var REG_DIM_NON_CONIC = new RegExp(`(?:\\s+(?:${DIM_LEN_PCT})){1,2}$`);
var REG_GRAD = /^(?:repeating-)?(?:conic|linear|radial)-gradient\(/;
var REG_GRAD_CAPT = /^((?:repeating-)?(?:conic|linear|radial)-gradient)\(/;
var REG_LINE_CONIC = new RegExp(`^(?:${LINE_SYNTAX_CONIC})$`);
var REG_LINE_LINEAR = new RegExp(`^(?:${LINE_SYNTAX_LINEAR})$`);
var REG_LINE_RADIAL = new RegExp(`^(?:${LINE_SYNTAX_RADIAL})$`);
/**
* get gradient type
* @param value - gradient value
* @returns gradient type
*/
var getGradientType = (value) => {
if (isString(value)) {
value = value.trim();
if (REG_GRAD.test(value)) {
const [, type] = value.match(REG_GRAD_CAPT);
return type;
}
}
return "";
};
/**
* validate gradient line
* @param value - gradient line value
* @param type - gradient type
* @returns result
*/
var validateGradientLine = (value, type) => {
if (isString(value) && isString(type)) {
value = value.trim();
type = type.trim();
let reg = null;
let defaultValues = [];
if (IS_LINEAR.test(type)) {
reg = REG_LINE_LINEAR;
defaultValues = DEFAULT_LINEAR;
} else if (IS_RADIAL.test(type)) {
reg = REG_LINE_RADIAL;
defaultValues = DEFAULT_RADIAL;
} else if (IS_CONIC.test(type)) {
reg = REG_LINE_CONIC;
defaultValues = DEFAULT_CONIC;
}
if (reg) {
const valid = reg.test(value);
if (valid) {
let line = value;
for (const defaultValue of defaultValues) line = line.replace(defaultValue, "");
line = line.replace(/\s{2,}/g, " ").trim();
return {
line,
valid
};
}
return {
valid,
line: value
};
}
}
return {
line: value,
valid: false
};
};
/**
* validate color stop list
* @param list
* @param type
* @param [opt]
* @returns result
*/
var validateColorStopList = (list, type, opt = {}) => {
if (Array.isArray(list) && list.length > 1) {
const isConic = IS_CONIC.test(type);
const regColorHint = isConic ? REG_COLOR_HINT_CONIC : REG_COLOR_HINT_NON_CONIC;
const regDimension = isConic ? REG_DIM_CONIC : REG_DIM_NON_CONIC;
const valueList = [];
let prevType = "";
for (let i = 0; i < list.length; i++) {
const item = list[i];
if (isString(item)) {
if (regColorHint.test(item)) {
if (i === 0 || prevType === "hint") return {
colorStops: list,
valid: false
};
prevType = "hint";
valueList.push(item);
} else {
const itemColor = item.replace(regDimension, "");
if (isColor(itemColor, { format: "specifiedValue" })) {
const resolvedColor = resolveColor(itemColor, opt);
prevType = "color";
valueList.push(item.replace(itemColor, resolvedColor));
} else return {
colorStops: list,
valid: false
};
}
} else return {
colorStops: list,
valid: false
};
}
if (prevType !== "color") return {
colorStops: list,
valid: false
};
return {
valid: true,
colorStops: valueList
};
}
return {
colorStops: list,
valid: false
};
};
/**
* parse CSS gradient
* @param value - gradient value
* @param [opt] - options
* @returns parsed result
*/
var parseGradient = (value, opt = {}) => {
if (isString(value)) {
value = value.trim();
const cacheKey = createCacheKey({
namespace: NAMESPACE,
name: "parseGradient",
value
}, opt);
const cachedResult = getCache(cacheKey);
if (cachedResult !== false) return cachedResult.item;
const type = getGradientType(value);
const gradValue = value.replace(REG_GRAD, "").replace(/\)$/, "");
if (type && gradValue) {
const [lineOrColorStop = "", ...itemList] = splitValue(gradValue, { delimiter: "," });
const regDimension = IS_CONIC.test(type) ? REG_DIM_CONIC : REG_DIM_NON_CONIC;
let colorStop = "";
if (regDimension.test(lineOrColorStop)) {
const itemColor = lineOrColorStop.replace(regDimension, "");
if (isColor(itemColor, { format: "specifiedValue" })) {
const resolvedColor = resolveColor(itemColor, opt);
colorStop = lineOrColorStop.replace(itemColor, resolvedColor);
}
} else if (isColor(lineOrColorStop, { format: "specifiedValue" })) colorStop = resolveColor(lineOrColorStop, opt);
if (colorStop) {
itemList.unshift(colorStop);
const { colorStops, valid } = validateColorStopList(itemList, type, opt);
if (valid) {
const res = {
value,
type,
colorStopList: colorStops
};
setCache(cacheKey, res);
return res;
}
} else if (itemList.length > 1) {
const { line: gradientLine, valid: validLine } = validateGradientLine(lineOrColorStop, type);
const { colorStops, valid: validColorStops } = validateColorStopList(itemList, type, opt);
if (validLine && validColorStops) {
const res = {
value,
type,
gradientLine,
colorStopList: colorStops
};
setCache(cacheKey, res);
return res;
}
}
}
setCache(cacheKey, null);
return null;
}
return null;
};
/**
* resolve CSS gradient
* @param value - CSS value
* @param [opt] - options
* @returns result
*/
var resolveGradient = (value, opt = {}) => {
const { format = VAL_COMP } = opt;
const gradient = parseGradient(value, opt);
if (gradient) {
const { type = "", gradientLine = "", colorStopList = [] } = gradient;
if (type && Array.isArray(colorStopList) && colorStopList.length > 1) {
if (gradientLine) return `${type}(${gradientLine}, ${colorStopList.join(", ")})`;
return `${type}(${colorStopList.join(", ")})`;
}
}
if (format === "specifiedValue") return "";
return "none";
};
/**
* is CSS gradient
* @param value - CSS value
* @param [opt] - options
* @returns result
*/
var isGradient = (value, opt = {}) => {
return parseGradient(value, opt) !== null;
};
//#endregion
export { getGradientType, isGradient, parseGradient, resolveGradient, validateColorStopList, validateGradientLine };
//# sourceMappingURL=css-gradient.js.map
@@ -0,0 +1 @@
{"version":3,"file":"css-gradient.js","names":[],"sources":["../../../src/js/css-gradient.ts"],"sourcesContent":["/**\n * css-gradient\n */\n\nimport { createCacheKey, getCache, setCache } from './cache';\nimport { isString } from './common';\nimport { isColor, resolveColor } from './resolve';\nimport { MatchedRegExp, Options } from './typedef';\nimport { splitValue } from './util';\n\n/* constants */\nimport {\n ANGLE,\n CS_HUE,\n CS_RECT,\n LENGTH,\n NUM,\n NUM_POSITIVE,\n PCT,\n VAL_COMP,\n VAL_SPEC\n} from './constant';\nconst NAMESPACE = 'css-gradient';\nconst DIM_ANGLE = `${NUM}(?:${ANGLE})`;\nconst DIM_ANGLE_PCT = `${DIM_ANGLE}|${PCT}`;\nconst DIM_LEN = `${NUM}(?:${LENGTH})|0`;\nconst DIM_LEN_PCT = `${DIM_LEN}|${PCT}`;\nconst DIM_LEN_PCT_POSI = `${NUM_POSITIVE}(?:${LENGTH}|%)|0`;\nconst DIM_LEN_POSI = `${NUM_POSITIVE}(?:${LENGTH})|0`;\nconst CTR = 'center';\nconst L_R = 'left|right';\nconst T_B = 'top|bottom';\nconst S_E = 'start|end';\nconst AXIS_X = `${L_R}|x-(?:${S_E})`;\nconst AXIS_Y = `${T_B}|y-(?:${S_E})`;\nconst BLOCK = `block-(?:${S_E})`;\nconst INLINE = `inline-(?:${S_E})`;\nconst POS_1 = `${CTR}|${AXIS_X}|${AXIS_Y}|${BLOCK}|${INLINE}|${DIM_LEN_PCT}`;\nconst POS_2 = [\n `(?:${CTR}|${AXIS_X})\\\\s+(?:${CTR}|${AXIS_Y})`,\n `(?:${CTR}|${AXIS_Y})\\\\s+(?:${CTR}|${AXIS_X})`,\n `(?:${CTR}|${AXIS_X}|${DIM_LEN_PCT})\\\\s+(?:${CTR}|${AXIS_Y}|${DIM_LEN_PCT})`,\n `(?:${CTR}|${BLOCK})\\\\s+(?:${CTR}|${INLINE})`,\n `(?:${CTR}|${INLINE})\\\\s+(?:${CTR}|${BLOCK})`,\n `(?:${CTR}|${S_E})\\\\s+(?:${CTR}|${S_E})`\n].join('|');\nconst POS_4 = [\n `(?:${AXIS_X})\\\\s+(?:${DIM_LEN_PCT})\\\\s+(?:${AXIS_Y})\\\\s+(?:${DIM_LEN_PCT})`,\n `(?:${AXIS_Y})\\\\s+(?:${DIM_LEN_PCT})\\\\s+(?:${AXIS_X})\\\\s+(?:${DIM_LEN_PCT})`,\n `(?:${BLOCK})\\\\s+(?:${DIM_LEN_PCT})\\\\s+(?:${INLINE})\\\\s+(?:${DIM_LEN_PCT})`,\n `(?:${INLINE})\\\\s+(?:${DIM_LEN_PCT})\\\\s+(?:${BLOCK})\\\\s+(?:${DIM_LEN_PCT})`,\n `(?:${S_E})\\\\s+(?:${DIM_LEN_PCT})\\\\s+(?:${S_E})\\\\s+(?:${DIM_LEN_PCT})`\n].join('|');\nconst RAD_EXTENT = '(?:clos|farth)est-(?:corner|side)';\nconst RAD_SIZE = [\n `${RAD_EXTENT}(?:\\\\s+${RAD_EXTENT})?`,\n `${DIM_LEN_POSI}`,\n `(?:${DIM_LEN_PCT_POSI})\\\\s+(?:${DIM_LEN_PCT_POSI})`\n].join('|');\nconst RAD_SHAPE = 'circle|ellipse';\nconst FROM_ANGLE = `from\\\\s+${DIM_ANGLE}`;\nconst AT_POSITION = `at\\\\s+(?:${POS_1}|${POS_2}|${POS_4})`;\nconst TO_SIDE_CORNER = `to\\\\s+(?:(?:${L_R})(?:\\\\s(?:${T_B}))?|(?:${T_B})(?:\\\\s(?:${L_R}))?)`;\nconst IN_COLOR_SPACE = `in\\\\s+(?:${CS_RECT}|${CS_HUE})`;\nconst LINE_SYNTAX_LINEAR = [\n `(?:${DIM_ANGLE}|${TO_SIDE_CORNER})(?:\\\\s+${IN_COLOR_SPACE})?`,\n `${IN_COLOR_SPACE}(?:\\\\s+(?:${DIM_ANGLE}|${TO_SIDE_CORNER}))?`\n].join('|');\nconst LINE_SYNTAX_RADIAL = [\n `(?:${RAD_SHAPE})(?:\\\\s+(?:${RAD_SIZE}))?(?:\\\\s+${AT_POSITION})?(?:\\\\s+${IN_COLOR_SPACE})?`,\n `(?:${RAD_SIZE})(?:\\\\s+(?:${RAD_SHAPE}))?(?:\\\\s+${AT_POSITION})?(?:\\\\s+${IN_COLOR_SPACE})?`,\n `${AT_POSITION}(?:\\\\s+${IN_COLOR_SPACE})?`,\n `${IN_COLOR_SPACE}(?:\\\\s+${RAD_SHAPE})(?:\\\\s+(?:${RAD_SIZE}))?(?:\\\\s+${AT_POSITION})?`,\n `${IN_COLOR_SPACE}(?:\\\\s+${RAD_SIZE})(?:\\\\s+(?:${RAD_SHAPE}))?(?:\\\\s+${AT_POSITION})?`,\n `${IN_COLOR_SPACE}(?:\\\\s+${AT_POSITION})?`\n].join('|');\nconst LINE_SYNTAX_CONIC = [\n `${FROM_ANGLE}(?:\\\\s+${AT_POSITION})?(?:\\\\s+${IN_COLOR_SPACE})?`,\n `${AT_POSITION}(?:\\\\s+${IN_COLOR_SPACE})?`,\n `${IN_COLOR_SPACE}(?:\\\\s+${FROM_ANGLE})?(?:\\\\s+${AT_POSITION})?`\n].join('|');\nconst DEFAULT_LINEAR = [/to\\s+bottom/];\nconst DEFAULT_RADIAL = [/ellipse/, /farthest-corner/, /at\\s+center/];\nconst DEFAULT_CONIC = [/at\\s+center/];\n\n/* type definitions */\n/**\n * @type ColorStopList - list of color stops\n */\ntype ColorStopList = [string, string, ...string[]];\n\n/**\n * @typedef ValidateGradientLine - validate gradient line\n * @property line - gradient line\n * @property valid - result\n */\ninterface ValidateGradientLine {\n line: string;\n valid: boolean;\n}\n\n/**\n * @typedef ValidateColorStops - validate color stops\n * @property colorStops - list of color stops\n * @property valid - result\n */\ninterface ValidateColorStops {\n colorStops: string[];\n valid: boolean;\n}\n\n/**\n * @typedef Gradient - parsed CSS gradient\n * @property value - input value\n * @property type - gradient type\n * @property [gradientLine] - gradient line\n * @property colorStopList - list of color stops\n */\ninterface Gradient {\n value: string;\n type: string;\n gradientLine?: string;\n colorStopList: ColorStopList;\n}\n\n/* regexp */\nconst IS_CONIC = /^(?:repeating-)?conic-gradient$/;\nconst IS_LINEAR = /^(?:repeating-)?linear-gradient$/;\nconst IS_RADIAL = /^(?:repeating-)?radial-gradient$/;\nconst REG_COLOR_HINT_CONIC = new RegExp(`^(?:${DIM_ANGLE_PCT})$`);\nconst REG_COLOR_HINT_NON_CONIC = new RegExp(`^(?:${DIM_LEN_PCT})$`);\nconst REG_DIM_CONIC = new RegExp(`(?:\\\\s+(?:${DIM_ANGLE_PCT})){1,2}$`);\nconst REG_DIM_NON_CONIC = new RegExp(`(?:\\\\sLine truncated
@@ -0,0 +1,66 @@
import { CSSToken } from '@csstools/css-tokenizer';
import { ColorChannels, Options, StringColorChannels } from './typedef.js';
/**
* @type NumberOrStringColorChannels - color channel
*/
type NumberOrStringColorChannels = ColorChannels & StringColorChannels;
/**
* resolve relative color channels
* @param value
* - CSS color value
* - system colors are not supported
* @param [opt] - options
* @param [opt.currentColor]
* - color to use for `currentcolor` keyword
* - if omitted, it will be treated as a missing color
* i.e. `rgb(none none none / none)`
* @param [opt.customProperty]
* - custom properties
* - pair of `--` prefixed property name and value,
* e.g. `customProperty: { '--some-color': '#0000ff' }`
* - and/or `callback` function to get the value of the custom property,
* e.g. `customProperty: { callback: someDeclaration.getPropertyValue }`
* @param [opt.dimension]
* - dimension, convert relative length to pixels
* - pair of unit and it's value as a number in pixels,
* e.g. `dimension: { em: 12, rem: 16, vw: 10.26 }`
* - and/or `callback` function to get the value as a number in pixels,
* e.g. `dimension: { callback: convertUnitToPixel }`
* @param [opt.format]
* - output format, one of below
* - `computedValue` (default), [computed value][139] of the color
* - `specifiedValue`, [specified value][140] of the color
* - `hex`, hex color notation, i.e. `rrggbb`
* - `hexAlpha`, hex color notation with alpha channel, i.e. `#rrggbbaa`
* @returns
* - one of rgba?(), #rrggbb(aa)?, color-name, '(empty-string)',
* color(color-space r g b / alpha), color(color-space x y z / alpha),
* lab(l a b / alpha), lch(l c h / alpha), oklab(l a b / alpha),
* oklch(l c h / alpha), null
* - in `computedValue`, values are numbers, however `rgb()` values are
* integers
* - in `specifiedValue`, returns `empty string` for unknown and/or invalid
* color
* - in `hex`, returns `null` for `transparent`, and also returns `null` if
* any of `r`, `g`, `b`, `alpha` is not a number
* - in `hexAlpha`, returns `#00000000` for `transparent`,
* however returns `null` if any of `r`, `g`, `b`, `alpha` is not a number
*/
export declare function resolveColorChannels(tokens: CSSToken[], opt?: Options): NumberOrStringColorChannels | null;
/**
* extract origin color
* @param value - CSS color value
* @param opt - options
* @param resolver - resolver function
* @returns origin color value
*/
export declare function extractOriginColor(value: string, opt?: Options, resolver?: (v: string, o?: Options) => string | null): string | null;
/**
* resolve relative color
* @param value - CSS relative color value
* @param opt - options
* @param resolver - resolver function
* @returns resolved value
*/
export declare function resolveRelativeColor(value: string, opt?: Options, resolver?: (v: string, o?: Options) => string | null): string | null;
export {};
@@ -0,0 +1,462 @@
import { createCacheKey, getCache, setCache } from "./cache.js";
import { isString, isStringOrNumber } from "./common.js";
import { roundToPrecision, splitValue } from "./util.js";
import { CS_LAB, CS_LCH, FN_REL, FN_REL_CAPT, FN_VAR, NONE, SYN_COLOR_TYPE, SYN_FN_MATH_START, SYN_FN_VAR, SYN_MIX, VAL_SPEC } from "./constant.js";
import { NAMED_COLORS, convertColorToRgb } from "./color.js";
import { resolveDimension, serializeCalc } from "./css-calc-var.js";
import { TokenType, tokenize } from "@csstools/css-tokenizer";
import { SyntaxFlag, color } from "@csstools/css-color-parser";
import { parseComponentValue } from "@csstools/css-parser-algorithms";
//#region src/js/relative-color.ts
/**
* relative-color
*/
var { CloseParen: PAREN_CLOSE, Comment: COMMENT, Delim: DELIM, Dimension: DIM, EOF, Function: FUNC, Ident: IDENT, Number: NUM, OpenParen: PAREN_OPEN, Percentage: PCT, Whitespace: W_SPACE } = TokenType;
var { HasNoneKeywords: KEY_NONE } = SyntaxFlag;
var NAMESPACE = "relative-color";
var OCT = 8;
var DEC = 10;
var HEX = 16;
var MAX_PCT = 100;
var MAX_RGB = 255;
var COLOR_CHANNELS = /* @__PURE__ */ new Map([
["color", [
"r",
"g",
"b",
"alpha"
]],
["hsl", [
"h",
"s",
"l",
"alpha"
]],
["hsla", [
"h",
"s",
"l",
"alpha"
]],
["hwb", [
"h",
"w",
"b",
"alpha"
]],
["lab", [
"l",
"a",
"b",
"alpha"
]],
["lch", [
"l",
"c",
"h",
"alpha"
]],
["oklab", [
"l",
"a",
"b",
"alpha"
]],
["oklch", [
"l",
"c",
"h",
"alpha"
]],
["rgb", [
"r",
"g",
"b",
"alpha"
]],
["rgba", [
"r",
"g",
"b",
"alpha"
]]
]);
var REG_COLOR_CAPT = new RegExp(`^${FN_REL}(${SYN_COLOR_TYPE}|${SYN_MIX})\\s+`);
var REG_CS_HSL = /(?:hsla?|hwb)$/;
var REG_CS_CIE = new RegExp(`^(?:${CS_LAB}|${CS_LCH})$`);
var REG_FN_CALC_SUM = /^(?:abs|sig?n|cos|tan)\(/;
var REG_FN_MATH_START = new RegExp(SYN_FN_MATH_START);
var REG_FN_REL = new RegExp(FN_REL);
var REG_FN_REL_CAPT = new RegExp(`^${FN_REL_CAPT}`);
var REG_FN_REL_START = new RegExp(`^${FN_REL}`);
var REG_FN_VAR = new RegExp(SYN_FN_VAR);
/**
* resolve relative color channels
* @param value
* - CSS color value
* - system colors are not supported
* @param [opt] - options
* @param [opt.currentColor]
* - color to use for `currentcolor` keyword
* - if omitted, it will be treated as a missing color
* i.e. `rgb(none none none / none)`
* @param [opt.customProperty]
* - custom properties
* - pair of `--` prefixed property name and value,
* e.g. `customProperty: { '--some-color': '#0000ff' }`
* - and/or `callback` function to get the value of the custom property,
* e.g. `customProperty: { callback: someDeclaration.getPropertyValue }`
* @param [opt.dimension]
* - dimension, convert relative length to pixels
* - pair of unit and it's value as a number in pixels,
* e.g. `dimension: { em: 12, rem: 16, vw: 10.26 }`
* - and/or `callback` function to get the value as a number in pixels,
* e.g. `dimension: { callback: convertUnitToPixel }`
* @param [opt.format]
* - output format, one of below
* - `computedValue` (default), [computed value][139] of the color
* - `specifiedValue`, [specified value][140] of the color
* - `hex`, hex color notation, i.e. `rrggbb`
* - `hexAlpha`, hex color notation with alpha channel, i.e. `#rrggbbaa`
* @returns
* - one of rgba?(), #rrggbb(aa)?, color-name, '(empty-string)',
* color(color-space r g b / alpha), color(color-space x y z / alpha),
* lab(l a b / alpha), lch(l c h / alpha), oklab(l a b / alpha),
* oklch(l c h / alpha), null
* - in `computedValue`, values are numbers, however `rgb()` values are
* integers
* - in `specifiedValue`, returns `empty string` for unknown and/or invalid
* color
* - in `hex`, returns `null` for `transparent`, and also returns `null` if
* any of `r`, `g`, `b`, `alpha` is not a number
* - in `hexAlpha`, returns `#00000000` for `transparent`,
* however returns `null` if any of `r`, `g`, `b`, `alpha` is not a number
*/
function resolveColorChannels(tokens, opt = {}) {
if (!Array.isArray(tokens)) throw new TypeError(`${tokens} is not an array.`);
const { colorSpace = "", format = "" } = opt;
const colorChannel = COLOR_CHANNELS.get(colorSpace);
if (!colorChannel) return null;
const mathFunc = /* @__PURE__ */ new Set();
const channels = [
[],
[],
[],
[]
];
let i = 0;
let nest = 0;
let func = "";
let precededPct = false;
for (const token of tokens) {
if (!Array.isArray(token)) throw new TypeError(`${token} is not an array.`);
const [type, value, , , detail] = token;
const channel = channels[i];
if (Array.isArray(channel)) switch (type) {
case DELIM:
if (func) {
if ((value === "+" || value === "-") && precededPct && !REG_FN_CALC_SUM.test(func)) return null;
precededPct = false;
channel.push(value);
}
break;
case DIM: {
if (!func || !REG_FN_CALC_SUM.test(func)) return null;
const resolvedValue = resolveDimension(token, opt);
if (isString(resolvedValue)) channel.push(resolvedValue);
else channel.push(value);
break;
}
case FUNC:
channel.push(value);
func = value;
nest++;
if (REG_FN_MATH_START.test(value)) mathFunc.add(nest);
break;
case IDENT:
if (!colorChannel.includes(value)) return null;
channel.push(value);
if (!func) i++;
break;
case NUM:
channel.push(Number(detail?.value));
if (!func) i++;
break;
case PAREN_OPEN:
channel.push(value);
nest++;
break;
case PAREN_CLOSE:
if (func) {
if (channel[channel.length - 1] === " ") channel[channel.length - 1] = value;
else channel.push(value);
if (mathFunc.has(nest)) mathFunc.delete(nest);
nest--;
if (nest === 0) {
func = "";
i++;
}
}
break;
case PCT:
if (!func) return null;
else if (!REG_FN_CALC_SUM.test(func)) {
let lastValue;
for (let j = channel.length - 1; j >= 0; j--) if (channel[j] !== " ") {
lastValue = channel[j];
break;
}
if (lastValue === "+" || lastValue === "-") return null;
else if (lastValue === "*" || lastValue === "/") precededPct = false;
else precededPct = true;
}
channel.push(Number(detail?.value) / MAX_PCT);
break;
case W_SPACE:
if (channel.length && func) {
const lastValue = channel[channel.length - 1];
if (typeof lastValue === "number") channel.push(value);
else if (isString(lastValue) && !lastValue.endsWith("(") && lastValue !== " ") channel.push(value);
}
break;
default: if (type !== COMMENT && type !== EOF && func) channel.push(value);
}
}
const channelValues = [];
for (const channel of channels) if (channel.length === 1) {
const [resolvedValue] = channel;
if (isStringOrNumber(resolvedValue)) channelValues.push(resolvedValue);
} else if (channel.length) {
const resolvedValue = serializeCalc(channel.join(""), { format });
channelValues.push(resolvedValue);
}
return channelValues;
}
/**
* extract origin color
* @param value - CSS color value
* @param opt - options
* @param resolver - resolver function
* @returns origin color value
*/
function extractOriginColor(value, opt = {}, resolver = () => null) {
const { colorScheme = "normal", currentColor = "", format = "" } = opt;
if (isString(value)) {
value = value.toLowerCase().trim();
if (!value) return null;
if (!REG_FN_REL_START.test(value)) return value;
} else return null;
const cacheKey = createCacheKey({
namespace: NAMESPACE,
name: "extractOriginColor",
value
}, opt);
const cachedResult = getCache(cacheKey);
if (cachedResult !== false) return cachedResult.item;
if (/currentcolor/.test(value)) {
if (currentColor) value = value.replace(/currentcolor/g, currentColor);
else {
setCache(cacheKey, null);
return null;
}
}
let colorSpace = "";
if (REG_FN_REL_CAPT.test(value)) [, colorSpace] = value.match(REG_FN_REL_CAPT);
opt.colorSpace = colorSpace;
if (value.includes("light-dark(")) {
const colorParts = value.replace(new RegExp(`^${colorSpace}\\(`), "").replace(/\)$/, "");
const [, originColor = ""] = splitValue(colorParts);
const specifiedOriginColor = resolver(originColor, {
colorScheme,
format: VAL_SPEC
});
if (!specifiedOriginColor) {
setCache(cacheKey, null);
return null;
}
if (format === "specifiedValue") value = value.replace(originColor, specifiedOriginColor);
else {
const resolvedOriginColor = resolver(specifiedOriginColor, opt);
if (isString(resolvedOriginColor)) value = value.replace(originColor, resolvedOriginColor);
else {
setCache(cacheKey, null);
return null;
}
}
}
if (REG_COLOR_CAPT.test(value)) {
const [, originColor] = value.match(REG_COLOR_CAPT);
const [, restValue] = value.split(originColor);
if (/^[a-z]+$/.test(originColor)) {
if (!/^transparent$/.test(originColor) && !Object.hasOwn(NAMED_COLORS, originColor)) {
setCache(cacheKey, null);
return null;
}
} else if (format === "specifiedValue") {
const resolvedOriginColor = resolver(originColor, opt);
if (isString(resolvedOriginColor)) value = value.replace(originColor, resolvedOriginColor);
}
if (format === "specifiedValue") {
const channelValues = resolveColorChannels(tokenize({ css: restValue }), opt);
if (channelValues === null) {
setCache(cacheKey, null);
return null;
}
const [v1, v2, v3, v4] = channelValues;
let channelValue = "";
if (isStringOrNumber(v4)) channelValue = ` ${v1} ${v2} ${v3} / ${v4})`;
else channelValue = ` ${channelValues.join(" ")})`;
if (restValue !== channelValue) value = value.replace(restValue, channelValue);
}
} else {
const [, restValue] = value.split(REG_FN_REL_START);
const tokens = tokenize({ css: restValue });
const originColor = [];
let nest = 0;
let tokenIndex = 0;
for (const [type, tokenValue] of tokens) {
tokenIndex++;
switch (type) {
case FUNC:
case PAREN_OPEN:
originColor.push(tokenValue);
nest++;
break;
case PAREN_CLOSE: {
const lastValue = originColor[originColor.length - 1];
if (lastValue === " ") originColor[originColor.length - 1] = tokenValue;
else if (isString(lastValue)) originColor.push(tokenValue);
nest--;
break;
}
case W_SPACE: {
const lastValue = originColor[originColor.length - 1];
if (isString(lastValue) && !lastValue.endsWith("(") && lastValue !== " ") originColor.push(tokenValue);
break;
}
default: if (type !== COMMENT && type !== EOF) originColor.push(tokenValue);
}
if (nest === 0) break;
}
const resolvedOriginColor = resolveRelativeColor(originColor.join("").trim(), opt, resolver);
if (resolvedOriginColor === null) {
setCache(cacheKey, null);
return null;
}
const channelValues = resolveColorChannels(tokens.slice(tokenIndex), opt);
if (channelValues === null) {
setCache(cacheKey, null);
return null;
}
const [v1, v2, v3, v4] = channelValues;
let channelValue = "";
if (isStringOrNumber(v4)) channelValue = ` ${v1} ${v2} ${v3} / ${v4})`;
else channelValue = ` ${channelValues.join(" ")})`;
value = value.replace(restValue, `${resolvedOriginColor}${channelValue}`);
}
setCache(cacheKey, value);
return value;
}
/**
* resolve relative color
* @param value - CSS relative color value
* @param opt - options
* @param resolver - resolver function
* @returns resolved value
*/
function resolveRelativeColor(value, opt = {}, resolver = () => null) {
const { format = "" } = opt;
if (isString(value)) {
if (REG_FN_VAR.test(value)) {
if (format !== "specifiedValue") throw new SyntaxError(`Unexpected token ${FN_VAR} found.`);
return value;
} else if (!REG_FN_REL.test(value)) return value;
value = value.toLowerCase().trim();
} else throw new TypeError(`${value} is not a string.`);
const cacheKey = createCacheKey({
namespace: NAMESPACE,
name: "resolveRelativeColor",
value
}, opt);
const cachedResult = getCache(cacheKey);
if (cachedResult !== false) return cachedResult.item;
const originColor = extractOriginColor(value, opt, resolver);
if (originColor === null) {
setCache(cacheKey, null);
return null;
}
value = originColor;
if (format === "specifiedValue") {
if (value.startsWith("rgba(")) value = value.replace("rgba(", "rgb(");
else if (value.startsWith("hsla(")) value = value.replace("hsla(", "hsl(");
return value;
}
const tokens = tokenize({ css: value });
const components = parseComponentValue(tokens);
const parsedComponents = color(components);
if (!parsedComponents) {
setCache(cacheKey, null);
return null;
}
const { alpha: alphaComponent, channels: channelsComponent, colorNotation, syntaxFlags } = parsedComponents;
let alpha;
if (Number.isNaN(Number(alphaComponent))) {
if (syntaxFlags instanceof Set && syntaxFlags.has(KEY_NONE)) alpha = NONE;
else alpha = 0;
} else alpha = roundToPrecision(Number(alphaComponent), OCT);
let v1;
let v2;
let v3;
[v1, v2, v3] = channelsComponent;
let resolvedValue;
if (REG_CS_CIE.test(colorNotation)) {
const hasNone = syntaxFlags instanceof Set && syntaxFlags.has(KEY_NONE);
if (Number.isNaN(v1)) {
if (hasNone) v1 = NONE;
else v1 = 0;
} else v1 = roundToPrecision(v1, HEX);
if (Number.isNaN(v2)) {
if (hasNone) v2 = NONE;
else v2 = 0;
} else v2 = roundToPrecision(v2, HEX);
if (Number.isNaN(v3)) {
if (hasNone) v3 = NONE;
else v3 = 0;
} else v3 = roundToPrecision(v3, HEX);
if (alpha === 1) resolvedValue = `${colorNotation}(${v1} ${v2} ${v3})`;
else resolvedValue = `${colorNotation}(${v1} ${v2} ${v3} / ${alpha})`;
} else if (REG_CS_HSL.test(colorNotation)) {
if (Number.isNaN(v1)) v1 = 0;
if (Number.isNaN(v2)) v2 = 0;
if (Number.isNaN(v3)) v3 = 0;
let [r, g, b] = convertColorToRgb(`${colorNotation}(${v1} ${v2} ${v3} / ${alpha})`);
r = roundToPrecision(r / MAX_RGB, DEC);
g = roundToPrecision(g / MAX_RGB, DEC);
b = roundToPrecision(b / MAX_RGB, DEC);
if (alpha === 1) resolvedValue = `color(srgb ${r} ${g} ${b})`;
else resolvedValue = `color(srgb ${r} ${g} ${b} / ${alpha})`;
} else {
const cs = colorNotation === "rgb" ? "srgb" : colorNotation;
const hasNone = syntaxFlags instanceof Set && syntaxFlags.has(KEY_NONE);
if (Number.isNaN(v1)) {
if (hasNone) v1 = NONE;
else v1 = 0;
} else v1 = roundToPrecision(v1, DEC);
if (Number.isNaN(v2)) {
if (hasNone) v2 = NONE;
else v2 = 0;
} else v2 = roundToPrecision(v2, DEC);
if (Number.isNaN(v3)) {
if (hasNone) v3 = NONE;
else v3 = 0;
} else v3 = roundToPrecision(v3, DEC);
if (alpha === 1) resolvedValue = `color(${cs} ${v1} ${v2} ${v3})`;
else resolvedValue = `color(${cs} ${v1} ${v2} ${v3} / ${alpha})`;
}
setCache(cacheKey, resolvedValue);
return resolvedValue;
}
//#endregion
export { extractOriginColor, resolveColorChannels, resolveRelativeColor };
//# sourceMappingURL=relative-color.js.map
@@ -0,0 +1 @@
{"version":3,"file":"relative-color.js","names":[],"sources":["../../../src/js/relative-color.ts"],"sourcesContent":["/**\n * relative-color\n */\n\nimport { SyntaxFlag, color as colorParser } from '@csstools/css-color-parser';\nimport {\n ComponentValue,\n parseComponentValue\n} from '@csstools/css-parser-algorithms';\nimport { CSSToken, TokenType, tokenize } from '@csstools/css-tokenizer';\nimport { createCacheKey, getCache, setCache } from './cache';\nimport { NAMED_COLORS, convertColorToRgb } from './color';\nimport { isString, isStringOrNumber } from './common';\nimport { resolveDimension, serializeCalc } from './css-calc-var';\nimport { roundToPrecision, splitValue } from './util';\nimport {\n ColorChannels,\n MatchedRegExp,\n Options,\n StringColorChannels\n} from './typedef';\n\n/* constants */\nimport {\n CS_LAB,\n CS_LCH,\n FN_LIGHT_DARK,\n FN_REL,\n FN_REL_CAPT,\n FN_VAR,\n NONE,\n SYN_COLOR_TYPE,\n SYN_FN_MATH_START,\n SYN_FN_VAR,\n SYN_MIX,\n VAL_SPEC\n} from './constant';\nconst {\n CloseParen: PAREN_CLOSE,\n Comment: COMMENT,\n Delim: DELIM,\n Dimension: DIM,\n EOF,\n Function: FUNC,\n Ident: IDENT,\n Number: NUM,\n OpenParen: PAREN_OPEN,\n Percentage: PCT,\n Whitespace: W_SPACE\n} = TokenType;\nconst { HasNoneKeywords: KEY_NONE } = SyntaxFlag;\nconst NAMESPACE = 'relative-color';\n\n/* constants */\nconst OCT = 8;\nconst DEC = 10;\nconst HEX = 16;\nconst MAX_PCT = 100;\nconst MAX_RGB = 255;\nconst COLOR_CHANNELS = new Map([\n ['color', ['r', 'g', 'b', 'alpha']],\n ['hsl', ['h', 's', 'l', 'alpha']],\n ['hsla', ['h', 's', 'l', 'alpha']],\n ['hwb', ['h', 'w', 'b', 'alpha']],\n ['lab', ['l', 'a', 'b', 'alpha']],\n ['lch', ['l', 'c', 'h', 'alpha']],\n ['oklab', ['l', 'a', 'b', 'alpha']],\n ['oklch', ['l', 'c', 'h', 'alpha']],\n ['rgb', ['r', 'g', 'b', 'alpha']],\n ['rgba', ['r', 'g', 'b', 'alpha']]\n]);\n\n/* type definitions */\n/**\n * @type NumberOrStringColorChannels - color channel\n */\ntype NumberOrStringColorChannels = ColorChannels & StringColorChannels;\n\n/* regexp */\nconst REG_COLOR_CAPT = new RegExp(\n `^${FN_REL}(${SYN_COLOR_TYPE}|${SYN_MIX})\\\\s+`\n);\nconst REG_CS_HSL = /(?:hsla?|hwb)$/;\nconst REG_CS_CIE = new RegExp(`^(?:${CS_LAB}|${CS_LCH})$`);\nconst REG_FN_CALC_SUM = /^(?:abs|sig?n|cos|tan)\\(/;\nconst REG_FN_MATH_START = new RegExp(SYN_FN_MATH_START);\nconst REG_FN_REL = new RegExp(FN_REL);\nconst REG_FN_REL_CAPT = new RegExp(`^${FN_REL_CAPT}`);\nconst REG_FN_REL_START = new RegExp(`^${FN_REL}`);\nconst REG_FN_VAR = new RegExp(SYN_FN_VAR);\n\n/**\n * resolve relative color channels\n * @param value\n * - CSS color value\n * - system colors are not supported\n * @param [opt] - options\n * @param [opt.currentColor]\n * - color to use for `currentcolor` keyword\n * - if omitted, it will be treated as a missing color\n * i.e. `rgb(none none none / none)`\n * @param [opt.customProperty]\n * - custom properties\n * - pair of `--` prefixed property name and value,\n * e.g. `customProperty: { '--some-color': '#0000ff' }`\n * - and/or `callback` function to get the value of the custom property,\n * e.g. `customProperty: { callback: someDeclaration.getPropertyValue }`\n * @param [opt.dimension]\n * - dimension, convert relative length to pixels\n * - pair of unit and it's value as a number in pixels,\n * e.g. `dimension: { em: 12, rem: 16, vw: 10.26 }`\n * - and/or `callback` function to get the value as a number in pixels,\n * e.g. `dimension: { callback: convertUnitToPixel }`\n * @param [opt.format]\n * - output format, one of below\n * - `computedValue` (default), [computed value][139] of the color\n * - `specifiedValue`, [specified value][140] of the color\n * - `hex`, hex color notation, i.e. `rrggbb`\n * - `hexAlpha`, hex color notation with alpha channel, i.e. `#rrggbbaa`\n * @returns\n * - one of rgba?(), #rrggbb(aa)?, color-name, '(empty-string)',\n * color(color-space r g b / alpha), color(color-space x y z / alpha),\n * lab(l a b / alpha), lch(l c h / alpha), oklab(l a b / alpha),\n * oklch(l c h / alpha), null\n * - in `computedValue`, values are numbers, however `rgb()` values are\n * integers\n * - in `specifiedValue`, returns `empty string` for unknown and/or invalid\n * color\n * - in `hex`, returns `null` for `transparent`, and also returns `null` if\n * any of `r`, `g`, `b`, `alpha` is not a number\n * - in `hexAlpha`, returns `#00000000` for `transparent`,\n * however returns `null` if any of `r`, `g`, `b`, `alpha` is not a number\n */\nexport function resolveColorChannels(\n tokens: CSSToken[],\n opt: Options = {}\n): NumberOrStringColorChannels | null {\n if (!Array.isArray(tokens)) {\n throw new TypeError(`${tokens} is not an array.`);\n }\n const { colorSpace = '', format = '' } = opt;\n const colorChannel = COLOR_CHANNELS.get(colorSpace);\n // invalid color channel\n if (!colorChannel) {\n return null;\n }\n const maLine truncated
+22
View File
@@ -0,0 +1,22 @@
import { Options } from './typedef.js';
/**
* resolve color
* @param value - CSS color value
* @param opt - options
* @returns resolved color
*/
export declare const resolveColor: (value: string, opt?: Options) => string | null;
/**
* resolve CSS color
* @param value - CSS color value. system colors are not supported
* @param opt - options
* @returns resolved value
*/
export declare const resolve: (value: string, opt?: Options) => string | null;
/**
* is color
* @param value - CSS value
* @param opt - options
* @returns result
*/
export declare const isColor: (value: unknown, opt?: Options) => boolean;
+241
View File
@@ -0,0 +1,241 @@
import { createCacheKey, getCache, setCache } from "./cache.js";
import { isString } from "./common.js";
import { splitValue } from "./util.js";
import { SYN_COLOR_TYPE, SYN_FN_CALC, SYN_FN_LIGHT_DARK, SYN_FN_REL, SYN_FN_VAR, SYN_MIX, VAL_COMP, VAL_SPEC } from "./constant.js";
import { NAMED_COLORS, convertRgbToHex, resolveColorFunc, resolveColorMix, resolveColorValue } from "./color.js";
import { resolveRelativeColor } from "./relative-color.js";
import { cssCalc, resolveVar } from "./css-calc-var.js";
//#region src/js/resolve.ts
/**
* resolve
*/
var NAMESPACE = "resolve";
var RGB_TRANSPARENT = "rgba(0, 0, 0, 0)";
var REG_COLOR = new RegExp(`^(?:${SYN_COLOR_TYPE})$`);
var REG_FN_CALC = new RegExp(SYN_FN_CALC);
var REG_FN_COLOR = /^(?:(?:ok)?l(?:ab|ch)|color(?:-mix)?|hsla?|hwb|rgba?|var)\(/;
var REG_FN_LIGHT_DARK = new RegExp(SYN_FN_LIGHT_DARK);
var REG_FN_REL = new RegExp(SYN_FN_REL);
var REG_FN_VAR = new RegExp(SYN_FN_VAR);
var REG_MIX = new RegExp(SYN_MIX);
/**
* resolve color
* @param value - CSS color value
* @param opt - options
* @returns resolved color
*/
var resolveColor = (value, opt = {}) => {
if (!isString(value)) throw new TypeError(`${value} is not a string.`);
value = value.trim();
const { colorScheme = "normal", currentColor = "", format = VAL_COMP, nullable = false } = opt;
const cacheKey = createCacheKey({
namespace: NAMESPACE,
name: "resolve",
value
}, opt);
const cachedResult = getCache(cacheKey);
if (cachedResult !== false) return cachedResult.item;
if (REG_FN_VAR.test(value)) {
if (format === "specifiedValue") {
setCache(cacheKey, value);
return value;
}
const resolvedVar = resolveVar(value, opt);
if (resolvedVar === null) {
const res = format === "hex" || format === "hexAlpha" || nullable ? null : RGB_TRANSPARENT;
setCache(cacheKey, res);
return res;
}
value = resolvedVar;
}
if (opt.format !== format) opt.format = format;
value = value.toLowerCase();
if (REG_FN_LIGHT_DARK.test(value) && value.endsWith(")")) {
const colorParts = value.replace(REG_FN_LIGHT_DARK, "").replace(/\)$/, "");
const [light = "", dark = ""] = splitValue(colorParts, { delimiter: "," });
if (light && dark) {
if (format === "specifiedValue") {
const lightColor = resolveColor(light, opt);
const darkColor = resolveColor(dark, opt);
const res = lightColor && darkColor ? `light-dark(${lightColor}, ${darkColor})` : "";
setCache(cacheKey, res);
return res;
}
const resolved = resolveColor(colorScheme === "dark" ? dark : light, opt);
const res = resolved === null && !nullable ? RGB_TRANSPARENT : resolved;
setCache(cacheKey, res);
return res;
}
let invalidRes;
if (format === "specifiedValue") invalidRes = "";
else if (format === "hex" || format === "hexAlpha") invalidRes = null;
else invalidRes = RGB_TRANSPARENT;
setCache(cacheKey, invalidRes);
return invalidRes;
}
if (REG_FN_REL.test(value)) {
const resolvedRel = resolveRelativeColor(value, opt, resolveColor);
if (format === "computedValue") {
const res = resolvedRel === null && !nullable ? RGB_TRANSPARENT : resolvedRel;
setCache(cacheKey, res);
return res;
}
if (format === "specifiedValue") {
const res = resolvedRel === null ? "" : resolvedRel;
setCache(cacheKey, res);
return res;
}
value = resolvedRel === null ? "" : resolvedRel;
}
if (REG_FN_CALC.test(value)) value = cssCalc(value, opt);
let cs = "";
let r = NaN;
let g = NaN;
let b = NaN;
let alpha = NaN;
if (value === "transparent") {
let res;
switch (format) {
case VAL_SPEC:
res = value;
break;
case "hex":
res = null;
break;
case "hexAlpha":
res = "#00000000";
break;
default: res = RGB_TRANSPARENT;
}
setCache(cacheKey, res);
return res;
}
if (value === "currentcolor") {
if (format === "specifiedValue") {
setCache(cacheKey, value);
return value;
}
if (currentColor) {
let resolvedCurrent;
if (currentColor.startsWith("color-mix(")) resolvedCurrent = resolveColorMix(currentColor, opt, resolveColor);
else if (currentColor.startsWith("color(")) resolvedCurrent = resolveColorFunc(currentColor, opt);
else resolvedCurrent = resolveColorValue(currentColor, opt);
if (resolvedCurrent === null) {
setCache(cacheKey, null);
return null;
}
[cs, r, g, b, alpha] = resolvedCurrent;
} else {
const res = format === "computedValue" ? RGB_TRANSPARENT : value;
if (format === "computedValue") {
setCache(cacheKey, res);
return res;
}
}
} else if (format === "specifiedValue") {
let res = "";
if (value.startsWith("color-mix(")) {
const mixRes = resolveColorMix(value, opt, resolveColor);
res = mixRes ? mixRes : "";
} else if (value.startsWith("color(")) {
const funcRes = resolveColorFunc(value, opt);
if (isString(funcRes)) res = funcRes;
else if (Array.isArray(funcRes)) {
const [scs, rr, gg, bb, aa] = funcRes;
res = aa === 1 ? `color(${scs} ${rr} ${gg} ${bb})` : `color(${scs} ${rr} ${gg} ${bb} / ${aa})`;
}
} else {
const rgb = resolveColorValue(value, opt);
if (isString(rgb)) res = rgb;
else if (Array.isArray(rgb)) {
const [scs, rr, gg, bb, aa] = rgb;
if (scs === "rgb") res = aa === 1 ? `${scs}(${rr}, ${gg}, ${bb})` : `${scs}a(${rr}, ${gg}, ${bb}, ${aa})`;
else res = aa === 1 ? `${scs}(${rr} ${gg} ${bb})` : `${scs}(${rr} ${gg} ${bb} / ${aa})`;
}
}
setCache(cacheKey, res);
return res;
} else if (value.startsWith("color-mix(")) {
if (currentColor) value = value.replace(/currentcolor/g, currentColor);
value = value.replace(/transparent/g, RGB_TRANSPARENT);
const resolvedMix = resolveColorMix(value, opt, resolveColor);
if (resolvedMix === null) {
setCache(cacheKey, null);
return null;
}
[cs, r, g, b, alpha] = resolvedMix;
} else if (value.startsWith("color(")) {
const resolvedFunc = resolveColorFunc(value, opt);
if (resolvedFunc === null) {
setCache(cacheKey, null);
return null;
}
[cs, r, g, b, alpha] = resolvedFunc;
} else if (value) {
const resolvedVal = resolveColorValue(value, opt);
if (resolvedVal === null) {
setCache(cacheKey, null);
return null;
}
[cs, r, g, b, alpha] = resolvedVal;
}
let finalRes = "";
switch (format) {
case "hex":
case "hexAlpha":
if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b) || Number.isNaN(alpha) || format === "hex" && alpha === 0) finalRes = null;
else finalRes = convertRgbToHex([
r,
g,
b,
format === "hex" ? 1 : alpha
]);
break;
default: if (cs === "rgb") finalRes = alpha === 1 ? `${cs}(${r}, ${g}, ${b})` : `${cs}a(${r}, ${g}, ${b}, ${alpha})`;
else if ([
"lab",
"lch",
"oklab",
"oklch"
].includes(cs)) finalRes = alpha === 1 ? `${cs}(${r} ${g} ${b})` : `${cs}(${r} ${g} ${b} / ${alpha})`;
else finalRes = alpha === 1 ? `color(${cs} ${r} ${g} ${b})` : `color(${cs} ${r} ${g} ${b} / ${alpha})`;
}
setCache(cacheKey, finalRes);
return finalRes;
};
/**
* resolve CSS color
* @param value - CSS color value. system colors are not supported
* @param opt - options
* @returns resolved value
*/
var resolve = (value, opt = {}) => {
opt.nullable = false;
return resolveColor(value, opt);
};
/**
* is color
* @param value - CSS value
* @param opt - options
* @returns result
*/
var isColor = (value, opt = {}) => {
if (!isString(value)) return false;
const str = value.toLowerCase().trim();
if (!str) return false;
if (/^[a-z]+$/.test(str)) return str === "currentcolor" || str === "transparent" || Object.hasOwn(NAMED_COLORS, str);
if (REG_COLOR.test(str) || REG_MIX.test(str)) return true;
if (REG_FN_COLOR.test(str)) {
const colorOpt = {
...opt,
nullable: true
};
if (!colorOpt.format) colorOpt.format = VAL_SPEC;
return !!resolveColor(str, colorOpt);
}
return false;
};
//#endregion
export { isColor, resolve, resolveColor };
//# sourceMappingURL=resolve.js.map
@@ -0,0 +1 @@
{"version":3,"file":"resolve.js","names":[],"sources":["../../../src/js/resolve.ts"],"sourcesContent":["/**\n * resolve\n */\n\nimport { createCacheKey, getCache, setCache } from './cache';\nimport {\n NAMED_COLORS,\n convertRgbToHex,\n resolveColorFunc,\n resolveColorMix,\n resolveColorValue\n} from './color';\nimport { isString } from './common';\nimport { cssCalc, resolveVar } from './css-calc-var';\nimport { resolveRelativeColor } from './relative-color';\nimport { splitValue } from './util';\nimport {\n ComputedColorChannels,\n Options,\n SpecifiedColorChannels\n} from './typedef';\n\n/* constants */\nimport {\n FN_COLOR,\n FN_MIX,\n SYN_COLOR_TYPE,\n SYN_FN_CALC,\n SYN_FN_LIGHT_DARK,\n SYN_FN_REL,\n SYN_FN_VAR,\n SYN_MIX,\n VAL_COMP,\n VAL_SPEC\n} from './constant';\nconst NAMESPACE = 'resolve';\nconst RGB_TRANSPARENT = 'rgba(0, 0, 0, 0)';\n\n/* regexp */\nconst REG_COLOR = new RegExp(`^(?:${SYN_COLOR_TYPE})$`);\nconst REG_FN_CALC = new RegExp(SYN_FN_CALC);\nconst REG_FN_COLOR =\n /^(?:(?:ok)?l(?:ab|ch)|color(?:-mix)?|hsla?|hwb|rgba?|var)\\(/;\nconst REG_FN_LIGHT_DARK = new RegExp(SYN_FN_LIGHT_DARK);\nconst REG_FN_REL = new RegExp(SYN_FN_REL);\nconst REG_FN_VAR = new RegExp(SYN_FN_VAR);\nconst REG_MIX = new RegExp(SYN_MIX);\n\n/**\n * resolve color\n * @param value - CSS color value\n * @param opt - options\n * @returns resolved color\n */\nexport const resolveColor = (\n value: string,\n opt: Options = {}\n): string | null => {\n if (!isString(value)) {\n throw new TypeError(`${value} is not a string.`);\n }\n value = value.trim();\n const {\n colorScheme = 'normal',\n currentColor = '',\n format = VAL_COMP,\n nullable = false\n } = opt;\n const cacheKey: string = createCacheKey(\n { namespace: NAMESPACE, name: 'resolve', value },\n opt\n );\n const cachedResult = getCache(cacheKey);\n if (cachedResult !== false) {\n return cachedResult.item as string | null;\n }\n // 1. var() resolution\n if (REG_FN_VAR.test(value)) {\n if (format === VAL_SPEC) {\n setCache(cacheKey, value);\n return value;\n }\n const resolvedVar = resolveVar(value, opt);\n if (resolvedVar === null) {\n const res =\n format === 'hex' || format === 'hexAlpha' || nullable\n ? null\n : RGB_TRANSPARENT;\n setCache(cacheKey, res);\n return res;\n }\n value = resolvedVar;\n }\n if (opt.format !== format) {\n opt.format = format;\n }\n value = value.toLowerCase();\n // 2. light-dark() resolution\n if (REG_FN_LIGHT_DARK.test(value) && value.endsWith(')')) {\n const colorParts = value.replace(REG_FN_LIGHT_DARK, '').replace(/\\)$/, '');\n const [light = '', dark = ''] = splitValue(colorParts, { delimiter: ',' });\n if (light && dark) {\n if (format === VAL_SPEC) {\n const lightColor = resolveColor(light, opt);\n const darkColor = resolveColor(dark, opt);\n const res =\n lightColor && darkColor\n ? `light-dark(${lightColor}, ${darkColor})`\n : '';\n setCache(cacheKey, res);\n return res;\n }\n const chosen = colorScheme === 'dark' ? dark : light;\n const resolved = resolveColor(chosen, opt);\n const res = resolved === null && !nullable ? RGB_TRANSPARENT : resolved;\n setCache(cacheKey, res);\n return res;\n }\n // fallback for invalid light-dark\n let invalidRes;\n if (format === VAL_SPEC) {\n invalidRes = '';\n } else if (format === 'hex' || format === 'hexAlpha') {\n invalidRes = null;\n } else {\n invalidRes = RGB_TRANSPARENT;\n }\n setCache(cacheKey, invalidRes);\n return invalidRes;\n }\n // 3. Relative Color resolution\n if (REG_FN_REL.test(value)) {\n const resolvedRel = resolveRelativeColor(value, opt, resolveColor);\n if (format === VAL_COMP) {\n const res =\n resolvedRel === null && !nullable ? RGB_TRANSPARENT : resolvedRel;\n setCache(cacheKey, res);\n return res;\n }\n if (format === VAL_SPEC) {\n const res = resolvedRel === null ? '' : resolvedRel;\n setCache(cacheKey, res);\n return res;\n }\n value = resolvedRel === null ? '' : resolvedRel;\n }\n // 4. calc() resolution\n if (REG_FN_CALC.test(value)) {\n value = cssCalc(value, opt);\n }\n // 5. Keyword & Color-space resolution\n let cs = '';\n let r = NaN;\n let g = NaN;\n let b = NaN;\n let alpha = NaN;\n if (value === 'transparent') {\n let res: string | null;\n switch (format) {\n case VAL_SPEC: {\n res = value;\n break;\n }\n case 'hex': {\n res = null;\n break;\n }\n case 'hexAlpha': {\n res = '#00000000';\n break;\n }\n default: {\n res = RGB_TRANSPARENT;\n }\n }\n setCache(cacheKey, res);\n return res;\n }\n if (value === 'currentcolor') {\n if (format === VAL_SPEC) {\n setCache(cacheKey, value);\n retLine truncated
+80
View File
@@ -0,0 +1,80 @@
/**
* typedef
*/
/**
* @typedef Options - options
* @property [alpha] - enable alpha
* @property [colorSpace] - color space
* @property [currentColor] - color for currentcolor
* @property [customProperty] - custom properties
* @property [d50] - white point in d50
* @property [dimension] - dimension
* @property [format] - output format
* @property [key] - key
*/
export interface Options {
alpha?: boolean;
colorScheme?: string;
colorSpace?: string;
currentColor?: string;
customProperty?: Record<string, string | ((K: string) => string)>;
d50?: boolean;
delimiter?: string | string[];
dimension?: Record<string, number | ((K: string) => number)>;
format?: string;
nullable?: boolean;
preserveComment?: boolean;
}
/**
* @type ColorChannels - color channels
*/
export type ColorChannels = [x: number, y: number, z: number, alpha: number];
/**
* @type StringColorChannels - color channels
*/
export type StringColorChannels = [
x: string,
y: string,
z: string,
alpha: string | undefined
];
/**
* @type StringColorSpacedChannels - specified value
*/
export type StringColorSpacedChannels = [
cs: string,
x: string,
y: string,
z: string,
alpha: string | undefined
];
/**
* @type ComputedColorChannels - computed value
*/
export type ComputedColorChannels = [
cs: string,
x: number,
y: number,
z: number,
alpha: number
];
/**
* @type SpecifiedColorChannels - specified value
*/
export type SpecifiedColorChannels = [
cs: string,
x: number | string,
y: number | string,
z: number | string,
alpha: number | string
];
/**
* @type MatchedRegExp - matched regexp array
*/
export type MatchedRegExp = [
match: string,
gr1: string,
gr2: string,
gr3: string,
gr4: string
];
+39
View File
@@ -0,0 +1,39 @@
import { Options } from './typedef.js';
/**
* split value
* NOTE: comments are stripped, it can be preserved if, in the options param,
* `delimiter` is either ',' or '/' and with `preserveComment` set to `true`
* @param value - CSS value
* @param [opt] - options
* @returns array of values
*/
export declare const splitValue: (value: string, opt?: Options) => string[];
/**
* extract dashed-ident tokens
* @param value - CSS value
* @returns array of dashed-ident tokens
*/
export declare const extractDashedIdent: (value: string) => string[];
/**
* round to specified precision
* @param value - numeric value
* @param bit - minimum bits
* @returns rounded value
*/
export declare const roundToPrecision: (value: number, bit?: number) => number;
/**
* interpolate hue
* @param hueA - hue value
* @param hueB - hue value
* @param arc - shorter | longer | increasing | decreasing
* @returns result - [hueA, hueB]
*/
export declare const interpolateHue: (hueA: number, hueB: number, arc?: string) => [number, number];
/**
* resolve length in pixels
* @param value - value
* @param unit - unit
* @param [opt] - options
* @returns pixelated value
*/
export declare const resolveLengthInPixels: (value: number | string, unit: string | undefined, opt?: Options) => number;
+222
View File
@@ -0,0 +1,222 @@
import { CacheItem, createCacheKey, getCache, setCache } from "./cache.js";
import { isString } from "./common.js";
import { TokenType, tokenize } from "@csstools/css-tokenizer";
//#region src/js/util.ts
/**
* util
*/
var { CloseParen: PAREN_CLOSE, Comma: COMMA, Comment: COMMENT, Delim: DELIM, EOF, Function: FUNC, OpenParen: PAREN_OPEN, Whitespace: W_SPACE } = TokenType;
var NAMESPACE = "util";
var DEC = 10;
var HEX = 16;
var DEG = 360;
var DEG_HALF = 180;
var REG_DASHED_IDENT = /--[\w-]+/g;
var REG_COMMA = /^,$/;
var REG_SLASH = /^\/$/;
var REG_WHITESPACE = /^\s+$/;
/**
* split value
* NOTE: comments are stripped, it can be preserved if, in the options param,
* `delimiter` is either ',' or '/' and with `preserveComment` set to `true`
* @param value - CSS value
* @param [opt] - options
* @returns array of values
*/
var splitValue = (value, opt = {}) => {
if (!isString(value)) throw new TypeError(`${value} is not a string.`);
const strValue = value.trim();
const { delimiter = " ", preserveComment = false } = opt;
const cacheKey = createCacheKey({
namespace: NAMESPACE,
name: "splitValue",
value: strValue
}, {
delimiter,
preserveComment
});
const cachedResult = getCache(cacheKey);
if (cachedResult instanceof CacheItem) return cachedResult.item;
let regDelimiter;
switch (delimiter) {
case ",":
regDelimiter = REG_COMMA;
break;
case "/":
regDelimiter = REG_SLASH;
break;
default: regDelimiter = REG_WHITESPACE;
}
const tokens = tokenize({ css: strValue });
let nest = 0;
let currentStr = "";
const res = [];
for (const [type, val] of tokens) switch (type) {
case COMMA:
case DELIM:
if (nest === 0 && regDelimiter.test(val)) {
res.push(currentStr.trim());
currentStr = "";
} else currentStr += val;
break;
case COMMENT:
if (preserveComment && (delimiter === "," || delimiter === "/")) currentStr += val;
break;
case FUNC:
case PAREN_OPEN:
currentStr += val;
nest++;
break;
case PAREN_CLOSE:
currentStr += val;
nest--;
break;
case W_SPACE:
if (regDelimiter.test(val)) {
if (nest === 0) {
if (currentStr) {
res.push(currentStr.trim());
currentStr = "";
}
} else currentStr += " ";
} else if (!currentStr.endsWith(" ")) currentStr += " ";
break;
default: if (type === EOF) {
res.push(currentStr.trim());
currentStr = "";
} else currentStr += val;
}
setCache(cacheKey, res);
return res;
};
/**
* extract dashed-ident tokens
* @param value - CSS value
* @returns array of dashed-ident tokens
*/
var extractDashedIdent = (value) => {
if (!isString(value)) throw new TypeError(`${value} is not a string.`);
const strValue = value.trim();
const cacheKey = createCacheKey({
namespace: NAMESPACE,
name: "extractDashedIdent",
value: strValue
});
const cachedResult = getCache(cacheKey);
if (cachedResult instanceof CacheItem) return cachedResult.item;
const matches = strValue.match(REG_DASHED_IDENT);
const res = matches ? [...new Set(matches)] : [];
setCache(cacheKey, res);
return res;
};
/**
* round to specified precision
* @param value - numeric value
* @param bit - minimum bits
* @returns rounded value
*/
var roundToPrecision = (value, bit = 0) => {
if (!Number.isFinite(value)) throw new TypeError(`${value} is not a finite number.`);
if (!Number.isFinite(bit)) throw new TypeError(`${bit} is not a finite number.`);
if (bit < 0 || bit > HEX) throw new RangeError(`${bit} is not between 0 and ${HEX}.`);
if (bit === 0) return Math.round(value);
const precision = bit === HEX ? 6 : bit < DEC ? 4 : 5;
return parseFloat(value.toPrecision(precision));
};
/**
* interpolate hue
* @param hueA - hue value
* @param hueB - hue value
* @param arc - shorter | longer | increasing | decreasing
* @returns result - [hueA, hueB]
*/
var interpolateHue = (hueA, hueB, arc = "shorter") => {
if (!Number.isFinite(hueA)) throw new TypeError(`${hueA} is not a finite number.`);
if (!Number.isFinite(hueB)) throw new TypeError(`${hueB} is not a finite number.`);
let a = hueA;
let b = hueB;
switch (arc) {
case "decreasing":
if (b > a) a += DEG;
break;
case "increasing":
if (b < a) b += DEG;
break;
case "longer":
if (b > a && b < a + DEG_HALF) a += DEG;
else if (b > a - DEG_HALF && b <= a) b += DEG;
break;
default: if (b > a + DEG_HALF) a += DEG;
else if (b < a - DEG_HALF) b += DEG;
}
return [a, b];
};
var absoluteFontSize = /* @__PURE__ */ new Map([
["xx-small", 9 / 16],
["x-small", 5 / 8],
["small", 13 / 16],
["medium", 1],
["large", 9 / 8],
["x-large", 3 / 2],
["xx-large", 2],
["xxx-large", 3]
]);
var relativeFontSize = /* @__PURE__ */ new Map([["smaller", 1 / 1.2], ["larger", 1.2]]);
var absoluteLength = /* @__PURE__ */ new Map([
["cm", 96 / 2.54],
["mm", 96 / 25.4],
["q", 96 / 101.6],
["in", 96],
["pc", 16],
["pt", 96 / 72],
["px", 1]
]);
var relativeLength = /* @__PURE__ */ new Map([
["rcap", 1],
["rch", .5],
["rem", 1],
["rex", .5],
["ric", 1],
["rlh", 1.2]
]);
/**
* resolve length in pixels
* @param value - value
* @param unit - unit
* @param [opt] - options
* @returns pixelated value
*/
var resolveLengthInPixels = (value, unit, opt = {}) => {
const { dimension = {} } = opt;
const { callback, em, rem, vh, vw } = dimension;
if (isString(value)) {
const str = value.toLowerCase().trim();
const ratio = absoluteFontSize.get(str);
if (ratio !== void 0) return ratio * rem;
const relRatio = relativeFontSize.get(str);
if (relRatio !== void 0) return relRatio * em;
return NaN;
}
if (Number.isFinite(value) && unit) {
const u = unit.toLowerCase();
if (Object.hasOwn(dimension, u)) return value * Number(dimension[u]);
if (typeof callback === "function") return value * (callback(u) ?? NaN);
const absRatio = absoluteLength.get(u);
if (absRatio !== void 0) return value * absRatio;
const relRatio = relativeLength.get(u);
if (relRatio !== void 0) return value * relRatio * rem;
const rUnitRatio = relativeLength.get(`r${u}`);
if (rUnitRatio !== void 0) return value * rUnitRatio * em;
switch (u) {
case "vb": return value * vh;
case "vi": return value * vw;
case "vmax": return value * Math.max(vh, vw);
case "vmin": return value * Math.min(vh, vw);
}
}
return NaN;
};
//#endregion
export { extractDashedIdent, interpolateHue, resolveLengthInPixels, roundToPrecision, splitValue };
//# sourceMappingURL=util.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"util.js","names":[],"sources":["../../../src/js/util.ts"],"sourcesContent":["/**\n * util\n */\n\nimport { TokenType, tokenize } from '@csstools/css-tokenizer';\nimport { CacheItem, createCacheKey, getCache, setCache } from './cache';\nimport { isString } from './common';\nimport { Options } from './typedef';\n\n/* constants */\nconst {\n CloseParen: PAREN_CLOSE,\n Comma: COMMA,\n Comment: COMMENT,\n Delim: DELIM,\n EOF,\n Function: FUNC,\n OpenParen: PAREN_OPEN,\n Whitespace: W_SPACE\n} = TokenType;\nconst NAMESPACE = 'util';\n\n/* numeric constants */\nconst DEC = 10;\nconst HEX = 16;\nconst DEG = 360;\nconst DEG_HALF = 180;\n\n/* regexp */\nconst REG_DASHED_IDENT = /--[\\w-]+/g;\nconst REG_COMMA = /^,$/;\nconst REG_SLASH = /^\\/$/;\nconst REG_WHITESPACE = /^\\s+$/;\n\n/**\n * split value\n * NOTE: comments are stripped, it can be preserved if, in the options param,\n * `delimiter` is either ',' or '/' and with `preserveComment` set to `true`\n * @param value - CSS value\n * @param [opt] - options\n * @returns array of values\n */\nexport const splitValue = (value: string, opt: Options = {}): string[] => {\n if (!isString(value)) {\n throw new TypeError(`${value} is not a string.`);\n }\n const strValue = value.trim();\n const { delimiter = ' ', preserveComment = false } = opt;\n const cacheKey: string = createCacheKey(\n {\n namespace: NAMESPACE,\n name: 'splitValue',\n value: strValue\n },\n { delimiter, preserveComment }\n );\n const cachedResult = getCache(cacheKey);\n if (cachedResult instanceof CacheItem) {\n return cachedResult.item as string[];\n }\n let regDelimiter;\n switch (delimiter) {\n case ',': {\n regDelimiter = REG_COMMA;\n break;\n }\n case '/': {\n regDelimiter = REG_SLASH;\n break;\n }\n default: {\n regDelimiter = REG_WHITESPACE;\n }\n }\n const tokens = tokenize({ css: strValue });\n let nest = 0;\n let currentStr = '';\n const res: string[] = [];\n for (const [type, val] of tokens) {\n switch (type) {\n case COMMA:\n case DELIM: {\n if (nest === 0 && regDelimiter.test(val)) {\n res.push(currentStr.trim());\n currentStr = '';\n } else {\n currentStr += val;\n }\n break;\n }\n case COMMENT: {\n if (preserveComment && (delimiter === ',' || delimiter === '/')) {\n currentStr += val;\n }\n break;\n }\n case FUNC:\n case PAREN_OPEN: {\n currentStr += val;\n nest++;\n break;\n }\n case PAREN_CLOSE: {\n currentStr += val;\n nest--;\n break;\n }\n case W_SPACE: {\n if (regDelimiter.test(val)) {\n if (nest === 0) {\n if (currentStr) {\n res.push(currentStr.trim());\n currentStr = '';\n }\n } else {\n currentStr += ' ';\n }\n } else if (!currentStr.endsWith(' ')) {\n currentStr += ' ';\n }\n break;\n }\n default: {\n if (type === EOF) {\n res.push(currentStr.trim());\n currentStr = '';\n } else {\n currentStr += val;\n }\n }\n }\n }\n setCache(cacheKey, res);\n return res;\n};\n\n/**\n * extract dashed-ident tokens\n * @param value - CSS value\n * @returns array of dashed-ident tokens\n */\nexport const extractDashedIdent = (value: string): string[] => {\n if (!isString(value)) {\n throw new TypeError(`${value} is not a string.`);\n }\n const strValue = value.trim();\n const cacheKey: string = createCacheKey({\n namespace: NAMESPACE,\n name: 'extractDashedIdent',\n value: strValue\n });\n const cachedResult = getCache(cacheKey);\n if (cachedResult instanceof CacheItem) {\n return cachedResult.item as string[];\n }\n const matches = strValue.match(REG_DASHED_IDENT);\n const res = matches ? [...new Set(matches)] : [];\n setCache(cacheKey, res);\n return res;\n};\n\n/**\n * round to specified precision\n * @param value - numeric value\n * @param bit - minimum bits\n * @returns rounded value\n */\nexport const roundToPrecision = (value: number, bit: number = 0): number => {\n if (!Number.isFinite(value)) {\n throw new TypeError(`${value} is not a finite number.`);\n }\n if (!Number.isFinite(bit)) {\n throw new TypeError(`${bit} is not a finite number.`);\n }\n if (bit < 0 || bit > HEX) {\n throw new RangeError(`${bit} is not between 0 and ${HEX}.`);\n }\n if (bit === 0) {\n return Math.round(value);\n }\n const precision = bit === HEX ? 6 : bit < DEC ? 4 : 5;\n return parseFloat(value.toPrecision(precision));\n};\n\n/**\n * interpolate hue\n * @param hueA - hue value\n * @param hueB - hue value\n * @param arc - shorter | longer | increasing | decreasing\n * @returns result - [hueA, hueB]\n */\nexport const interpolateHue = (\n hueA: number,\n hueB: number,\n Line truncated
+82
View File
@@ -0,0 +1,82 @@
{
"name": "@asamuzakjp/css-color",
"description": "CSS color - Resolve and convert CSS colors.",
"author": "asamuzaK",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/asamuzaK/cssColor.git"
},
"homepage": "https://github.com/asamuzaK/cssColor#readme",
"bugs": {
"url": "https://github.com/asamuzaK/cssColor/issues"
},
"files": [
"dist",
"src"
],
"type": "module",
"types": "./dist/esm/index.d.ts",
"exports": {
".": {
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.js"
},
"./package.json": "./package.json"
},
"dependencies": {
"@csstools/css-calc": "^3.3.0",
"@csstools/css-color-parser": "^4.1.10",
"@csstools/css-parser-algorithms": "^4.0.0",
"@csstools/css-tokenizer": "^4.0.0",
"lru-cache": "^11.5.2"
},
"devDependencies": {
"@tanstack/vite-config": "^0.6.0",
"@types/node": "^26.2.0",
"@vitest/coverage-istanbul": "^4.1.10",
"esbuild": "^0.28.1",
"eslint": "^9.39.5",
"eslint-plugin-regexp": "^3.1.1",
"globals": "^17.9.0",
"knip": "^6.32.0",
"mitata": "^1.0.34",
"neostandard": "^0.13.0",
"prettier": "^3.9.6",
"publint": "^0.3.23",
"rimraf": "^6.1.3",
"tsx": "^4.23.11",
"typescript": "^6.0.3",
"vite": "^8.2.1",
"vitest": "^4.1.10"
},
"packageManager": "pnpm@10.34.4",
"pnpm": {
"onlyBuiltDependencies": [
"esbuild",
"oxc-resolver",
"unrs-resolver"
],
"overrides": {
"ajv@>=7.0.0-alpha.0 <8.18.0": ">=8.18.0",
"lodash@>=4.0.0 <=4.17.23": ">=4.18.0",
"minimatch@<3.1.4": ">=3.1.4"
}
},
"scripts": {
"bench:color": "npx tsx --expose-gc ./benchmark/bench-color.ts",
"build": "pnpm run clean && pnpm run test && pnpm run knip && vite build && pnpm run publint",
"clean": "rimraf ./coverage ./dist",
"knip": "knip",
"prettier": "prettier . --ignore-unknown --write",
"publint": "publint --strict",
"test": "pnpm run prettier && pnpm run --stream \"/^test:.*/\"",
"test:eslint": "eslint ./src ./test --fix",
"test:types": "tsc",
"test:unit": "vitest"
},
"engines": {
"node": "^22.13.0 || >=24.0.0"
},
"version": "6.0.7"
}
+29
View File
@@ -0,0 +1,29 @@
/*!
* CSS color - Resolve, parse, convert CSS color.
* @license MIT
* @copyright asamuzaK (Kazz)
* @see {@link https://github.com/asamuzaK/cssColor/blob/main/LICENSE}
*/
import { cssCalc, cssVar } from './js/css-calc-var';
import { isGradient, resolveGradient } from './js/css-gradient';
import { isColor } from './js/resolve';
import {
extractDashedIdent,
resolveLengthInPixels,
splitValue
} from './js/util';
export { convert } from './js/convert';
export { resolve } from './js/resolve';
/* utils */
export const utils = {
cssCalc,
cssVar,
extractDashedIdent,
isColor,
isGradient,
resolveGradient,
resolveLengthInPixels,
splitValue
};
+117
View File
@@ -0,0 +1,117 @@
/**
* cache
*/
import { LRUCache } from 'lru-cache';
import { Options } from './typedef';
/* constants */
const CACHE_SIZE = 4096;
/**
* CacheItem
*/
export class CacheItem {
/* private */
#item: unknown;
constructor(item: unknown) {
this.#item = item;
}
get item() {
return this.#item;
}
}
/*
* lru cache instance
*/
export const lruCache = new LRUCache<string, CacheItem>({
max: CACHE_SIZE
});
/**
* set cache
* @param key - cache key
* @param value - value to cache
* @returns void
*/
export const setCache = (key: string, value: unknown): void => {
if (!key) {
return;
}
if (value instanceof CacheItem) {
lruCache.set(key, value);
} else {
lruCache.set(key, new CacheItem(value));
}
};
/**
* get cache
* @param key - cache key
* @returns cached item or false otherwise
*/
export const getCache = (key: string): CacheItem | false => {
if (!key) {
return false;
}
const item = lruCache.get(key);
if (item !== undefined) {
return item as CacheItem;
}
return false;
};
/**
* helper function to sort object keys alphabetically
* @param obj - Object
* @returns stringified JSON
*/
const stringifySorted = (obj: Record<string, unknown>): string => {
const keys = Object.keys(obj);
if (keys.length === 0) {
return '';
}
keys.sort();
let result = '';
for (const key of keys) {
result += `${key}:${JSON.stringify(obj[key])};`;
}
return result;
};
/**
* create cache key
* @param keyData - key data
* @param [opt] - options
* @returns cache key
*/
export const createCacheKey = (
keyData: Record<string, string>,
opt: Options = {}
): string => {
if (
!keyData ||
(opt.customProperty && typeof opt.customProperty.callback === 'function') ||
(opt.dimension && typeof opt.dimension.callback === 'function')
) {
return '';
}
const namespace = keyData.namespace || '';
const name = keyData.name || '';
const value = keyData.value || '';
if (!namespace && !name && !value) {
return '';
}
const baseKey = `${namespace}:${name}:${value}`;
const optStr = `${opt.format || ''}|${opt.colorSpace || ''}|${opt.colorScheme || ''}|${opt.currentColor || ''}|${opt.d50 ? '1' : '0'}|${opt.nullable ? '1' : '0'}|${opt.preserveComment ? '1' : '0'}|${opt.delimiter || ''}`;
const customPropStr = opt.customProperty
? stringifySorted(opt.customProperty as Record<string, unknown>)
: '';
const dimStr = opt.dimension
? stringifySorted(opt.dimension as Record<string, unknown>)
: '';
return `${baseKey}::${optStr}::${customPropStr}::${dimStr}`;
};
File diff suppressed because it is too large. Load diff
+31
View File
@@ -0,0 +1,31 @@
/**
* common
*/
/* numeric constants */
const TYPE_FROM = 8;
const TYPE_TO = -1;
/**
* get type
* @param o - object to check
* @returns type of object
*/
export const getType = (o: unknown): string =>
Object.prototype.toString.call(o).slice(TYPE_FROM, TYPE_TO);
/**
* is string
* @param o - object to check
* @returns result
*/
export const isString = (o: unknown): o is string =>
typeof o === 'string' || o instanceof String;
/**
* is string or number
* @param o - object to check
* @returns result
*/
export const isStringOrNumber = (o: unknown): boolean =>
isString(o) || typeof o === 'number';
+68
View File
@@ -0,0 +1,68 @@
/**
* constant
*/
/* values and units */
const _DIGIT = '(?:0|[1-9]\\d*)';
const _COMPARE = 'clamp|max|min';
const _EXPO = 'exp|hypot|log|pow|sqrt';
const _SIGN = 'abs|sign';
const _STEP = 'mod|rem|round';
const _TRIG = 'a?(?:cos|sin|tan)|atan2';
const _MATH = `${_COMPARE}|${_EXPO}|${_SIGN}|${_STEP}|${_TRIG}`;
const _CALC = `calc|${_MATH}`;
const _VAR = `var|${_CALC}`;
export const ANGLE = 'deg|g?rad|turn';
export const LENGTH =
'[cm]m|[dls]?v(?:[bhiw]|max|min)|in|p[ctx]|q|r?(?:[cl]h|cap|e[mx]|ic)';
export const NUM = `[+-]?(?:${_DIGIT}(?:\\.\\d*)?|\\.\\d+)(?:e-?${_DIGIT})?`;
export const NUM_POSITIVE = `\\+?(?:${_DIGIT}(?:\\.\\d*)?|\\.\\d+)(?:e-?${_DIGIT})?`;
export const NONE = 'none';
export const PCT = `${NUM}%`;
export const SYN_FN_CALC = `^(?:${_CALC})\\(|(?<=[*\\/\\s\\(])(?:${_CALC})\\(`;
export const SYN_FN_MATH_START = `^(?:${_MATH})\\($`;
export const SYN_FN_VAR = '^var\\(|(?<=[*\\/\\s\\(])var\\(';
export const SYN_FN_VAR_START = `^(?:${_VAR})\\(`;
/* colors */
const _ALPHA = `(?:\\s*\\/\\s*(?:${NUM}|${PCT}|${NONE}))?`;
const _ALPHA_LV3 = `(?:\\s*,\\s*(?:${NUM}|${PCT}))?`;
const _COLOR_FUNC = '(?:ok)?l(?:ab|ch)|color|hsla?|hwb|rgba?';
const _COLOR_KEY = '[a-z]+|#[\\da-f]{3}|#[\\da-f]{4}|#[\\da-f]{6}|#[\\da-f]{8}';
const _CS_HUE = '(?:ok)?lch|hsl|hwb';
const _CS_HUE_ARC = '(?:de|in)creasing|longer|shorter';
const _NUM_ANGLE = `${NUM}(?:${ANGLE})?`;
const _NUM_ANGLE_NONE = `(?:${NUM}(?:${ANGLE})?|${NONE})`;
const _NUM_PCT_NONE = `(?:${NUM}|${PCT}|${NONE})`;
export const CS_HUE = `(?:${_CS_HUE})(?:\\s(?:${_CS_HUE_ARC})\\shue)?`;
export const CS_HUE_CAPT = `(${_CS_HUE})(?:\\s(${_CS_HUE_ARC})\\shue)?`;
export const CS_LAB = '(?:ok)?lab';
export const CS_LCH = '(?:ok)?lch';
export const CS_SRGB = 'srgb(?:-linear)?';
export const CS_RGB = `(?:a98|prophoto)-rgb|display-p3|rec2020|${CS_SRGB}`;
export const CS_XYZ = 'xyz(?:-d(?:50|65))?';
export const CS_RECT = `${CS_LAB}|${CS_RGB}|${CS_XYZ}`;
export const CS_MIX = `${CS_HUE}|${CS_RECT}`;
export const FN_COLOR = 'color(';
export const FN_LIGHT_DARK = 'light-dark(';
export const FN_MIX = 'color-mix(';
export const FN_REL = `(?:${_COLOR_FUNC})\\(\\s*from\\s+`;
export const FN_REL_CAPT = `(${_COLOR_FUNC})\\(\\s*from\\s+`;
export const FN_VAR = 'var(';
export const SYN_FN_COLOR = `(?:${CS_RGB}|${CS_XYZ})(?:\\s+${_NUM_PCT_NONE}){3}${_ALPHA}`;
export const SYN_FN_LIGHT_DARK = '^light-dark\\(';
export const SYN_FN_REL = `^${FN_REL}|(?<=[\\s])${FN_REL}`;
export const SYN_HSL = `${_NUM_ANGLE_NONE}(?:\\s+${_NUM_PCT_NONE}){2}${_ALPHA}`;
export const SYN_HSL_LV3 = `${_NUM_ANGLE}(?:\\s*,\\s*${PCT}){2}${_ALPHA_LV3}`;
export const SYN_LCH = `(?:${_NUM_PCT_NONE}\\s+){2}${_NUM_ANGLE_NONE}${_ALPHA}`;
export const SYN_MOD = `${_NUM_PCT_NONE}(?:\\s+${_NUM_PCT_NONE}){2}${_ALPHA}`;
export const SYN_RGB_LV3 = `(?:${NUM}(?:\\s*,\\s*${NUM}){2}|${PCT}(?:\\s*,\\s*${PCT}){2})${_ALPHA_LV3}`;
export const SYN_COLOR_TYPE = `${_COLOR_KEY}|hsla?\\(\\s*${SYN_HSL_LV3}\\s*\\)|rgba?\\(\\s*${SYN_RGB_LV3}\\s*\\)|(?:hsla?|hwb)\\(\\s*${SYN_HSL}\\s*\\)|(?:(?:ok)?lab|rgba?)\\(\\s*${SYN_MOD}\\s*\\)|(?:ok)?lch\\(\\s*${SYN_LCH}\\s*\\)|color\\(\\s*${SYN_FN_COLOR}\\s*\\)`;
export const SYN_MIX_PART = `(?:${SYN_COLOR_TYPE})(?:\\s+${PCT})?`;
export const SYN_MIX = `color-mix\\(\\s*in\\s+(?:${CS_MIX})\\s*,\\s*${SYN_MIX_PART}\\s*,\\s*${SYN_MIX_PART}\\s*\\)`;
export const SYN_MIX_CAPT = `color-mix\\(\\s*in\\s+(${CS_MIX})\\s*,\\s*(${SYN_MIX_PART})\\s*,\\s*(${SYN_MIX_PART})\\s*\\)`;
/* formats */
export const VAL_COMP = 'computedValue';
export const VAL_MIX = 'mixValue';
export const VAL_SPEC = 'specifiedValue';
+320
View File
@@ -0,0 +1,320 @@
/**
* convert
*/
import { createCacheKey, getCache, setCache } from './cache';
import {
convertColorToHsl,
convertColorToHwb,
convertColorToLab,
convertColorToLch,
convertColorToOklab,
convertColorToOklch,
convertColorToRgb,
numberToHexString,
parseColorFunc,
parseColorValue
} from './color';
import { isString } from './common';
import { cssCalc, resolveVar } from './css-calc-var';
import { resolveRelativeColor } from './relative-color';
import { resolveColor } from './resolve';
import { ColorChannels, ComputedColorChannels, Options } from './typedef';
/* constants */
import { SYN_FN_CALC, SYN_FN_REL, SYN_FN_VAR, VAL_COMP } from './constant';
const NAMESPACE = 'convert';
/* regexp */
const REG_FN_CALC = new RegExp(SYN_FN_CALC);
const REG_FN_REL = new RegExp(SYN_FN_REL);
const REG_FN_VAR = new RegExp(SYN_FN_VAR);
/**
* pre process
* @param value - CSS color value
* @param [opt] - options
* @returns value
*/
export const preProcess = (value: string, opt: Options = {}): string | null => {
if (!isString(value)) {
return null;
}
value = value.trim();
if (!value) {
return null;
}
const cacheKey: string = createCacheKey(
{ namespace: NAMESPACE, name: 'preProcess', value },
opt
);
const cachedResult = getCache(cacheKey);
if (cachedResult !== false) {
return cachedResult.item as string | null;
}
let res: string | null = value;
if (REG_FN_VAR.test(value)) {
const resolved = resolveVar(value, opt);
if (isString(resolved)) {
res = resolved;
} else {
setCache(cacheKey, null);
return null;
}
}
if (isString(res)) {
if (REG_FN_REL.test(res)) {
const resolved = resolveRelativeColor(res, opt);
if (isString(resolved)) {
res = resolved;
} else {
setCache(cacheKey, null);
return null;
}
} else if (REG_FN_CALC.test(res)) {
res = cssCalc(res, opt);
}
}
if (isString(res)) {
if (res.startsWith('color-mix')) {
const resolvedColorMix = resolveColor(res, {
...opt,
format: VAL_COMP,
nullable: true
});
res = typeof resolvedColorMix === 'string' ? resolvedColorMix : null;
}
}
setCache(cacheKey, res);
return res;
};
/**
* converter factory to reduce boilerplate
* @param name - function name for cache
* @param format - color format
* @param convertFn - conversion function
* @returns color converter function
*/
const createColorConverter = (
name: string,
format: string,
convertFn: Function
) => {
const colorConverterFn = (
value: string,
opt: Options = {}
): ColorChannels => {
if (!isString(value)) {
throw new TypeError(`${value} is not a string.`);
}
const resolved = preProcess(value, opt);
if (resolved === null) {
return [0, 0, 0, 0];
}
const val = resolved.toLowerCase();
const cacheKey = createCacheKey(
{ namespace: NAMESPACE, name, value: val },
opt
);
const cached = getCache(cacheKey);
if (cached !== false) {
return cached.item as ColorChannels;
}
const result = convertFn(val, { ...opt, format }) as ColorChannels;
setCache(cacheKey, result);
return result;
};
return colorConverterFn;
};
/**
* convert number to hex string
* @param value - numeric value
* @returns hex string: 00..ff
*/
export const numberToHex = (value: number): string => numberToHexString(value);
/**
* convert color to hex
* @param value - CSS color value
* @param [opt] - options
* @param [opt.alpha] - enable alpha channel
* @returns #rrggbb | #rrggbbaa | null
*/
export const colorToHex = (value: string, opt: Options = {}): string | null => {
if (!isString(value)) {
throw new TypeError(`${value} is not a string.`);
}
const resolved = preProcess(value, opt);
if (resolved === null) {
return null;
}
const val = resolved.toLowerCase();
const cacheKey = createCacheKey(
{ namespace: NAMESPACE, name: 'colorToHex', value: val },
opt
);
const cached = getCache(cacheKey);
if (cached !== false) {
return cached.item as string | null;
}
const hex = resolveColor(val, {
...opt,
nullable: true,
format: opt.alpha ? 'hexAlpha' : 'hex'
});
if (isString(hex)) {
setCache(cacheKey, hex);
return hex;
}
setCache(cacheKey, null);
return null;
};
/**
* convert color to hsl
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [h, s, l, alpha]
*/
export const colorToHsl = createColorConverter(
'colorToHsl',
'hsl',
convertColorToHsl
);
/**
* convert color to hwb
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [h, w, b, alpha]
*/
export const colorToHwb = createColorConverter(
'colorToHwb',
'hwb',
convertColorToHwb
);
/**
* convert color to lab
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [l, a, b, alpha]
*/
export const colorToLab = createColorConverter(
'colorToLab',
'lab',
convertColorToLab
);
/**
* convert color to lch
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [l, c, h, alpha]
*/
export const colorToLch = createColorConverter(
'colorToLch',
'lch',
convertColorToLch
);
/**
* convert color to oklab
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [l, a, b, alpha]
*/
export const colorToOklab = createColorConverter(
'colorToOklab',
'oklab',
convertColorToOklab
);
/**
* convert color to oklch
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [l, c, h, alpha]
*/
export const colorToOklch = createColorConverter(
'colorToOklch',
'oklch',
convertColorToOklch
);
/**
* convert color to rgb
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [r, g, b, alpha]
*/
export const colorToRgb = createColorConverter(
'colorToRgb',
'rgb',
convertColorToRgb
);
/**
* convert color to xyz
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [x, y, z, alpha]
*/
export const colorToXyz = (value: string, opt: Options = {}): ColorChannels => {
if (!isString(value)) {
throw new TypeError(`${value} is not a string.`);
}
const resolved = preProcess(value, opt);
if (resolved === null) {
return [0, 0, 0, 0];
}
const val = resolved.toLowerCase();
const cacheKey = createCacheKey(
{ namespace: NAMESPACE, name: 'colorToXyz', value: val },
opt
);
const cached = getCache(cacheKey);
if (cached !== false) {
return cached.item as ColorChannels;
}
let parsed;
if (val.startsWith('color(')) {
parsed = parseColorFunc(val, opt);
} else {
parsed = parseColorValue(val, opt);
}
const [, ...xyz] = parsed as ComputedColorChannels;
setCache(cacheKey, xyz);
return xyz as ColorChannels;
};
/**
* convert color to xyz-d50
* @param value - CSS color value
* @param [opt] - options
* @returns ColorChannels - [x, y, z, alpha]
*/
export const colorToXyzD50 = (
value: string,
opt: Options = {}
): ColorChannels => {
opt.d50 = true;
return colorToXyz(value, opt);
};
/* convert */
export const convert = {
colorToHex,
colorToHsl,
colorToHwb,
colorToLab,
colorToLch,
colorToOklab,
colorToOklch,
colorToRgb,
colorToXyz,
colorToXyzD50,
numberToHex
};
File diff suppressed because it is too large. Load diff
+371
View File
@@ -0,0 +1,371 @@
/**
* css-gradient
*/
import { createCacheKey, getCache, setCache } from './cache';
import { isString } from './common';
import { isColor, resolveColor } from './resolve';
import { MatchedRegExp, Options } from './typedef';
import { splitValue } from './util';
/* constants */
import {
ANGLE,
CS_HUE,
CS_RECT,
LENGTH,
NUM,
NUM_POSITIVE,
PCT,
VAL_COMP,
VAL_SPEC
} from './constant';
const NAMESPACE = 'css-gradient';
const DIM_ANGLE = `${NUM}(?:${ANGLE})`;
const DIM_ANGLE_PCT = `${DIM_ANGLE}|${PCT}`;
const DIM_LEN = `${NUM}(?:${LENGTH})|0`;
const DIM_LEN_PCT = `${DIM_LEN}|${PCT}`;
const DIM_LEN_PCT_POSI = `${NUM_POSITIVE}(?:${LENGTH}|%)|0`;
const DIM_LEN_POSI = `${NUM_POSITIVE}(?:${LENGTH})|0`;
const CTR = 'center';
const L_R = 'left|right';
const T_B = 'top|bottom';
const S_E = 'start|end';
const AXIS_X = `${L_R}|x-(?:${S_E})`;
const AXIS_Y = `${T_B}|y-(?:${S_E})`;
const BLOCK = `block-(?:${S_E})`;
const INLINE = `inline-(?:${S_E})`;
const POS_1 = `${CTR}|${AXIS_X}|${AXIS_Y}|${BLOCK}|${INLINE}|${DIM_LEN_PCT}`;
const POS_2 = [
`(?:${CTR}|${AXIS_X})\\s+(?:${CTR}|${AXIS_Y})`,
`(?:${CTR}|${AXIS_Y})\\s+(?:${CTR}|${AXIS_X})`,
`(?:${CTR}|${AXIS_X}|${DIM_LEN_PCT})\\s+(?:${CTR}|${AXIS_Y}|${DIM_LEN_PCT})`,
`(?:${CTR}|${BLOCK})\\s+(?:${CTR}|${INLINE})`,
`(?:${CTR}|${INLINE})\\s+(?:${CTR}|${BLOCK})`,
`(?:${CTR}|${S_E})\\s+(?:${CTR}|${S_E})`
].join('|');
const POS_4 = [
`(?:${AXIS_X})\\s+(?:${DIM_LEN_PCT})\\s+(?:${AXIS_Y})\\s+(?:${DIM_LEN_PCT})`,
`(?:${AXIS_Y})\\s+(?:${DIM_LEN_PCT})\\s+(?:${AXIS_X})\\s+(?:${DIM_LEN_PCT})`,
`(?:${BLOCK})\\s+(?:${DIM_LEN_PCT})\\s+(?:${INLINE})\\s+(?:${DIM_LEN_PCT})`,
`(?:${INLINE})\\s+(?:${DIM_LEN_PCT})\\s+(?:${BLOCK})\\s+(?:${DIM_LEN_PCT})`,
`(?:${S_E})\\s+(?:${DIM_LEN_PCT})\\s+(?:${S_E})\\s+(?:${DIM_LEN_PCT})`
].join('|');
const RAD_EXTENT = '(?:clos|farth)est-(?:corner|side)';
const RAD_SIZE = [
`${RAD_EXTENT}(?:\\s+${RAD_EXTENT})?`,
`${DIM_LEN_POSI}`,
`(?:${DIM_LEN_PCT_POSI})\\s+(?:${DIM_LEN_PCT_POSI})`
].join('|');
const RAD_SHAPE = 'circle|ellipse';
const FROM_ANGLE = `from\\s+${DIM_ANGLE}`;
const AT_POSITION = `at\\s+(?:${POS_1}|${POS_2}|${POS_4})`;
const TO_SIDE_CORNER = `to\\s+(?:(?:${L_R})(?:\\s(?:${T_B}))?|(?:${T_B})(?:\\s(?:${L_R}))?)`;
const IN_COLOR_SPACE = `in\\s+(?:${CS_RECT}|${CS_HUE})`;
const LINE_SYNTAX_LINEAR = [
`(?:${DIM_ANGLE}|${TO_SIDE_CORNER})(?:\\s+${IN_COLOR_SPACE})?`,
`${IN_COLOR_SPACE}(?:\\s+(?:${DIM_ANGLE}|${TO_SIDE_CORNER}))?`
].join('|');
const LINE_SYNTAX_RADIAL = [
`(?:${RAD_SHAPE})(?:\\s+(?:${RAD_SIZE}))?(?:\\s+${AT_POSITION})?(?:\\s+${IN_COLOR_SPACE})?`,
`(?:${RAD_SIZE})(?:\\s+(?:${RAD_SHAPE}))?(?:\\s+${AT_POSITION})?(?:\\s+${IN_COLOR_SPACE})?`,
`${AT_POSITION}(?:\\s+${IN_COLOR_SPACE})?`,
`${IN_COLOR_SPACE}(?:\\s+${RAD_SHAPE})(?:\\s+(?:${RAD_SIZE}))?(?:\\s+${AT_POSITION})?`,
`${IN_COLOR_SPACE}(?:\\s+${RAD_SIZE})(?:\\s+(?:${RAD_SHAPE}))?(?:\\s+${AT_POSITION})?`,
`${IN_COLOR_SPACE}(?:\\s+${AT_POSITION})?`
].join('|');
const LINE_SYNTAX_CONIC = [
`${FROM_ANGLE}(?:\\s+${AT_POSITION})?(?:\\s+${IN_COLOR_SPACE})?`,
`${AT_POSITION}(?:\\s+${IN_COLOR_SPACE})?`,
`${IN_COLOR_SPACE}(?:\\s+${FROM_ANGLE})?(?:\\s+${AT_POSITION})?`
].join('|');
const DEFAULT_LINEAR = [/to\s+bottom/];
const DEFAULT_RADIAL = [/ellipse/, /farthest-corner/, /at\s+center/];
const DEFAULT_CONIC = [/at\s+center/];
/* type definitions */
/**
* @type ColorStopList - list of color stops
*/
type ColorStopList = [string, string, ...string[]];
/**
* @typedef ValidateGradientLine - validate gradient line
* @property line - gradient line
* @property valid - result
*/
interface ValidateGradientLine {
line: string;
valid: boolean;
}
/**
* @typedef ValidateColorStops - validate color stops
* @property colorStops - list of color stops
* @property valid - result
*/
interface ValidateColorStops {
colorStops: string[];
valid: boolean;
}
/**
* @typedef Gradient - parsed CSS gradient
* @property value - input value
* @property type - gradient type
* @property [gradientLine] - gradient line
* @property colorStopList - list of color stops
*/
interface Gradient {
value: string;
type: string;
gradientLine?: string;
colorStopList: ColorStopList;
}
/* regexp */
const IS_CONIC = /^(?:repeating-)?conic-gradient$/;
const IS_LINEAR = /^(?:repeating-)?linear-gradient$/;
const IS_RADIAL = /^(?:repeating-)?radial-gradient$/;
const REG_COLOR_HINT_CONIC = new RegExp(`^(?:${DIM_ANGLE_PCT})$`);
const REG_COLOR_HINT_NON_CONIC = new RegExp(`^(?:${DIM_LEN_PCT})$`);
const REG_DIM_CONIC = new RegExp(`(?:\\s+(?:${DIM_ANGLE_PCT})){1,2}$`);
const REG_DIM_NON_CONIC = new RegExp(`(?:\\s+(?:${DIM_LEN_PCT})){1,2}$`);
const REG_GRAD = /^(?:repeating-)?(?:conic|linear|radial)-gradient\(/;
const REG_GRAD_CAPT = /^((?:repeating-)?(?:conic|linear|radial)-gradient)\(/;
const REG_LINE_CONIC = new RegExp(`^(?:${LINE_SYNTAX_CONIC})$`);
const REG_LINE_LINEAR = new RegExp(`^(?:${LINE_SYNTAX_LINEAR})$`);
const REG_LINE_RADIAL = new RegExp(`^(?:${LINE_SYNTAX_RADIAL})$`);
/**
* get gradient type
* @param value - gradient value
* @returns gradient type
*/
export const getGradientType = (value: string): string => {
if (isString(value)) {
value = value.trim();
if (REG_GRAD.test(value)) {
const [, type] = value.match(REG_GRAD_CAPT) as MatchedRegExp;
return type;
}
}
return '';
};
/**
* validate gradient line
* @param value - gradient line value
* @param type - gradient type
* @returns result
*/
export const validateGradientLine = (
value: string,
type: string
): ValidateGradientLine => {
if (isString(value) && isString(type)) {
value = value.trim();
type = type.trim();
let reg: RegExp | null = null;
let defaultValues: RegExp[] = [];
if (IS_LINEAR.test(type)) {
reg = REG_LINE_LINEAR;
defaultValues = DEFAULT_LINEAR;
} else if (IS_RADIAL.test(type)) {
reg = REG_LINE_RADIAL;
defaultValues = DEFAULT_RADIAL;
} else if (IS_CONIC.test(type)) {
reg = REG_LINE_CONIC;
defaultValues = DEFAULT_CONIC;
}
if (reg) {
const valid = reg.test(value);
if (valid) {
let line = value;
for (const defaultValue of defaultValues) {
line = line.replace(defaultValue, '');
}
line = line.replace(/\s{2,}/g, ' ').trim();
return { line, valid };
}
return { valid, line: value };
}
}
return { line: value, valid: false };
};
/**
* validate color stop list
* @param list
* @param type
* @param [opt]
* @returns result
*/
export const validateColorStopList = (
list: string[],
type: string,
opt: Options = {}
): ValidateColorStops => {
if (Array.isArray(list) && list.length > 1) {
const isConic = IS_CONIC.test(type);
const regColorHint = isConic
? REG_COLOR_HINT_CONIC
: REG_COLOR_HINT_NON_CONIC;
const regDimension = isConic ? REG_DIM_CONIC : REG_DIM_NON_CONIC;
const valueList: string[] = [];
// State tracker: 'color' or 'hint'
let prevType = '';
for (let i = 0; i < list.length; i++) {
const item = list[i];
if (isString(item)) {
if (regColorHint.test(item)) {
// Hints cannot be the first item, and two hints cannot be adjacent
if (i === 0 || prevType === 'hint') {
return { colorStops: list, valid: false };
}
prevType = 'hint';
valueList.push(item);
} else {
const itemColor = item.replace(regDimension, '');
if (isColor(itemColor, { format: VAL_SPEC })) {
const resolvedColor = resolveColor(itemColor, opt) as string;
prevType = 'color';
valueList.push(item.replace(itemColor, resolvedColor));
} else {
return { colorStops: list, valid: false };
}
}
} else {
return { colorStops: list, valid: false };
}
}
// The last item must be a color, not a hint
if (prevType !== 'color') {
return { colorStops: list, valid: false };
}
return { valid: true, colorStops: valueList };
}
return { colorStops: list, valid: false };
};
/**
* parse CSS gradient
* @param value - gradient value
* @param [opt] - options
* @returns parsed result
*/
export const parseGradient = (
value: string,
opt: Options = {}
): Gradient | null => {
if (isString(value)) {
value = value.trim();
const cacheKey: string = createCacheKey(
{
namespace: NAMESPACE,
name: 'parseGradient',
value
},
opt
);
const cachedResult = getCache(cacheKey);
if (cachedResult !== false) {
return cachedResult.item as Gradient | null;
}
const type = getGradientType(value);
const gradValue = value.replace(REG_GRAD, '').replace(/\)$/, '');
if (type && gradValue) {
const [lineOrColorStop = '', ...itemList] = splitValue(gradValue, {
delimiter: ','
});
const isConic = IS_CONIC.test(type);
const regDimension = isConic ? REG_DIM_CONIC : REG_DIM_NON_CONIC;
let colorStop = '';
if (regDimension.test(lineOrColorStop)) {
const itemColor = lineOrColorStop.replace(regDimension, '');
if (isColor(itemColor, { format: VAL_SPEC })) {
const resolvedColor = resolveColor(itemColor, opt) as string;
colorStop = lineOrColorStop.replace(itemColor, resolvedColor);
}
} else if (isColor(lineOrColorStop, { format: VAL_SPEC })) {
colorStop = resolveColor(lineOrColorStop, opt) as string;
}
if (colorStop) {
itemList.unshift(colorStop);
const { colorStops, valid } = validateColorStopList(
itemList,
type,
opt
);
if (valid) {
const res: Gradient = {
value,
type,
colorStopList: colorStops as ColorStopList
};
setCache(cacheKey, res);
return res;
}
} else if (itemList.length > 1) {
const { line: gradientLine, valid: validLine } = validateGradientLine(
lineOrColorStop,
type
);
const { colorStops, valid: validColorStops } = validateColorStopList(
itemList,
type,
opt
);
if (validLine && validColorStops) {
const res: Gradient = {
value,
type,
gradientLine,
colorStopList: colorStops as ColorStopList
};
setCache(cacheKey, res);
return res;
}
}
}
setCache(cacheKey, null);
return null;
}
return null;
};
/**
* resolve CSS gradient
* @param value - CSS value
* @param [opt] - options
* @returns result
*/
export const resolveGradient = (value: string, opt: Options = {}): string => {
const { format = VAL_COMP } = opt;
const gradient = parseGradient(value, opt);
if (gradient) {
const { type = '', gradientLine = '', colorStopList = [] } = gradient;
if (type && Array.isArray(colorStopList) && colorStopList.length > 1) {
if (gradientLine) {
return `${type}(${gradientLine}, ${colorStopList.join(', ')})`;
}
return `${type}(${colorStopList.join(', ')})`;
}
}
if (format === VAL_SPEC) {
return '';
}
return 'none';
};
/**
* is CSS gradient
* @param value - CSS value
* @param [opt] - options
* @returns result
*/
export const isGradient = (value: string, opt: Options = {}): boolean => {
const gradient = parseGradient(value, opt);
return gradient !== null;
};
+668
View File
@@ -0,0 +1,668 @@
/**
* relative-color
*/
import { SyntaxFlag, color as colorParser } from '@csstools/css-color-parser';
import {
ComponentValue,
parseComponentValue
} from '@csstools/css-parser-algorithms';
import { CSSToken, TokenType, tokenize } from '@csstools/css-tokenizer';
import { createCacheKey, getCache, setCache } from './cache';
import { NAMED_COLORS, convertColorToRgb } from './color';
import { isString, isStringOrNumber } from './common';
import { resolveDimension, serializeCalc } from './css-calc-var';
import { roundToPrecision, splitValue } from './util';
import {
ColorChannels,
MatchedRegExp,
Options,
StringColorChannels
} from './typedef';
/* constants */
import {
CS_LAB,
CS_LCH,
FN_LIGHT_DARK,
FN_REL,
FN_REL_CAPT,
FN_VAR,
NONE,
SYN_COLOR_TYPE,
SYN_FN_MATH_START,
SYN_FN_VAR,
SYN_MIX,
VAL_SPEC
} from './constant';
const {
CloseParen: PAREN_CLOSE,
Comment: COMMENT,
Delim: DELIM,
Dimension: DIM,
EOF,
Function: FUNC,
Ident: IDENT,
Number: NUM,
OpenParen: PAREN_OPEN,
Percentage: PCT,
Whitespace: W_SPACE
} = TokenType;
const { HasNoneKeywords: KEY_NONE } = SyntaxFlag;
const NAMESPACE = 'relative-color';
/* constants */
const OCT = 8;
const DEC = 10;
const HEX = 16;
const MAX_PCT = 100;
const MAX_RGB = 255;
const COLOR_CHANNELS = new Map([
['color', ['r', 'g', 'b', 'alpha']],
['hsl', ['h', 's', 'l', 'alpha']],
['hsla', ['h', 's', 'l', 'alpha']],
['hwb', ['h', 'w', 'b', 'alpha']],
['lab', ['l', 'a', 'b', 'alpha']],
['lch', ['l', 'c', 'h', 'alpha']],
['oklab', ['l', 'a', 'b', 'alpha']],
['oklch', ['l', 'c', 'h', 'alpha']],
['rgb', ['r', 'g', 'b', 'alpha']],
['rgba', ['r', 'g', 'b', 'alpha']]
]);
/* type definitions */
/**
* @type NumberOrStringColorChannels - color channel
*/
type NumberOrStringColorChannels = ColorChannels & StringColorChannels;
/* regexp */
const REG_COLOR_CAPT = new RegExp(
`^${FN_REL}(${SYN_COLOR_TYPE}|${SYN_MIX})\\s+`
);
const REG_CS_HSL = /(?:hsla?|hwb)$/;
const REG_CS_CIE = new RegExp(`^(?:${CS_LAB}|${CS_LCH})$`);
const REG_FN_CALC_SUM = /^(?:abs|sig?n|cos|tan)\(/;
const REG_FN_MATH_START = new RegExp(SYN_FN_MATH_START);
const REG_FN_REL = new RegExp(FN_REL);
const REG_FN_REL_CAPT = new RegExp(`^${FN_REL_CAPT}`);
const REG_FN_REL_START = new RegExp(`^${FN_REL}`);
const REG_FN_VAR = new RegExp(SYN_FN_VAR);
/**
* resolve relative color channels
* @param value
* - CSS color value
* - system colors are not supported
* @param [opt] - options
* @param [opt.currentColor]
* - color to use for `currentcolor` keyword
* - if omitted, it will be treated as a missing color
* i.e. `rgb(none none none / none)`
* @param [opt.customProperty]
* - custom properties
* - pair of `--` prefixed property name and value,
* e.g. `customProperty: { '--some-color': '#0000ff' }`
* - and/or `callback` function to get the value of the custom property,
* e.g. `customProperty: { callback: someDeclaration.getPropertyValue }`
* @param [opt.dimension]
* - dimension, convert relative length to pixels
* - pair of unit and it's value as a number in pixels,
* e.g. `dimension: { em: 12, rem: 16, vw: 10.26 }`
* - and/or `callback` function to get the value as a number in pixels,
* e.g. `dimension: { callback: convertUnitToPixel }`
* @param [opt.format]
* - output format, one of below
* - `computedValue` (default), [computed value][139] of the color
* - `specifiedValue`, [specified value][140] of the color
* - `hex`, hex color notation, i.e. `rrggbb`
* - `hexAlpha`, hex color notation with alpha channel, i.e. `#rrggbbaa`
* @returns
* - one of rgba?(), #rrggbb(aa)?, color-name, '(empty-string)',
* color(color-space r g b / alpha), color(color-space x y z / alpha),
* lab(l a b / alpha), lch(l c h / alpha), oklab(l a b / alpha),
* oklch(l c h / alpha), null
* - in `computedValue`, values are numbers, however `rgb()` values are
* integers
* - in `specifiedValue`, returns `empty string` for unknown and/or invalid
* color
* - in `hex`, returns `null` for `transparent`, and also returns `null` if
* any of `r`, `g`, `b`, `alpha` is not a number
* - in `hexAlpha`, returns `#00000000` for `transparent`,
* however returns `null` if any of `r`, `g`, `b`, `alpha` is not a number
*/
export function resolveColorChannels(
tokens: CSSToken[],
opt: Options = {}
): NumberOrStringColorChannels | null {
if (!Array.isArray(tokens)) {
throw new TypeError(`${tokens} is not an array.`);
}
const { colorSpace = '', format = '' } = opt;
const colorChannel = COLOR_CHANNELS.get(colorSpace);
// invalid color channel
if (!colorChannel) {
return null;
}
const mathFunc = new Set();
const channels: [
(number | string)[],
(number | string)[],
(number | string)[],
(number | string)[]
] = [[], [], [], []];
let i = 0;
let nest = 0;
let func = '';
let precededPct = false;
for (const token of tokens) {
if (!Array.isArray(token)) {
throw new TypeError(`${token} is not an array.`);
}
const [type, value, , , detail] = token as [
TokenType,
string,
number,
number,
{ value: string | number } | undefined
];
const channel = channels[i];
if (Array.isArray(channel)) {
switch (type) {
case DELIM: {
if (func) {
if (
(value === '+' || value === '-') &&
precededPct &&
!REG_FN_CALC_SUM.test(func)
) {
return null;
}
precededPct = false;
channel.push(value);
}
break;
}
case DIM: {
if (!func || !REG_FN_CALC_SUM.test(func)) {
return null;
}
const resolvedValue = resolveDimension(token, opt);
if (isString(resolvedValue)) {
channel.push(resolvedValue);
} else {
channel.push(value);
}
break;
}
case FUNC: {
channel.push(value);
func = value;
nest++;
if (REG_FN_MATH_START.test(value)) {
mathFunc.add(nest);
}
break;
}
case IDENT: {
// invalid channel key
if (!colorChannel.includes(value)) {
return null;
}
channel.push(value);
if (!func) {
i++;
}
break;
}
case NUM: {
channel.push(Number(detail?.value));
if (!func) {
i++;
}
break;
}
case PAREN_OPEN: {
channel.push(value);
nest++;
break;
}
case PAREN_CLOSE: {
if (func) {
const lastValue = channel[channel.length - 1];
if (lastValue === ' ') {
channel[channel.length - 1] = value;
} else {
channel.push(value);
}
if (mathFunc.has(nest)) {
mathFunc.delete(nest);
}
nest--;
if (nest === 0) {
func = '';
i++;
}
}
break;
}
case PCT: {
if (!func) {
return null;
} else if (!REG_FN_CALC_SUM.test(func)) {
let lastValue: string | number | undefined;
for (let j = channel.length - 1; j >= 0; j--) {
if (channel[j] !== ' ') {
lastValue = channel[j];
break;
}
}
if (lastValue === '+' || lastValue === '-') {
return null;
} else if (lastValue === '*' || lastValue === '/') {
precededPct = false;
} else {
precededPct = true;
}
}
channel.push(Number(detail?.value) / MAX_PCT);
break;
}
case W_SPACE: {
if (channel.length && func) {
const lastValue = channel[channel.length - 1];
if (typeof lastValue === 'number') {
channel.push(value);
} else if (
isString(lastValue) &&
!lastValue.endsWith('(') &&
lastValue !== ' '
) {
channel.push(value);
}
}
break;
}
default: {
if (type !== COMMENT && type !== EOF && func) {
channel.push(value);
}
}
}
}
}
const channelValues = [];
for (const channel of channels) {
if (channel.length === 1) {
const [resolvedValue] = channel;
if (isStringOrNumber(resolvedValue)) {
channelValues.push(resolvedValue);
}
} else if (channel.length) {
const resolvedValue = serializeCalc(channel.join(''), {
format
});
channelValues.push(resolvedValue);
}
}
return channelValues as NumberOrStringColorChannels;
}
/**
* extract origin color
* @param value - CSS color value
* @param opt - options
* @param resolver - resolver function
* @returns origin color value
*/
export function extractOriginColor(
value: string,
opt: Options = {},
resolver: (v: string, o?: Options) => string | null = () => null
): string | null {
const { colorScheme = 'normal', currentColor = '', format = '' } = opt;
if (isString(value)) {
value = value.toLowerCase().trim();
if (!value) {
return null;
}
if (!REG_FN_REL_START.test(value)) {
return value;
}
} else {
return null;
}
const cacheKey: string = createCacheKey(
{
namespace: NAMESPACE,
name: 'extractOriginColor',
value
},
opt
);
const cachedResult = getCache(cacheKey);
if (cachedResult !== false) {
return cachedResult.item as string | null;
}
if (/currentcolor/.test(value)) {
if (currentColor) {
value = value.replace(/currentcolor/g, currentColor);
} else {
setCache(cacheKey, null);
return null;
}
}
let colorSpace = '';
if (REG_FN_REL_CAPT.test(value)) {
[, colorSpace] = value.match(REG_FN_REL_CAPT) as MatchedRegExp;
}
opt.colorSpace = colorSpace;
if (value.includes(FN_LIGHT_DARK)) {
const colorParts = value
.replace(new RegExp(`^${colorSpace}\\(`), '')
.replace(/\)$/, '');
const [, originColor = ''] = splitValue(colorParts);
const specifiedOriginColor = resolver(originColor, {
colorScheme,
format: VAL_SPEC
}) as string;
if (!specifiedOriginColor) {
setCache(cacheKey, null);
return null;
}
if (format === VAL_SPEC) {
value = value.replace(originColor, specifiedOriginColor);
} else {
const resolvedOriginColor = resolver(specifiedOriginColor, opt);
if (isString(resolvedOriginColor)) {
value = value.replace(originColor, resolvedOriginColor);
} else {
setCache(cacheKey, null);
return null;
}
}
}
if (REG_COLOR_CAPT.test(value)) {
const [, originColor] = value.match(REG_COLOR_CAPT) as MatchedRegExp;
const [, restValue] = value.split(originColor) as MatchedRegExp;
if (/^[a-z]+$/.test(originColor)) {
if (
!/^transparent$/.test(originColor) &&
!Object.hasOwn(NAMED_COLORS, originColor)
) {
setCache(cacheKey, null);
return null;
}
} else if (format === VAL_SPEC) {
const resolvedOriginColor = resolver(originColor, opt);
if (isString(resolvedOriginColor)) {
value = value.replace(originColor, resolvedOriginColor);
}
}
if (format === VAL_SPEC) {
const tokens = tokenize({ css: restValue });
const channelValues = resolveColorChannels(tokens, opt);
if (channelValues === null) {
setCache(cacheKey, null);
return null;
}
const [v1, v2, v3, v4] = channelValues;
let channelValue = '';
if (isStringOrNumber(v4)) {
channelValue = ` ${v1} ${v2} ${v3} / ${v4})`;
} else {
channelValue = ` ${channelValues.join(' ')})`;
}
if (restValue !== channelValue) {
value = value.replace(restValue, channelValue);
}
}
} else {
// nested relative color
const [, restValue] = value.split(REG_FN_REL_START) as MatchedRegExp;
const tokens = tokenize({ css: restValue });
const originColor: string[] = [];
let nest = 0;
let tokenIndex = 0;
for (const [type, tokenValue] of tokens) {
tokenIndex++;
switch (type) {
case FUNC:
case PAREN_OPEN: {
originColor.push(tokenValue);
nest++;
break;
}
case PAREN_CLOSE: {
const lastValue = originColor[originColor.length - 1];
if (lastValue === ' ') {
originColor[originColor.length - 1] = tokenValue;
} else if (isString(lastValue)) {
originColor.push(tokenValue);
}
nest--;
break;
}
case W_SPACE: {
const lastValue = originColor[originColor.length - 1];
if (
isString(lastValue) &&
!lastValue.endsWith('(') &&
lastValue !== ' '
) {
originColor.push(tokenValue);
}
break;
}
default: {
if (type !== COMMENT && type !== EOF) {
originColor.push(tokenValue);
}
}
}
if (nest === 0) {
break;
}
}
const resolvedOriginColor = resolveRelativeColor(
originColor.join('').trim(),
opt,
resolver
);
if (resolvedOriginColor === null) {
setCache(cacheKey, null);
return null;
}
const channelValues = resolveColorChannels(tokens.slice(tokenIndex), opt);
if (channelValues === null) {
setCache(cacheKey, null);
return null;
}
const [v1, v2, v3, v4] = channelValues;
let channelValue = '';
if (isStringOrNumber(v4)) {
channelValue = ` ${v1} ${v2} ${v3} / ${v4})`;
} else {
channelValue = ` ${channelValues.join(' ')})`;
}
value = value.replace(restValue, `${resolvedOriginColor}${channelValue}`);
}
setCache(cacheKey, value);
return value;
}
/**
* resolve relative color
* @param value - CSS relative color value
* @param opt - options
* @param resolver - resolver function
* @returns resolved value
*/
export function resolveRelativeColor(
value: string,
opt: Options = {},
resolver: (v: string, o?: Options) => string | null = () => null
): string | null {
const { format = '' } = opt;
if (isString(value)) {
if (REG_FN_VAR.test(value)) {
// var() must be resolved before resolveRelativeColor()
if (format !== VAL_SPEC) {
throw new SyntaxError(`Unexpected token ${FN_VAR} found.`);
}
return value;
} else if (!REG_FN_REL.test(value)) {
return value;
}
value = value.toLowerCase().trim();
} else {
throw new TypeError(`${value} is not a string.`);
}
const cacheKey: string = createCacheKey(
{
namespace: NAMESPACE,
name: 'resolveRelativeColor',
value
},
opt
);
const cachedResult = getCache(cacheKey);
if (cachedResult !== false) {
return cachedResult.item as string | null;
}
const originColor = extractOriginColor(value, opt, resolver);
if (originColor === null) {
setCache(cacheKey, null);
return null;
}
value = originColor;
if (format === VAL_SPEC) {
if (value.startsWith('rgba(')) {
value = value.replace('rgba(', 'rgb(');
} else if (value.startsWith('hsla(')) {
value = value.replace('hsla(', 'hsl(');
}
return value;
}
const tokens = tokenize({ css: value });
const components = parseComponentValue(tokens) as ComponentValue;
const parsedComponents = colorParser(components);
if (!parsedComponents) {
setCache(cacheKey, null);
return null;
}
const {
alpha: alphaComponent,
channels: channelsComponent,
colorNotation,
syntaxFlags
} = parsedComponents;
let alpha: number | string;
if (Number.isNaN(Number(alphaComponent))) {
if (syntaxFlags instanceof Set && syntaxFlags.has(KEY_NONE)) {
alpha = NONE;
} else {
alpha = 0;
}
} else {
alpha = roundToPrecision(Number(alphaComponent), OCT);
}
let v1: number | string;
let v2: number | string;
let v3: number | string;
[v1, v2, v3] = channelsComponent;
let resolvedValue;
if (REG_CS_CIE.test(colorNotation)) {
const hasNone = syntaxFlags instanceof Set && syntaxFlags.has(KEY_NONE);
if (Number.isNaN(v1)) {
if (hasNone) {
v1 = NONE;
} else {
v1 = 0;
}
} else {
v1 = roundToPrecision(v1, HEX);
}
if (Number.isNaN(v2)) {
if (hasNone) {
v2 = NONE;
} else {
v2 = 0;
}
} else {
v2 = roundToPrecision(v2, HEX);
}
if (Number.isNaN(v3)) {
if (hasNone) {
v3 = NONE;
} else {
v3 = 0;
}
} else {
v3 = roundToPrecision(v3, HEX);
}
if (alpha === 1) {
resolvedValue = `${colorNotation}(${v1} ${v2} ${v3})`;
} else {
resolvedValue = `${colorNotation}(${v1} ${v2} ${v3} / ${alpha})`;
}
} else if (REG_CS_HSL.test(colorNotation)) {
if (Number.isNaN(v1)) {
v1 = 0;
}
if (Number.isNaN(v2)) {
v2 = 0;
}
if (Number.isNaN(v3)) {
v3 = 0;
}
let [r, g, b] = convertColorToRgb(
`${colorNotation}(${v1} ${v2} ${v3} / ${alpha})`
) as ColorChannels;
r = roundToPrecision(r / MAX_RGB, DEC);
g = roundToPrecision(g / MAX_RGB, DEC);
b = roundToPrecision(b / MAX_RGB, DEC);
if (alpha === 1) {
resolvedValue = `color(srgb ${r} ${g} ${b})`;
} else {
resolvedValue = `color(srgb ${r} ${g} ${b} / ${alpha})`;
}
} else {
const cs = colorNotation === 'rgb' ? 'srgb' : colorNotation;
const hasNone = syntaxFlags instanceof Set && syntaxFlags.has(KEY_NONE);
if (Number.isNaN(v1)) {
if (hasNone) {
v1 = NONE;
} else {
v1 = 0;
}
} else {
v1 = roundToPrecision(v1, DEC);
}
if (Number.isNaN(v2)) {
if (hasNone) {
v2 = NONE;
} else {
v2 = 0;
}
} else {
v2 = roundToPrecision(v2, DEC);
}
if (Number.isNaN(v3)) {
if (hasNone) {
v3 = NONE;
} else {
v3 = 0;
}
} else {
v3 = roundToPrecision(v3, DEC);
}
if (alpha === 1) {
resolvedValue = `color(${cs} ${v1} ${v2} ${v3})`;
} else {
resolvedValue = `color(${cs} ${v1} ${v2} ${v3} / ${alpha})`;
}
}
setCache(cacheKey, resolvedValue);
return resolvedValue;
}
+351
View File
@@ -0,0 +1,351 @@
/**
* resolve
*/
import { createCacheKey, getCache, setCache } from './cache';
import {
NAMED_COLORS,
convertRgbToHex,
resolveColorFunc,
resolveColorMix,
resolveColorValue
} from './color';
import { isString } from './common';
import { cssCalc, resolveVar } from './css-calc-var';
import { resolveRelativeColor } from './relative-color';
import { splitValue } from './util';
import {
ComputedColorChannels,
Options,
SpecifiedColorChannels
} from './typedef';
/* constants */
import {
FN_COLOR,
FN_MIX,
SYN_COLOR_TYPE,
SYN_FN_CALC,
SYN_FN_LIGHT_DARK,
SYN_FN_REL,
SYN_FN_VAR,
SYN_MIX,
VAL_COMP,
VAL_SPEC
} from './constant';
const NAMESPACE = 'resolve';
const RGB_TRANSPARENT = 'rgba(0, 0, 0, 0)';
/* regexp */
const REG_COLOR = new RegExp(`^(?:${SYN_COLOR_TYPE})$`);
const REG_FN_CALC = new RegExp(SYN_FN_CALC);
const REG_FN_COLOR =
/^(?:(?:ok)?l(?:ab|ch)|color(?:-mix)?|hsla?|hwb|rgba?|var)\(/;
const REG_FN_LIGHT_DARK = new RegExp(SYN_FN_LIGHT_DARK);
const REG_FN_REL = new RegExp(SYN_FN_REL);
const REG_FN_VAR = new RegExp(SYN_FN_VAR);
const REG_MIX = new RegExp(SYN_MIX);
/**
* resolve color
* @param value - CSS color value
* @param opt - options
* @returns resolved color
*/
export const resolveColor = (
value: string,
opt: Options = {}
): string | null => {
if (!isString(value)) {
throw new TypeError(`${value} is not a string.`);
}
value = value.trim();
const {
colorScheme = 'normal',
currentColor = '',
format = VAL_COMP,
nullable = false
} = opt;
const cacheKey: string = createCacheKey(
{ namespace: NAMESPACE, name: 'resolve', value },
opt
);
const cachedResult = getCache(cacheKey);
if (cachedResult !== false) {
return cachedResult.item as string | null;
}
// 1. var() resolution
if (REG_FN_VAR.test(value)) {
if (format === VAL_SPEC) {
setCache(cacheKey, value);
return value;
}
const resolvedVar = resolveVar(value, opt);
if (resolvedVar === null) {
const res =
format === 'hex' || format === 'hexAlpha' || nullable
? null
: RGB_TRANSPARENT;
setCache(cacheKey, res);
return res;
}
value = resolvedVar;
}
if (opt.format !== format) {
opt.format = format;
}
value = value.toLowerCase();
// 2. light-dark() resolution
if (REG_FN_LIGHT_DARK.test(value) && value.endsWith(')')) {
const colorParts = value.replace(REG_FN_LIGHT_DARK, '').replace(/\)$/, '');
const [light = '', dark = ''] = splitValue(colorParts, { delimiter: ',' });
if (light && dark) {
if (format === VAL_SPEC) {
const lightColor = resolveColor(light, opt);
const darkColor = resolveColor(dark, opt);
const res =
lightColor && darkColor
? `light-dark(${lightColor}, ${darkColor})`
: '';
setCache(cacheKey, res);
return res;
}
const chosen = colorScheme === 'dark' ? dark : light;
const resolved = resolveColor(chosen, opt);
const res = resolved === null && !nullable ? RGB_TRANSPARENT : resolved;
setCache(cacheKey, res);
return res;
}
// fallback for invalid light-dark
let invalidRes;
if (format === VAL_SPEC) {
invalidRes = '';
} else if (format === 'hex' || format === 'hexAlpha') {
invalidRes = null;
} else {
invalidRes = RGB_TRANSPARENT;
}
setCache(cacheKey, invalidRes);
return invalidRes;
}
// 3. Relative Color resolution
if (REG_FN_REL.test(value)) {
const resolvedRel = resolveRelativeColor(value, opt, resolveColor);
if (format === VAL_COMP) {
const res =
resolvedRel === null && !nullable ? RGB_TRANSPARENT : resolvedRel;
setCache(cacheKey, res);
return res;
}
if (format === VAL_SPEC) {
const res = resolvedRel === null ? '' : resolvedRel;
setCache(cacheKey, res);
return res;
}
value = resolvedRel === null ? '' : resolvedRel;
}
// 4. calc() resolution
if (REG_FN_CALC.test(value)) {
value = cssCalc(value, opt);
}
// 5. Keyword & Color-space resolution
let cs = '';
let r = NaN;
let g = NaN;
let b = NaN;
let alpha = NaN;
if (value === 'transparent') {
let res: string | null;
switch (format) {
case VAL_SPEC: {
res = value;
break;
}
case 'hex': {
res = null;
break;
}
case 'hexAlpha': {
res = '#00000000';
break;
}
default: {
res = RGB_TRANSPARENT;
}
}
setCache(cacheKey, res);
return res;
}
if (value === 'currentcolor') {
if (format === VAL_SPEC) {
setCache(cacheKey, value);
return value;
}
if (currentColor) {
let resolvedCurrent;
if (currentColor.startsWith(FN_MIX)) {
resolvedCurrent = resolveColorMix(currentColor, opt, resolveColor);
} else if (currentColor.startsWith(FN_COLOR)) {
resolvedCurrent = resolveColorFunc(currentColor, opt);
} else {
resolvedCurrent = resolveColorValue(currentColor, opt);
}
if (resolvedCurrent === null) {
setCache(cacheKey, null);
return null;
}
[cs, r, g, b, alpha] = resolvedCurrent as ComputedColorChannels;
} else {
// value is handled below if not VAL_COMP
const res = format === VAL_COMP ? RGB_TRANSPARENT : value;
if (format === VAL_COMP) {
setCache(cacheKey, res);
return res;
}
}
} else if (format === VAL_SPEC) {
let res = '';
if (value.startsWith(FN_MIX)) {
const mixRes = resolveColorMix(value, opt, resolveColor);
res = mixRes ? (mixRes as string) : '';
} else if (value.startsWith(FN_COLOR)) {
const funcRes = resolveColorFunc(value, opt);
if (isString(funcRes)) {
res = funcRes;
} else if (Array.isArray(funcRes)) {
const [scs, rr, gg, bb, aa] = funcRes as SpecifiedColorChannels;
res =
aa === 1
? `color(${scs} ${rr} ${gg} ${bb})`
: `color(${scs} ${rr} ${gg} ${bb} / ${aa})`;
}
} else {
const rgb = resolveColorValue(value, opt);
if (isString(rgb)) {
res = rgb;
} else if (Array.isArray(rgb)) {
const [scs, rr, gg, bb, aa] = rgb as SpecifiedColorChannels;
if (scs === 'rgb') {
res =
aa === 1
? `${scs}(${rr}, ${gg}, ${bb})`
: `${scs}a(${rr}, ${gg}, ${bb}, ${aa})`;
} else {
res =
aa === 1
? `${scs}(${rr} ${gg} ${bb})`
: `${scs}(${rr} ${gg} ${bb} / ${aa})`;
}
}
}
setCache(cacheKey, res);
return res;
} else if (value.startsWith(FN_MIX)) {
if (currentColor) {
value = value.replace(/currentcolor/g, currentColor);
}
value = value.replace(/transparent/g, RGB_TRANSPARENT);
const resolvedMix = resolveColorMix(value, opt, resolveColor);
if (resolvedMix === null) {
setCache(cacheKey, null);
return null;
}
[cs, r, g, b, alpha] = resolvedMix as ComputedColorChannels;
} else if (value.startsWith(FN_COLOR)) {
const resolvedFunc = resolveColorFunc(value, opt);
if (resolvedFunc === null) {
setCache(cacheKey, null);
return null;
}
[cs, r, g, b, alpha] = resolvedFunc as ComputedColorChannels;
} else if (value) {
const resolvedVal = resolveColorValue(value, opt);
if (resolvedVal === null) {
setCache(cacheKey, null);
return null;
}
[cs, r, g, b, alpha] = resolvedVal as ComputedColorChannels;
}
// 6. Format Finalization
let finalRes: string | null = '';
switch (format) {
case 'hex':
case 'hexAlpha': {
if (
Number.isNaN(r) ||
Number.isNaN(g) ||
Number.isNaN(b) ||
Number.isNaN(alpha) ||
(format === 'hex' && alpha === 0)
) {
finalRes = null;
} else {
finalRes = convertRgbToHex([r, g, b, format === 'hex' ? 1 : alpha]);
}
break;
}
default: {
if (cs === 'rgb') {
finalRes =
alpha === 1
? `${cs}(${r}, ${g}, ${b})`
: `${cs}a(${r}, ${g}, ${b}, ${alpha})`;
} else if (['lab', 'lch', 'oklab', 'oklch'].includes(cs)) {
finalRes =
alpha === 1
? `${cs}(${r} ${g} ${b})`
: `${cs}(${r} ${g} ${b} / ${alpha})`;
} else {
finalRes =
alpha === 1
? `color(${cs} ${r} ${g} ${b})`
: `color(${cs} ${r} ${g} ${b} / ${alpha})`;
}
}
}
setCache(cacheKey, finalRes);
return finalRes;
};
/**
* resolve CSS color
* @param value - CSS color value. system colors are not supported
* @param opt - options
* @returns resolved value
*/
export const resolve = (value: string, opt: Options = {}): string | null => {
opt.nullable = false;
return resolveColor(value, opt);
};
/**
* is color
* @param value - CSS value
* @param opt - options
* @returns result
*/
export const isColor = (value: unknown, opt: Options = {}): boolean => {
if (!isString(value)) {
return false;
}
const str = value.toLowerCase().trim();
if (!str) {
return false;
}
if (/^[a-z]+$/.test(str)) {
return (
str === 'currentcolor' ||
str === 'transparent' ||
Object.hasOwn(NAMED_COLORS, str)
);
}
if (REG_COLOR.test(str) || REG_MIX.test(str)) {
return true;
}
if (REG_FN_COLOR.test(str)) {
const colorOpt = { ...opt, nullable: true };
if (!colorOpt.format) colorOpt.format = VAL_SPEC;
return !!resolveColor(str, colorOpt);
}
return false;
};
+88
View File
@@ -0,0 +1,88 @@
/**
* typedef
*/
/* type definitions */
/**
* @typedef Options - options
* @property [alpha] - enable alpha
* @property [colorSpace] - color space
* @property [currentColor] - color for currentcolor
* @property [customProperty] - custom properties
* @property [d50] - white point in d50
* @property [dimension] - dimension
* @property [format] - output format
* @property [key] - key
*/
export interface Options {
alpha?: boolean;
colorScheme?: string;
colorSpace?: string;
currentColor?: string;
customProperty?: Record<string, string | ((K: string) => string)>;
d50?: boolean;
delimiter?: string | string[];
dimension?: Record<string, number | ((K: string) => number)>;
format?: string;
nullable?: boolean;
preserveComment?: boolean;
}
/**
* @type ColorChannels - color channels
*/
export type ColorChannels = [x: number, y: number, z: number, alpha: number];
/**
* @type StringColorChannels - color channels
*/
export type StringColorChannels = [
x: string,
y: string,
z: string,
alpha: string | undefined
];
/**
* @type StringColorSpacedChannels - specified value
*/
export type StringColorSpacedChannels = [
cs: string,
x: string,
y: string,
z: string,
alpha: string | undefined
];
/**
* @type ComputedColorChannels - computed value
*/
export type ComputedColorChannels = [
cs: string,
x: number,
y: number,
z: number,
alpha: number
];
/**
* @type SpecifiedColorChannels - specified value
*/
export type SpecifiedColorChannels = [
cs: string,
x: number | string,
y: number | string,
z: number | string,
alpha: number | string
];
/**
* @type MatchedRegExp - matched regexp array
*/
export type MatchedRegExp = [
match: string,
gr1: string,
gr2: string,
gr3: string,
gr4: string
];
+347
View File
@@ -0,0 +1,347 @@
/**
* util
*/
import { TokenType, tokenize } from '@csstools/css-tokenizer';
import { CacheItem, createCacheKey, getCache, setCache } from './cache';
import { isString } from './common';
import { Options } from './typedef';
/* constants */
const {
CloseParen: PAREN_CLOSE,
Comma: COMMA,
Comment: COMMENT,
Delim: DELIM,
EOF,
Function: FUNC,
OpenParen: PAREN_OPEN,
Whitespace: W_SPACE
} = TokenType;
const NAMESPACE = 'util';
/* numeric constants */
const DEC = 10;
const HEX = 16;
const DEG = 360;
const DEG_HALF = 180;
/* regexp */
const REG_DASHED_IDENT = /--[\w-]+/g;
const REG_COMMA = /^,$/;
const REG_SLASH = /^\/$/;
const REG_WHITESPACE = /^\s+$/;
/**
* split value
* NOTE: comments are stripped, it can be preserved if, in the options param,
* `delimiter` is either ',' or '/' and with `preserveComment` set to `true`
* @param value - CSS value
* @param [opt] - options
* @returns array of values
*/
export const splitValue = (value: string, opt: Options = {}): string[] => {
if (!isString(value)) {
throw new TypeError(`${value} is not a string.`);
}
const strValue = value.trim();
const { delimiter = ' ', preserveComment = false } = opt;
const cacheKey: string = createCacheKey(
{
namespace: NAMESPACE,
name: 'splitValue',
value: strValue
},
{ delimiter, preserveComment }
);
const cachedResult = getCache(cacheKey);
if (cachedResult instanceof CacheItem) {
return cachedResult.item as string[];
}
let regDelimiter;
switch (delimiter) {
case ',': {
regDelimiter = REG_COMMA;
break;
}
case '/': {
regDelimiter = REG_SLASH;
break;
}
default: {
regDelimiter = REG_WHITESPACE;
}
}
const tokens = tokenize({ css: strValue });
let nest = 0;
let currentStr = '';
const res: string[] = [];
for (const [type, val] of tokens) {
switch (type) {
case COMMA:
case DELIM: {
if (nest === 0 && regDelimiter.test(val)) {
res.push(currentStr.trim());
currentStr = '';
} else {
currentStr += val;
}
break;
}
case COMMENT: {
if (preserveComment && (delimiter === ',' || delimiter === '/')) {
currentStr += val;
}
break;
}
case FUNC:
case PAREN_OPEN: {
currentStr += val;
nest++;
break;
}
case PAREN_CLOSE: {
currentStr += val;
nest--;
break;
}
case W_SPACE: {
if (regDelimiter.test(val)) {
if (nest === 0) {
if (currentStr) {
res.push(currentStr.trim());
currentStr = '';
}
} else {
currentStr += ' ';
}
} else if (!currentStr.endsWith(' ')) {
currentStr += ' ';
}
break;
}
default: {
if (type === EOF) {
res.push(currentStr.trim());
currentStr = '';
} else {
currentStr += val;
}
}
}
}
setCache(cacheKey, res);
return res;
};
/**
* extract dashed-ident tokens
* @param value - CSS value
* @returns array of dashed-ident tokens
*/
export const extractDashedIdent = (value: string): string[] => {
if (!isString(value)) {
throw new TypeError(`${value} is not a string.`);
}
const strValue = value.trim();
const cacheKey: string = createCacheKey({
namespace: NAMESPACE,
name: 'extractDashedIdent',
value: strValue
});
const cachedResult = getCache(cacheKey);
if (cachedResult instanceof CacheItem) {
return cachedResult.item as string[];
}
const matches = strValue.match(REG_DASHED_IDENT);
const res = matches ? [...new Set(matches)] : [];
setCache(cacheKey, res);
return res;
};
/**
* round to specified precision
* @param value - numeric value
* @param bit - minimum bits
* @returns rounded value
*/
export const roundToPrecision = (value: number, bit: number = 0): number => {
if (!Number.isFinite(value)) {
throw new TypeError(`${value} is not a finite number.`);
}
if (!Number.isFinite(bit)) {
throw new TypeError(`${bit} is not a finite number.`);
}
if (bit < 0 || bit > HEX) {
throw new RangeError(`${bit} is not between 0 and ${HEX}.`);
}
if (bit === 0) {
return Math.round(value);
}
const precision = bit === HEX ? 6 : bit < DEC ? 4 : 5;
return parseFloat(value.toPrecision(precision));
};
/**
* interpolate hue
* @param hueA - hue value
* @param hueB - hue value
* @param arc - shorter | longer | increasing | decreasing
* @returns result - [hueA, hueB]
*/
export const interpolateHue = (
hueA: number,
hueB: number,
arc: string = 'shorter'
): [number, number] => {
if (!Number.isFinite(hueA)) {
throw new TypeError(`${hueA} is not a finite number.`);
}
if (!Number.isFinite(hueB)) {
throw new TypeError(`${hueB} is not a finite number.`);
}
let a = hueA;
let b = hueB;
switch (arc) {
case 'decreasing': {
if (b > a) {
a += DEG;
}
break;
}
case 'increasing': {
if (b < a) {
b += DEG;
}
break;
}
case 'longer': {
if (b > a && b < a + DEG_HALF) {
a += DEG;
} else if (b > a - DEG_HALF && b <= a) {
b += DEG;
}
break;
}
case 'shorter':
default: {
if (b > a + DEG_HALF) {
a += DEG;
} else if (b < a - DEG_HALF) {
b += DEG;
}
}
}
return [a, b];
};
/* absolute font size to pixel ratio */
const absoluteFontSize = new Map([
['xx-small', 9 / 16],
['x-small', 5 / 8],
['small', 13 / 16],
['medium', 1],
['large', 9 / 8],
['x-large', 3 / 2],
['xx-large', 2],
['xxx-large', 3]
]);
/* relative font size to pixel ratio */
const relativeFontSize = new Map([
['smaller', 1 / 1.2],
['larger', 1.2]
]);
/* absolute length to pixel ratio */
const absoluteLength = new Map([
['cm', 96 / 2.54],
['mm', 96 / 25.4],
['q', 96 / 101.6],
['in', 96],
['pc', 16],
['pt', 96 / 72],
['px', 1]
]);
/* relative length to pixel ratio */
const relativeLength = new Map([
['rcap', 1],
['rch', 0.5],
['rem', 1],
['rex', 0.5],
['ric', 1],
['rlh', 1.2]
]);
/**
* resolve length in pixels
* @param value - value
* @param unit - unit
* @param [opt] - options
* @returns pixelated value
*/
export const resolveLengthInPixels = (
value: number | string,
unit: string | undefined,
opt: Options = {}
): number => {
const { dimension = {} } = opt;
const { callback, em, rem, vh, vw } = dimension as {
callback: (K: string) => number;
em: number;
rem: number;
vh: number;
vw: number;
};
if (isString(value)) {
const str = value.toLowerCase().trim();
const ratio = absoluteFontSize.get(str);
if (ratio !== undefined) {
return ratio * rem;
}
const relRatio = relativeFontSize.get(str);
if (relRatio !== undefined) {
return relRatio * em;
}
return Number.NaN;
}
if (Number.isFinite(value) && unit) {
const u = unit.toLowerCase();
if (Object.hasOwn(dimension, u)) {
return value * Number(dimension[u]);
}
if (typeof callback === 'function') {
return value * (callback(u) ?? Number.NaN);
}
const absRatio = absoluteLength.get(u);
if (absRatio !== undefined) {
return value * absRatio;
}
const relRatio = relativeLength.get(u);
if (relRatio !== undefined) {
return value * relRatio * rem;
}
const rUnitRatio = relativeLength.get(`r${u}`);
if (rUnitRatio !== undefined) {
return value * rUnitRatio * em;
}
switch (u) {
case 'vb': {
return value * vh;
}
case 'vi': {
return value * vw;
}
case 'vmax': {
return value * Math.max(vh, vw);
}
case 'vmin': {
return value * Math.min(vh, vw);
}
default:
}
}
// unsupported or invalid value
return Number.NaN;
};
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 asamuzaK (Kazz)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+259
View File
@@ -0,0 +1,259 @@
# DOM Selector
[![build](https://github.com/asamuzaK/domSelector/actions/workflows/node.js.yml/badge.svg)](https://github.com/asamuzaK/domSelector/actions/workflows/node.js.yml)
[![CodeQL](https://github.com/asamuzaK/domSelector/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/asamuzaK/domSelector/actions/workflows/github-code-scanning/codeql)
[![npm (scoped)](https://img.shields.io/npm/v/@asamuzakjp/dom-selector)](https://www.npmjs.com/package/@asamuzakjp/dom-selector)
A CSS selector engine built for strict specification compliance.
## Features
* **Strict Specification Compliance**: Strictly adheres to modern web standards. It accurately parses, evaluates, and extracts elements across complex combinations of pseudo-classes and HTML attributes. Features comprehensive support for CSS Selectors Level 4 (e.g., `:is()`, `:not()`, `:where()`, `:has()`) and Shadow DOM pseudo-classes (`:host`, `:host-context`).
* **Utility Functions**: Provides utility methods alongside standard querying, such as `check()` for AST evaluation and `extractSubjects()` for extracting subject keys from selectors.
* **jsdom's Default Engine**: Adopted as the CSS selector engine for [jsdom](https://github.com/jsdom/jsdom).
## Install
``` console
npm i @asamuzakjp/dom-selector
```
## Usage
``` javascript
import { DOMSelector } from '@asamuzakjp/dom-selector';
import { JSDOM } from 'jsdom';
const { window } = new JSDOM();
// Destructuring methods (all methods are bound to the instance)
const {
check, closest, extractSubjects, matches, querySelector, querySelectorAll, supports
} = new DOMSelector(window);
```
## API
### `new DOMSelector(window, document?, opt?)`
Creates an instance of the DOMSelector.
* `window` **{Window}** The window object.
* `document` **{Document}?** The document object. Defaults to window.document.
* `opt` **{object}?** Options:
* `opt.cacheSize` **{number}?** Maximum number of items to store in the internal cache. Default is 2048.
### `matches(selector, node, opt?)`
Equivalent to [Element.matches()](https://developer.mozilla.org/docs/Web/API/Element/matches).
* `selector` **{string}** CSS selector.
* `node` **{Element}** Element node.
* `opt` **{object}?** Options:
* `opt.noexcept` **{boolean}?** Do not throw exceptions.
* `opt.warn` **{boolean}?** Console warn (e.g. unsupported pseudo-class).
* **Returns** **{boolean}** `true` if matched, `false` otherwise.
### `closest(selector, node, opt?)`
Equivalent to [Element.closest()](https://developer.mozilla.org/docs/Web/API/Element/closest).
* `selector` **{string}** CSS selector.
* `node` **{Element}** Element node.
* `opt` **{object}?** Options:
* `opt.noexcept` **{boolean}?** Do not throw exceptions.
* `opt.warn` **{boolean}?** Console warn (e.g. unsupported pseudo-class).
* **Returns** **{Element | null}** The matched ancestor node or `null`.
### `querySelector(selector, node, opt?)`
Equivalent to [Document.querySelector()](https://developer.mozilla.org/docs/Web/API/Document/querySelector), [DocumentFragment.querySelector()](https://developer.mozilla.org/docs/Web/API/DocumentFragment/querySelector) and [Element.querySelector()](https://developer.mozilla.org/docs/Web/API/Element/querySelector).
* `selector` **{string}** CSS selector.
* `node` **{Document | DocumentFragment | Element}** Node to find within.
* `opt` **{object}?** Options:
* `opt.noexcept` **{boolean}?** Do not throw exceptions.
* `opt.warn` **{boolean}?** Console warn (e.g. unsupported pseudo-class).
* **Returns** **{Element | null}** The matched node or `null`.
### `querySelectorAll(selector, node, opt?)`
Equivalent to [Document.querySelectorAll()](https://developer.mozilla.org/docs/Web/API/Document/querySelectorAll), [DocumentFragment.querySelectorAll()](https://developer.mozilla.org/docs/Web/API/DocumentFragment/querySelectorAll) and [Element.querySelectorAll()](https://developer.mozilla.org/docs/Web/API/Element/querySelectorAll).
**NOTE**: Returns a standard `Array`, not a `NodeList`.
* `selector` **{string}** CSS selector.
* `node` **{Document | DocumentFragment | Element}** Node to find within.
* `opt` **{object}?** Options:
* `opt.noexcept` **{boolean}?** Do not throw exceptions.
* `opt.warn` **{boolean}?** Console warn (e.g. unsupported pseudo-class).
* **Returns** **{Array}** Array of matched nodes.
### `check(selector, node, opt?)`
Checks if an element matches a CSS selector and returns additional abstract syntax tree (AST) information.
**NOTE**: Any pseudo-elements in the selector are excluded from the matching evaluation.
* `selector` **{string}** CSS selector.
* `node` **{Element}** Element node.
* `opt` **{object}?** Options:
* `opt.noexcept` **{boolean}?** Do not throw exceptions.
* `opt.warn` **{boolean}?** Console warn (e.g. unsupported pseudo-class).
* **Returns** **{object}** An object containing the following properties:
* `match` **{boolean}** `true` if the element matches the selector, `false` otherwise.
* `pseudoElement` **{string | null}** The pseudo-element extracted from the selector, if any.
* `ast` **{object | null}** The parsed AST object.
### `extractSubjects(selector, caseSensitive?)`
Parses a selector and extracts the rightmost subject keys (Id, Class, Tag).
* `selector` **{string}** CSS selector.
* `caseSensitive` **{boolean}?** `true` if the tag key should be case sensitive. Defaults to `false`.
* **Returns** **{Array\<{id: string|null, className: string|null, tag: string|null}\>}** An array of extracted keys.
### `supports(selector)`
Checks if the given CSS selector is supported by this engine.
See the table below for the full list of supported selectors.
* `selector` **{string}** CSS selector.
* **Returns** **{boolean}** `true` if the selector is supported, `false` otherwise.
### `clear(clearAll?)`
Clears the internal caches to free up memory.
* `clearAll` **{boolean}?** Whether to clear all caches. If false, only cached matching results are cleared. Defaults to `false`.
* **Returns** **{void}**
## Supported CSS selectors
| Pattern | Supported | Note |
| :--- | :---: | :--- |
| `*` | ✓ | |
| `E` | ✓ | |
| <code>ns\|E</code> | ✓ | |
| <code>*\|E</code> | ✓ | |
| <code>\|E</code> | ✓ | |
| `E F` | ✓ | |
| `E > F` | ✓ | |
| `E + F` | ✓ | |
| `E ~ F` | ✓ | |
| <code>F \|\| E</code> | Unsupported | |
| `E.warning` | ✓ | |
| `E#myid` | ✓ | |
| `E[foo]` | ✓ | |
| `E[foo="bar"]` | ✓ | |
| `E[foo="bar" i]` | ✓ | |
| `E[foo="bar" s]` | ✓ | |
| `E[foo~="bar"]` | ✓ | |
| `E[foo^="bar"]` | ✓ | |
| `E[foo$="bar"]` | ✓ | |
| `E[foo*="bar"]` | ✓ | |
| <code>E[foo\|="en"]</code> | ✓ | |
| `E:is(s1, s2, …)` | ✓ | |
| `E:not(s1, s2, …)` | ✓ | |
| `E:where(s1, s2, …)` | ✓ | |
| `E:has(rs1, rs2, …)` | ✓ | |
| `E:defined` | Partially supported | Matching with MathML is not yet supported. |
| `E:dir(ltr)` | ✓ | |
| `E:lang(en)` | ✓ | |
| `E:any-link` | ✓ | |
| `E:link` | ✓ | |
| `E:visited` | ✓ | Returns `false` or `null` to prevent fingerprinting. |
| `E:local-link` | ✓ | |
| `E:target` | ✓ | |
| `E:target-within` | | Removed from spec. |
| `E:scope` | ✓ | |
| `E:hover` | ✓ | |
| `E:active` | ✓ | |
| `E:focus` | ✓ | |
| `E:focus-visible` | ✓ | |
| `E:focus-within` | ✓ | |
| `E:current` | Unsupported | |
| `E:current(s)` | Unsupported | |
| `E:past` | Unsupported | |
| `E:future` | Unsupported | |
| `E:open` | Partially supported | Matching with `<select>`, e.g. `select:open`, or `<input type="color">` are not supported. |
| `E:popover-open` | Unsupported | |
| `E:enabled`<br>`E:disabled` | ✓ | |
| `E:read-write`<br>`E:read-only` | ✓ | |
| `E:placeholder-shown` | ✓ | |
| `E:default` | ✓ | |
| `E:checked` | ✓ | |
| `E:indeterminate` | ✓ | |
| `E:blank` | Unsupported | |
| `E:valid`<br>`E:invalid` | ✓ | |
| `E:in-range`<br>`E:out-of-range` | ✓ | |
| `E:required`<br>`E:optional` | ✓ | |
| `E:user-valid`<br>`E:user-invalid` | Unsupported | |
| `E:root` | ✓ | |
| `E:empty` | ✓ | |
| `E:nth-child(n [of S]?)` | ✓ | |
| `E:nth-last-child(n [of S]?)` | ✓ | |
| `E:first-child` | ✓ | |
| `E:last-child` | ✓ | |
| `E:only-child` | ✓ | |
| `E:nth-of-type(n)` | ✓ | |
| `E:nth-last-of-type(n)` | ✓ | |
| `E:first-of-type` | ✓ | |
| `E:last-of-type` | ✓ | |
| `E:only-of-type` | ✓ | |
| `E:nth-col(n)` | Unsupported | |
| `E:nth-last-col(n)` | Unsupported | |
| `CE:state(v)` | ✓ | \*1 |
| `:host` | ✓ | |
| `:host(s)` | ✓ | |
| `:host(:state(v))` | ✓ | \*1 |
| `:host:has(rs1, rs2, ...)` | ✓ | |
| `:host(s):has(rs1, rs2, ...)` | ✓ | |
| `:host-context(s)` | ✓ | |
| `:host-context(s):has(rs1, rs2, ...)` | ✓ | |
| `&` | ✓ | Only supports outermost `&`, i.e. equivalent to `:scope` |
\*1: `ElementInternals.states`, i.e. `CustomStateSet`, is not implemented in jsdom, so you need to apply a patch in the custom element constructor.
``` javascript
class LabeledCheckbox extends window.HTMLElement {
#internals;
constructor() {
super();
this.#internals = this.attachInternals();
// patch CustomStateSet
if (!this.#internals.states) {
this.#internals.states = new Set();
}
this.addEventListener('click', this._onClick.bind(this));
}
get checked() {
return this.#internals.states.has('checked');
}
set checked(flag) {
if (flag) {
this.#internals.states.add('checked');
} else {
this.#internals.states.delete('checked');
}
}
_onClick(event) {
this.checked = !this.checked;
}
}
```
## Performance
See [benchmark](https://github.com/asamuzaK/domSelector/actions/workflows/benchmark.yml) for the latest results.
## Acknowledgments
The following resources have been of great help in the development of the DOM Selector.
* [CSSTree](https://github.com/csstree/csstree)
* [selery](https://github.com/danburzo/selery)
* [jsdom](https://github.com/jsdom/jsdom)
* [nwsapi](https://github.com/dperini/nwsapi)
---
Copyright (c) 2023 [asamuzaK (Kazz)](https://github.com/asamuzaK/)
+92
View File
@@ -0,0 +1,92 @@
{
"name": "@asamuzakjp/dom-selector",
"description": "A CSS selector engine.",
"author": "asamuzaK",
"license": "MIT",
"homepage": "https://github.com/asamuzaK/domSelector#readme",
"bugs": {
"url": "https://github.com/asamuzaK/domSelector/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/asamuzaK/domSelector.git"
},
"files": [
"src",
"types"
],
"type": "module",
"types": "./types/index.d.ts",
"exports": {
".": {
"types": "./types/index.d.ts",
"default": "./src/index.js"
},
"./package.json": "./package.json"
},
"dependencies": {
"bidi-js": "^1.0.3",
"css-tree": "^3.2.1",
"is-potential-custom-element-name": "^1.0.1",
"lru-cache": "^11.5.2"
},
"devDependencies": {
"@types/css-tree": "^2.3.11",
"@types/node": "^26.1.1",
"c8": "^12.0.0",
"chai": "^6.2.2",
"commander": "^15.0.0",
"eslint": "^9.39.5",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-jsdoc": "^63.2.1",
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-regexp": "^3.1.1",
"eslint-plugin-unicorn": "^65.0.1",
"globals": "^17.7.0",
"jsdom": "^29.1.1",
"mitata": "^1.0.34",
"mocha": "^11.7.6",
"neostandard": "^0.13.0",
"prettier": "^3.9.6",
"sinon": "^22.1.0",
"tinybench": "^6.1.2",
"typescript": "^7.0.2",
"wpt-runner": "^7.0.0"
},
"overrides": {
"c8": {
"yargs": "^18.0.0"
},
"eslint": {
"brace-expansion": "^1.1.16",
"js-yaml": "^4.3.0"
},
"jsdom": "$jsdom",
"mocha": {
"diff": "^8.0.3",
"js-yaml": "^4.3.0"
},
"serialize-javascript": "^7.0.4"
},
"scripts": {
"bench": "node --expose-gc benchmark/bench.js",
"bench:cache": "node --expose-gc benchmark/bench-cache.js",
"bench:nwsapi": "node --expose-gc benchmark/bench-nwsapi.js",
"bench:ps-form": "node --expose-gc benchmark/bench-default.js && node --expose-gc benchmark/bench-indeterminate.js && node --expose-gc benchmark/bench-valid-invalid.js",
"bench:ps-has": "node --expose-gc benchmark/bench-has.js",
"bench:ps-nth": "node --expose-gc benchmark/bench-nth-child.js && node --expose-gc benchmark/bench-nth-of-type.js",
"bench:ps-traverse": "node --expose-gc benchmark/bench-dir.js && node --expose-gc benchmark/bench-lang.js",
"bench:tl": "node --expose-gc benchmark/bench-testing-library.js",
"bench:universal": "node --expose-gc benchmark/bench-universal.js",
"build": "npm run tsc && npm run lint && npm test",
"lint": "eslint --fix .",
"test": "c8 --reporter=text mocha --exit test/**/*.test.js",
"test:wpt": "node test/wpt/wpt-runner.js",
"tsc": "node scripts/index clean --dir=types -i && npx tsc",
"update:wpt": "git submodule update --init --recursive --remote"
},
"engines": {
"node": "^22.13.0 || >=24.0.0"
},
"version": "8.3.2"
}
+379
View File
@@ -0,0 +1,379 @@
/*!
* DOM Selector - A CSS selector engine.
* @license MIT
* @copyright asamuzaK (Kazz)
* @see {@link https://github.com/asamuzaK/domSelector/blob/main/LICENSE}
*/
/* import */
import { LRUCache } from 'lru-cache';
import { Finder } from './js/finder.js';
import { Nwsapi } from './js/nwsapi.js';
import { extractSubjectsAst, parseSelector } from './js/parser.js';
import {
extractSubjectsRegExp,
filterSelector,
isSupportedAST
} from './js/selector.js';
import { collectAllDescendants, getType } from './js/utility.js';
/* constants */
import {
DOCUMENT_NODE,
ELEMENT_NODE,
TARGET_ALL,
TARGET_FIRST,
TARGET_LINEAL,
TARGET_SELF
} from './js/constant.js';
const CACHE_SIZE = 4096;
/* regexp */
const REG_SELECTOR = /[[\]():\\"'`]/;
const REG_UNIVERSAL = /^(?:\*\|)?\*$/;
/**
* @typedef {object} CheckResult
* @property {boolean} match - The match result.
* @property {string?} pseudoElement - The pseudo-element, if any.
* @property {object?} ast - The AST object.
*/
/* DOMSelector */
export class DOMSelector {
/* private fields */
#window;
#document;
#finder;
#idlUtils;
#nwsapi;
#cache;
/**
* Creates an instance of DOMSelector.
* @param {Window} window - The window object.
* @param {Document} document - The document object.
* @param {object} [opt] - Options.
*/
constructor(window, document, opt = {}) {
const { cacheSize, idlUtils } = opt;
this.#window = window;
this.#document = document ?? window.document;
this.#idlUtils = idlUtils;
this.#cache = new LRUCache({
max: cacheSize ?? CACHE_SIZE
});
this.#finder = new Finder(this.#window);
this.#nwsapi = new Nwsapi(this.#window, this.#document, cacheSize);
}
/**
* Wraps the node for IDL internal implementation if idlUtils is present.
* @private
* @param {Document|DocumentFragment|Element} node - The raw node.
* @returns {object} The wrapped or raw node.
*/
#wrapNode = node =>
this.#idlUtils ? this.#idlUtils.wrapperForImpl(node) : node;
/**
* Validates a node and returns an Error if invalid.
* @private
* @param {Document|DocumentFragment|Element} node - The node to check.
* @param {boolean} [element] - `true` if the node must be an Element.
* @returns {TypeError|null} Returns a TypeError if invalid, otherwise null.
*/
#validateNodeType = (node, element = false) => {
if (!node?.nodeType) {
return new this.#window.TypeError(`Unexpected type ${getType(node)}`);
}
if (element && node.nodeType !== ELEMENT_NODE) {
return new this.#window.TypeError(`Unexpected node ${node.nodeName}`);
}
return null;
};
/**
* Executes Nwsapi matching logic with caching and error wrapping.
* @private
* @param {string} selector - The CSS selector to match against.
* @param {Document|Element} node - The target node to check.
* @param {number} targetType - The target constant indicating the scope (e.g., TARGET_SELF).
* @param {function(object): (Array<Element>|Element|boolean|null)} callback - The callback function that executes the specific nwsapi method.
* @param {boolean} [isCheck] - True if is check method.
* @returns {{success: boolean, result: Array<Element>|Element|boolean|null}} An object indicating whether the execution succeeded and its result.
*/
#tryNwsapi = (selector, node, targetType, callback, isCheck = false) => {
const document = node.ownerDocument;
if (
node.isConnected &&
document === this.#document &&
document.contentType === 'text/html' &&
document.documentElement
) {
const cacheKey = `${isCheck ? 'check' : targetType}_${selector}`;
let filterMatches = this.#cache.get(cacheKey);
if (filterMatches === undefined) {
filterMatches = filterSelector(selector, targetType);
this.#cache.set(cacheKey, filterMatches);
}
if (filterMatches) {
try {
return { success: true, result: callback(node) };
} catch {
// fall through
}
}
}
return { success: false, result: null };
};
/**
* Encapsulates Finder traversal logic and error handling.
* @private
* @param {string} selector - The CSS selector to match against.
* @param {Document|DocumentFragment|Element} node - The node from which to start searching.
* @param {object} opt - Optional parameters.
* @param {number} targetType - The target constant indicating the scope (e.g., TARGET_FIRST, TARGET_ALL).
* @returns {Set<Element>|Array<Element>|Element|boolean|null} The search results from Finder, or the error handling return value.
*/
#findNodes = (selector, node, opt, targetType) => {
try {
return this.#finder.setup(selector, node, opt).find(targetType);
} catch (e) {
return this.#finder.onError(e, opt);
}
};
/**
* Clears the internal caches.
* @param {boolean} [clearAll] - Whether to clear all caches. If false,
* only cached matching results are cleared.
* @returns {void}
*/
clear = (clearAll = false) => {
if (clearAll) {
this.#cache.clear();
}
this.#finder.clearResults(true);
this.#nwsapi.clear(clearAll);
};
/**
* Parses a selector and extracts the rightmost subject keys (Id, Class, Tag).
* @param {string} selector - The CSS selector to parse.
* @param {boolean} caseSensitive - True if tag key should be case sensitive.
* @returns {Array<{id: string|null, className: string|null, tag: string|null}>} The list of extracted keys for each selector group.
*/
extractSubjects = (selector, caseSensitive = false) => {
if (!selector || typeof selector !== 'string') {
return [{ id: null, className: null, tag: null }];
}
const cacheKey = `extract_${selector}_${caseSensitive}`;
let subjects = this.#cache.get(cacheKey);
if (subjects !== undefined) {
return subjects;
}
subjects = [];
if (!REG_SELECTOR.test(selector)) {
subjects = extractSubjectsRegExp(selector, caseSensitive);
} else {
try {
const ast = parseSelector(selector);
subjects = extractSubjectsAst(ast);
} catch {
// fall through
}
}
if (!subjects.length) {
subjects.push({ id: null, className: null, tag: null });
}
this.#cache.set(cacheKey, subjects);
return subjects;
};
/**
* Checks if the given CSS selector is supported by this engine.
* @param {string} selector - The CSS selector to check.
* @returns {boolean} `true` if the selector is supported, `false` otherwise.
*/
supports = selector => {
if (typeof selector !== 'string') {
return false;
}
const cacheKey = `supports_${selector}`;
let isSupported = this.#cache.get(cacheKey);
if (isSupported !== undefined) {
return isSupported;
}
if (filterSelector(selector, TARGET_SELF)) {
isSupported = true;
} else {
try {
const ast = parseSelector(selector);
isSupported = isSupportedAST(ast);
} catch {
isSupported = false;
}
}
this.#cache.set(cacheKey, isSupported);
return isSupported;
};
/**
* Checks if an element matches a CSS selector.
* @param {string} selector - The CSS selector to check against.
* @param {Element} node - The element node to check.
* @param {object} [opt] - Optional parameters.
* @returns {CheckResult} An object containing the check result.
*/
check = (selector, node, opt = {}) => {
node = this.#wrapNode(node);
const error = this.#validateNodeType(node, true);
if (error) {
return this.#finder.onError(error, opt);
}
if (REG_UNIVERSAL.test(selector)) {
return {
ast: parseSelector(selector),
match: true,
pseudoElement: null
};
}
const nwsapiRes = this.#tryNwsapi(
selector,
node,
TARGET_SELF,
n => this.#nwsapi.match(selector, n),
true
);
if (nwsapiRes.success) {
let ast = null;
if (nwsapiRes.result) {
const astCacheKey = `check_ast_${selector}`;
ast = this.#cache.get(astCacheKey);
if (ast === undefined) {
ast = parseSelector(selector);
this.#cache.set(astCacheKey, ast);
}
}
return {
match: nwsapiRes.result,
ast,
pseudoElement: null
};
}
const options = { ...opt, check: true, noexcept: true, warn: false };
return this.#finder.setup(selector, node, options).find(TARGET_SELF);
};
/**
* Returns true if the element matches the selector.
* @param {string} selector - The CSS selector to match against.
* @param {Element} node - The element node to test.
* @param {object} [opt] - Optional parameters.
* @returns {boolean} True if the element matches, false otherwise.
*/
matches = (selector, node, opt = {}) => {
node = this.#wrapNode(node);
const error = this.#validateNodeType(node, true);
if (error) {
return this.#finder.onError(error, opt);
}
if (REG_UNIVERSAL.test(selector)) {
return true;
}
const nwsapiRes = this.#tryNwsapi(selector, node, TARGET_SELF, n =>
this.#nwsapi.match(selector, n)
);
if (nwsapiRes.success) {
return nwsapiRes.result;
}
const nodes = this.#findNodes(selector, node, opt, TARGET_SELF);
return !!(nodes && nodes.size > 0);
};
/**
* Traverses up the DOM tree to find the first node that matches the selector.
* @param {string} selector - The CSS selector to match against.
* @param {Element} node - The element from which to start traversing.
* @param {object} [opt] - Optional parameters.
* @returns {?Element} The first matching ancestor element, or `null`.
*/
closest = (selector, node, opt = {}) => {
node = this.#wrapNode(node);
const error = this.#validateNodeType(node, true);
if (error) {
return this.#finder.onError(error, opt);
}
if (REG_UNIVERSAL.test(selector)) {
return node;
}
const nwsapiRes = this.#tryNwsapi(selector, node, TARGET_LINEAL, n =>
this.#nwsapi.closest(selector, n)
);
if (nwsapiRes.success) {
return nwsapiRes.result;
}
const nodes = this.#findNodes(selector, node, opt, TARGET_LINEAL);
if (nodes && nodes.size) {
let refNode = node;
while (refNode) {
if (nodes.has(refNode)) {
return refNode;
}
refNode = refNode.parentNode;
}
}
return null;
};
/**
* Returns the first element within the subtree that matches the selector.
* @param {string} selector - The CSS selector to match.
* @param {Document|DocumentFragment|Element} node - The node to find within.
* @param {object} [opt] - Optional parameters.
* @returns {?Element} The first matching element, or `null`.
*/
querySelector = (selector, node, opt = {}) => {
node = this.#wrapNode(node);
const error = this.#validateNodeType(node);
if (error) {
return this.#finder.onError(error, opt);
}
if (REG_UNIVERSAL.test(selector)) {
return node.firstElementChild;
}
const nodes = this.#findNodes(selector, node, opt, TARGET_FIRST);
if (nodes && nodes.size) {
return nodes.values().next().value;
}
return null;
};
/**
* Returns an array of elements within the subtree that match the selector.
* Note: This method returns an Array, not a NodeList.
* @param {string} selector - The CSS selector to match.
* @param {Document|DocumentFragment|Element} node - The node to find within.
* @param {object} [opt] - Optional parameters.
* @returns {Array<Element>} An array of elements, or an empty array.
*/
querySelectorAll = (selector, node, opt = {}) => {
node = this.#wrapNode(node);
const error = this.#validateNodeType(node);
if (error) {
return this.#finder.onError(error, opt);
}
if (REG_UNIVERSAL.test(selector)) {
const document =
node.nodeType === DOCUMENT_NODE ? node : node.ownerDocument;
return collectAllDescendants(node, document);
}
const nodes = this.#findNodes(selector, node, opt, TARGET_ALL);
if (nodes && nodes.size) {
return [...nodes];
}
return [];
};
}
+286
View File
@@ -0,0 +1,286 @@
/**
* constant.js
*/
/* string */
export const ATRULE = 'Atrule';
export const ATTR_SELECTOR = 'AttributeSelector';
export const CLASS_SELECTOR = 'ClassSelector';
export const COMBINATOR = 'Combinator';
export const DIR_NEXT = 'next';
export const DIR_PREV = 'prev';
export const IDENT = 'Identifier';
export const ID_SELECTOR = 'IdSelector';
export const NEST_SELECTOR = 'NestingSelector';
export const NOT_SUPPORTED_ERR = 'NotSupportedError';
export const NTH = 'Nth';
export const OPERATOR = 'Operator';
export const PS_CLASS_SELECTOR = 'PseudoClassSelector';
export const PS_ELEMENT_SELECTOR = 'PseudoElementSelector';
export const RULE = 'Rule';
export const SCOPE = 'Scope';
export const SELECTOR = 'Selector';
export const SELECTOR_LIST = 'SelectorList';
export const STRING = 'String';
export const SYNTAX_ERR = 'SyntaxError';
export const TARGET_ALL = 'all';
export const TARGET_FIRST = 'first';
export const TARGET_LINEAL = 'lineal';
export const TARGET_SELF = 'self';
export const TYPE_SELECTOR = 'TypeSelector';
/* numeric */
export const BIT_01 = 1;
export const BIT_02 = 2;
export const BIT_04 = 4;
export const BIT_08 = 8;
export const BIT_16 = 0x10;
export const BIT_32 = 0x20;
export const BIT_FFFF = 0xffff;
export const DUO = 2;
export const HEX = 16;
export const TYPE_FROM = 8;
export const TYPE_TO = -1;
/* Node */
export const ELEMENT_NODE = 1;
export const TEXT_NODE = 3;
export const DOCUMENT_NODE = 9;
export const DOCUMENT_FRAGMENT_NODE = 11;
export const DOCUMENT_POSITION_PRECEDING = 2;
export const DOCUMENT_POSITION_CONTAINS = 8;
export const DOCUMENT_POSITION_CONTAINED_BY = 0x10;
/* NodeFilter */
export const FILTER_ACCEPT = 1;
export const FILTER_REJECT = 2;
export const FILTER_SKIP = 3;
export const SHOW_ALL = 0xffffffff;
export const SHOW_CONTAINER = 0x501;
export const SHOW_DOCUMENT = 0x100;
export const SHOW_DOCUMENT_FRAGMENT = 0x400;
export const SHOW_ELEMENT = 1;
/* selectors */
export const ALPHA_NUM = '[A-Z\\d]+';
export const CHILD_IDX = '(?:first|last|only)-(?:child|of-type)';
export const DIGIT = '(?:0|[1-9]\\d*)';
export const LANG_PART = `(?:-${ALPHA_NUM})*`;
export const PSEUDO_CLASS = `(?:any-)?link|${CHILD_IDX}|checked|empty|indeterminate|read-(?:only|write)|target`;
export const ANB = `[+-]?(?:${DIGIT}n?|n)|(?:[+-]?${DIGIT})?n\\s*[+-]\\s*${DIGIT}`;
// combinators
export const COMBO = '\\s?[\\s>~+]\\s?';
export const DESCEND = '\\s?[\\s>]\\s?';
export const SIBLING = '\\s?[+~]\\s?';
// LOGIC_IS: :is()
export const LOGIC_IS = `:is\\(\\s*[^)]+\\s*\\)`;
// N_TH: excludes An+B with selector list, e.g. :nth-child(2n+1 of .foo)
export const N_TH = `nth-(?:last-)?(?:child|of-type)\\(\\s*(?:even|odd|${ANB})\\s*\\)`;
// ATTR_TYPE: excludes [foo|=bar]
export const ATTR_TYPE = '\\[[^|\\]]+\\]';
// SUB_TYPE: attr, id, class, pseudo-class
export const SUB_TYPE = `${ATTR_TYPE}|[#.:][\\w-]+`;
export const SUB_TYPE_WO_PSEUDO = `${ATTR_TYPE}|[#.][\\w-]+`;
// TAG_TYPE: *, tag
export const TAG_TYPE_WO_UNIVERSAL = '[A-Za-z][\\w-]*';
export const TAG_TYPE = `\\*|${TAG_TYPE_WO_UNIVERSAL}`;
export const TAG_TYPE_I = '\\*|[A-Z][\\w-]*';
export const COMPOUND = `(?:${TAG_TYPE}|(?:${TAG_TYPE})?(?:${SUB_TYPE})+)`;
export const COMPOUND_L = `(?:${TAG_TYPE}|(?:${TAG_TYPE})?(?:${SUB_TYPE}|${LOGIC_IS})+)`;
export const COMPOUND_I = `(?:${TAG_TYPE_I}|(?:${TAG_TYPE_I})?(?:${SUB_TYPE})+)`;
export const COMPOUND_WO_PSEUDO = `(?:${TAG_TYPE}|(?:${TAG_TYPE})?(?:${SUB_TYPE_WO_PSEUDO})+)`;
export const COMPLEX = `${COMPOUND}(?:${COMBO}${COMPOUND})*`;
export const COMPLEX_L = `${COMPOUND_L}(?:${COMBO}${COMPOUND_L})*`;
export const HAS_COMPOUND = `has\\([\\s>]?\\s*${COMPOUND_WO_PSEUDO}\\s*\\)`;
export const LOGIC_COMPOUND = `(?:is|not)\\(\\s*${COMPOUND_L}(?:\\s*,\\s*${COMPOUND_L})*\\s*\\)`;
export const LOGIC_COMPLEX = `(?:is|not)\\(\\s*${COMPLEX_L}(?:\\s*,\\s*${COMPLEX_L})*\\s*\\)`;
/* forms and input types */
export const FORM_PARTS = Object.freeze([
'button',
'input',
'select',
'textarea'
]);
export const INPUT_BUTTON = Object.freeze(['button', 'reset', 'submit']);
export const INPUT_CHECK = Object.freeze(['checkbox', 'radio']);
export const INPUT_DATE = Object.freeze([
'date',
'datetime-local',
'month',
'time',
'week'
]);
export const INPUT_TEXT = Object.freeze([
'email',
'password',
'search',
'tel',
'text',
'url'
]);
export const INPUT_EDIT = Object.freeze([
...INPUT_DATE,
...INPUT_TEXT,
'number'
]);
export const INPUT_LTR = Object.freeze([
...INPUT_CHECK,
'color',
'date',
'image',
'number',
'range',
'time'
]);
/* Attribute names that must treet values as ASCII case-insensitive */
export const ATTR_VALUE_I = Object.freeze([
'accept',
'accept-charset',
'align',
'alink',
'axis',
'bgcolor',
'charset',
'checked',
'clear',
'codetype',
'color',
'compact',
'declare',
'defer',
'dir',
'direction',
'disabled',
'enctype',
'face',
'frame',
'hreflang',
'http-equiv',
'lang',
'language',
'link',
'media',
'method',
'multiple',
'nohref',
'noresize',
'noshade',
'nowrap',
'readonly',
'rel',
'rev',
'rules',
'scope',
'scrolling',
'selected',
'shape',
'target',
'text',
'type',
'valign',
'valuetype',
'vlink'
]);
/* logical combination pseudo-classes */
export const KEYS_LOGICAL = new Set(['has', 'is', 'not', 'where']);
/* lists of supported / unsupported pseudo-classes and pseudo-elements */
export const KEYS_PS_CLASS_SUPPORTED = new Set([
'active',
'any-link',
'checked',
'closed',
'default',
'defined',
'dir',
'disabled',
'empty',
'enabled',
'first-child',
'first-of-type',
'focus',
'focus-visible',
'focus-within',
'has',
'host',
'host-context',
'hover',
'in-range',
'indeterminate',
'invalid',
'is',
'lang',
'last-child',
'last-of-type',
'link',
'local-link',
'not',
'nth-child',
'nth-last-child',
'nth-last-of-type',
'nth-of-type',
'only-child',
'only-of-type',
'open',
'optional',
'out-of-range',
'placeholder-shown',
'read-only',
'read-write',
'required',
'root',
'scope',
'state',
'target',
'target-within',
'valid',
'visited',
'where'
]);
export const KEYS_PS_CLASS_UNSUPPORTED = new Set([
'autofill',
'blank',
'buffering',
'contains',
'current',
'fullscreen',
'future',
'has-slotted',
'heading',
'modal',
'muted',
'nth-col',
'nth-last-col',
'past',
'paused',
'picture-in-picture',
'playing',
'popover-open',
'seeking',
'stalled',
'user-invalid',
'user-valid',
'volume-locked',
'-webkit-autofill'
]);
export const KEYS_PS_ELEMENT_UNSUPPORTED = new Set([
'after',
'backdrop',
'before',
'cue',
'cue-region',
'file-selector-button',
'first-letter',
'first-line',
'marker',
'part',
'placeholder',
'selection',
'slotted',
'target-text'
]);
File diff suppressed because it is too large. Load diff
+154
View File
@@ -0,0 +1,154 @@
/**
* event.js
*/
/* constants */
const KEYS_MODIFIER = new Set([
'Alt',
'AltGraph',
'CapsLock',
'Control',
'Fn',
'FnLock',
'Hyper',
'Meta',
'NumLock',
'ScrollLock',
'Shift',
'Super',
'Symbol',
'SymbolLock'
]);
/**
* EventHandler
* Tracks window events to record recent user interactions.
*/
export class EventHandler {
/* private fields */
#window;
#event;
#eventHandlers;
#fucusEvent;
#lastFocusVisible;
#cleanupFunctions;
/**
* @param {object} window - The window object.
*/
constructor(window) {
this.#window = window;
this.#event = null;
this.#fucusEvent = null;
this.#lastFocusVisible = null;
this.#eventHandlers = [
{
keys: ['focus', 'focusin'],
handler: this.handleFocusEvent
},
{
keys: ['keydown', 'keyup'],
handler: this.handleKeyboardEvent
},
{
keys: ['mouseover', 'mousedown', 'mouseup', 'click', 'mouseout'],
handler: this.handleMouseEvent
}
];
this.registerEventListeners();
}
/**
* Gets the most recent non-modifier keyboard or mouse event.
* @returns {Event|null} The current event object.
*/
get currentEvent() {
return this.#event;
}
/**
* Gets the most recent focus event.
* @returns {Event|null} The current focus event object.
*/
get currentFocus() {
return this.#fucusEvent;
}
/**
* Gets the node that last had visible focus.
* @returns {Node|null} The last focused node.
*/
get lastFocusVisible() {
return this.#lastFocusVisible;
}
/**
* Sets the node that last had visible focus.
* @param {Node} node - The node to set as the last visible focus.
*/
set lastFocusVisible(node) {
this.#lastFocusVisible = node;
}
/**
* Handles focus events and updates the current focus state.
* @param {Event} evt - The focus event.
*/
handleFocusEvent = evt => {
this.#fucusEvent = evt;
};
/**
* Handles keyboard events and updates the current event state.
* @param {KeyboardEvent} evt - The keyboard event.
*/
handleKeyboardEvent = evt => {
const { key } = evt;
if (!KEYS_MODIFIER.has(key)) {
this.#event = evt;
}
};
/**
* Handles mouse events and updates the current event state.
* @param {MouseEvent} evt - The mouse event.
*/
handleMouseEvent = evt => {
this.#event = evt;
};
/**
* Registers all predefined event listeners on the window object.
*/
registerEventListeners = () => {
if (!this.#cleanupFunctions) {
this.#cleanupFunctions = [];
}
for (const { keys, handler } of this.#eventHandlers) {
for (const key of keys) {
this.#window.addEventListener(key, handler, {
capture: true,
passive: true
});
this.#cleanupFunctions.push(() => {
this.#window.removeEventListener(key, handler, { capture: true });
});
}
}
};
/**
* Removes all registered event listeners and resets all internal states.
*/
destroy = () => {
if (this.#cleanupFunctions) {
for (const cleanup of this.#cleanupFunctions) {
cleanup();
}
}
this.#cleanupFunctions = null;
this.#event = null;
this.#fucusEvent = null;
this.#lastFocusVisible = null;
};
}
File diff suppressed because it is too large. Load diff
+101
View File
@@ -0,0 +1,101 @@
/**
* mapper.js
*/
import { SelectorProcessor } from './processor.js';
import { parseSelector, walkAST } from './parser.js';
import { createHasValidator } from './selector.js';
/**
* Mapper
* Maps CSS selectors to parsed ASTs and sets up node arrays.
*/
export class Mapper {
#context;
#processor;
/**
* @param {import('./finder.js').Finder} context - The Finder instance.
*/
constructor(context) {
this.#context = context;
this.#processor = new SelectorProcessor(context);
}
/**
* Gets the corresponding AST and empty nodes array for the given selector.
* @param {string} selector - The CSS selector string.
* @returns {Array} An array containing the AST, empty nodes array, and selector AST.
*/
correspond(selector) {
const ctx = this.#context;
const nodes = [];
let ast = null;
// Check cache.
if (ctx.documentCache.has(ctx.document)) {
const cachedItem = ctx.documentCache.get(ctx.document);
if (cachedItem && cachedItem.has(selector)) {
const item = cachedItem.get(selector);
ast = item.ast;
ctx.invalidate = item.invalidate;
ctx.selectorAST = item.selectorAST;
}
}
// Clear flags for reuse if cache hit.
if (ast) {
const l = ast.length;
for (let i = 0; i < l; i++) {
ast[i].dir = null;
ast[i].filtered = false;
ast[i].find = false;
nodes[i] = [];
}
return [ast, nodes, ctx.selectorAST];
}
// Parse selector and build metadata.
const selectorAST = parseSelector(selector);
const { branches, info } = walkAST(
selectorAST,
true,
createHasValidator(ctx.window)
);
const {
hasHasPseudoFunc,
hasLogicalPseudoFunc,
hasNestingSelector,
hasNthChildOfSelector,
hasStatePseudoClass,
hasUnsupportedPseudoClass
} = info;
// Determine invalidation flags.
ctx.invalidate =
hasHasPseudoFunc ||
hasNestingSelector ||
hasStatePseudoClass ||
hasUnsupportedPseudoClass ||
!!(hasLogicalPseudoFunc && hasNthChildOfSelector);
// Process branches.
const processed = this.#processor.process(branches, selector);
ast = processed.ast;
const descendant = processed.descendant;
// Store in cache.
let cachedItem;
if (ctx.documentCache.has(ctx.document)) {
cachedItem = ctx.documentCache.get(ctx.document);
} else {
cachedItem = new Map();
}
cachedItem.set(selector, {
ast,
descendant,
invalidate: ctx.invalidate,
selectorAST
});
ctx.documentCache.set(ctx.document, cachedItem);
// Initialize nodes.
const l = ast.length;
for (let i = 0; i < l; i++) {
nodes[i] = [];
}
return [ast, nodes, selectorAST];
}
}
+768
View File
@@ -0,0 +1,768 @@
/**
* matcher.js
*/
/* import */
import { generateCSS, parseAstName, unescapeSelector } from './parser.js';
import {
generateException,
getDirectionality,
getLanguageAttribute,
getType,
isContentEditable,
isNamespaceDeclared
} from './utility.js';
/* constants */
import {
ALPHA_NUM,
ATTR_VALUE_I,
INPUT_EDIT,
LANG_PART,
NOT_SUPPORTED_ERR,
PS_ELEMENT_SELECTOR,
STRING,
SYNTAX_ERR
} from './constant.js';
const KEYS_ATTR_VALUE_I = new Set(ATTR_VALUE_I);
const KEYS_INPUT_EDIT = new Set(INPUT_EDIT);
/* regexp */
const REG_LANG_VALID = new RegExp(`^(?:\\*-)?${ALPHA_NUM}${LANG_PART}$`, 'i');
/* cache */
const astMetaCache = new WeakMap();
/**
* Validates a pseudo-element selector.
* @param {string} astName - The name of the pseudo-element from the AST.
* @param {string} astType - The type of the selector from the AST.
* @param {object} [opt] - Optional parameters.
* @param {boolean} [opt.forgive] - If true, ignores unknown pseudo-elements.
* @param {object} [opt.globalObject] - The global object.
* @param {boolean} [opt.warn] - If true, throws an error for unsupported ones.
* @throws {DOMException} If the selector is invalid or unsupported.
* @returns {void}
*/
export const matchPseudoElementSelector = (
astName,
astType,
{ forgive, globalObject, warn } = {}
) => {
if (astType !== PS_ELEMENT_SELECTOR) {
// Ensure the AST node is a pseudo-element selector.
throw new TypeError(`Unexpected ast type ${getType(astType)}`);
}
switch (astName) {
case 'after':
case 'backdrop':
case 'before':
case 'cue':
case 'cue-region':
case 'first-letter':
case 'first-line':
case 'file-selector-button':
case 'marker':
case 'placeholder':
case 'selection':
case 'target-text': {
// Warn if the pseudo-element is known but unsupported.
if (warn) {
throw generateException(
`Unsupported pseudo-element ::${astName}`,
NOT_SUPPORTED_ERR,
globalObject
);
}
break;
}
case 'part':
case 'slotted': {
// Warn if the functional pseudo-element is known but unsupported.
if (warn) {
throw generateException(
`Unsupported pseudo-element ::${astName}()`,
NOT_SUPPORTED_ERR,
globalObject
);
}
break;
}
default: {
// Handle vendor-prefixed unknown pseudo-elements.
if (astName.startsWith('-webkit-')) {
if (warn) {
throw generateException(
`Unsupported pseudo-element ::${astName}`,
NOT_SUPPORTED_ERR,
globalObject
);
}
} else if (!forgive) {
// Throw an error for unknown pseudo-elements if not forgiven.
throw generateException(
`Unknown pseudo-element ::${astName}`,
SYNTAX_ERR,
globalObject
);
}
}
}
};
/**
* Matches the :dir() pseudo-class against an element's directionality.
* @param {object} ast - The AST object for the pseudo-class.
* @param {object} node - The element node to match against.
* @param {WeakMap} [dirCache] - Cache for directionality.
* @throws {TypeError} If the AST does not contain a valid direction value.
* @returns {boolean} - True if matches, otherwise false.
*/
export const matchDirectionPseudoClass = (
ast,
node,
dirCache = new WeakMap()
) => {
const { name } = ast;
if (!name) {
const type = name === '' ? '(empty String)' : getType(name);
throw new TypeError(`Unexpected ast type ${type}`);
}
const dir = getDirectionality(node, dirCache);
return name === dir;
};
/**
* Matches the :lang() pseudo-class against an element's language attribute.
* @param {object} ast - The AST object for the pseudo-class child.
* @param {object} node - The element node to match against.
* @param {WeakMap} [langCache] - Cache for language attributes.
* @throws {TypeError} If the AST does not contain a valid language value.
* @returns {boolean} - True if matches, otherwise false.
*/
export const matchLanguagePseudoClass = (
ast,
node,
langCache = new WeakMap()
) => {
const elementLang = getLanguageAttribute(node, langCache);
if (elementLang === null) {
return false;
}
let meta = astMetaCache.get(ast);
if (meta === undefined) {
meta = {};
astMetaCache.set(ast, meta);
}
if (meta.langRegex !== undefined) {
if (meta.langPattern === '*') {
return elementLang !== '';
}
if (meta.langRegex === null) {
return false;
}
return meta.langRegex.test(elementLang);
}
const { name, type, value } = ast;
const langPattern =
type === STRING && value !== undefined ? value : unescapeSelector(name);
meta.langPattern = langPattern;
if (typeof langPattern !== 'string') {
meta.langRegex = null;
return false;
}
if (langPattern === '*') {
meta.langRegex = null;
return elementLang !== '';
}
if (!REG_LANG_VALID.test(langPattern)) {
meta.langRegex = null;
return false;
}
const regexStr = langPattern
.split('-')
.map((part, index) => {
const core = part === '*' ? ALPHA_NUM : part;
return (index === 0 ? core : `-${core}`) + LANG_PART;
})
.join('');
const matcherRegex = new RegExp(`^${regexStr}$`, 'i');
meta.langRegex = matcherRegex;
return matcherRegex.test(elementLang);
};
/**
* Matches the :checked pseudo-class.
* @param {object} node - The Element node.
* @returns {boolean} True if matches, otherwise false.
*/
export const matchCheckedPseudoClass = node => {
const { localName } = node;
if (localName === 'option') {
return node.selected;
}
if (localName === 'input') {
const attrType = node.getAttribute('type');
if (node.checked) {
return attrType === 'checkbox' || attrType === 'radio';
}
}
return false;
};
/**
* Matches the :any-link and :link pseudo-classes.
* @param {object} node - The Element node.
* @returns {boolean} True if matches, otherwise false.
*/
export const matchLinkPseudoClass = node => {
const { localName } = node;
if (localName === 'a' || localName === 'area') {
return node.hasAttribute('href');
}
return false;
};
/**
* Matches the :open pseudo-class.
* @param {object} node - The Element node.
* @returns {boolean} True if matches, otherwise false.
*/
export const matchOpenPseudoClass = node => {
const { localName } = node;
if (localName === 'details' || localName === 'dialog') {
return node.hasAttribute('open');
}
return false;
};
/**
* Matches the :placeholder-shown pseudo-class.
* @param {object} node - The Element node.
* @param {Set.<string>} keys - A set of input type keys.
* @returns {boolean} True if matches, otherwise false.
*/
export const matchPlaceholderShownPseudoClass = (node, keys) => {
let placeholder;
if (node.placeholder) {
placeholder = node.placeholder;
} else if (node.hasAttribute('placeholder')) {
placeholder = node.getAttribute('placeholder');
}
if (typeof placeholder === 'string' && !/[\r\n]/.test(placeholder)) {
let targetNode;
const { localName } = node;
if (localName === 'textarea') {
targetNode = node;
} else if (localName === 'input') {
if (node.hasAttribute('type')) {
if (keys.has(node.getAttribute('type'))) {
targetNode = node;
}
} else {
targetNode = node;
}
}
if (targetNode) {
return node.value === '';
}
}
return false;
};
/**
* Matches the :in-range and :out-of-range pseudo-classes.
* @param {string} astName - The name of the pseudo-class.
* @param {object} node - The Element node.
* @param {Set.<string>} keys - A set of input type keys.
* @returns {boolean} True if matches, otherwise false.
*/
export const matchRangePseudoClass = (astName, node, keys) => {
const { localName } = node;
const attrType = node.getAttribute('type');
if (
localName === 'input' &&
!(node.readOnly || node.hasAttribute('readonly')) &&
!(node.disabled || node.hasAttribute('disabled')) &&
keys.has(attrType)
) {
const flowed = node.validity.rangeUnderflow || node.validity.rangeOverflow;
if (astName === 'out-of-range') {
return flowed;
}
if (flowed) {
return false;
}
return (
node.hasAttribute('min') ||
node.hasAttribute('max') ||
attrType === 'range'
);
}
return false;
};
/**
* Match the :read-only and :read-write pseudo-classes
* @param {string} astName - pseudo-class name
* @param {object} node - Element node
* @returns {boolean} - True if matches, otherwise false.
*/
export const matchReadOnlyPseudoClass = (astName, node) => {
const { localName } = node;
let isReadOnly = false;
switch (localName) {
case 'textarea':
case 'input': {
const isEditableInput = !node.type || KEYS_INPUT_EDIT.has(node.type);
if (localName === 'textarea' || isEditableInput) {
isReadOnly =
node.readOnly ||
node.hasAttribute('readonly') ||
node.disabled ||
node.hasAttribute('disabled');
} else {
// Non-editable input types are always read-only
isReadOnly = true;
}
break;
}
default: {
isReadOnly = !isContentEditable(node);
}
}
if (astName === 'read-only') {
return isReadOnly;
}
return !isReadOnly;
};
/**
* Matches the :required and :optional pseudo-classes.
* @param {string} astName - The name of the pseudo-class.
* @param {object} node - The Element node.
* @param {Set.<string>} keys - A set of input type keys.
* @returns {boolean} True if matches, otherwise false.
*/
export const matchRequiredPseudoClass = (astName, node, keys) => {
const { localName } = node;
let required = false;
if (localName === 'select' || localName === 'textarea') {
if (node.required || node.hasAttribute('required')) {
required = true;
}
} else if (localName === 'input') {
if (node.hasAttribute('type')) {
const attrType = node.getAttribute('type');
if (keys.has(attrType)) {
if (node.required || node.hasAttribute('required')) {
required = true;
}
}
} else if (node.required || node.hasAttribute('required')) {
required = true;
}
} else {
return false;
}
if (astName === 'optional') {
return !required;
}
return required;
};
/**
* Matches an attribute selector against an element.
* @param {object} ast - The AST for the attribute selector.
* @param {object} node - The element node to match against.
* @param {object} [opt] - Optional parameters.
* @param {boolean} [opt.check] - True if running in an internal check.
* @param {boolean} [opt.forgive] - True to forgive certain syntax errors.
* @param {object} [opt.globalObject] - The global object.
* @returns {boolean} - True if the attribute selector matches, otherwise false.
*/
export const matchAttributeSelector = (
ast,
node,
{ check, forgive, globalObject } = {}
) => {
const {
flags: astFlags,
matcher: astMatcher,
name: astName,
value: astValue
} = ast;
if (typeof astFlags === 'string' && !/^[is]$/i.test(astFlags) && !forgive) {
const css = generateCSS(ast);
throw generateException(
`Invalid selector ${css}`,
SYNTAX_ERR,
globalObject
);
}
const isHTML = node.ownerDocument.contentType === 'text/html';
let meta = astMetaCache.get(ast);
if (meta === undefined) {
meta = {
attrValues: new Set()
};
astMetaCache.set(ast, meta);
}
if (astMatcher === null && !astFlags && typeof astName?.name === 'string') {
if (meta.astName === undefined) {
const rawName = unescapeSelector(astName.name);
meta.astName = rawName;
meta.hasPipe = rawName.indexOf('|') > -1;
}
if (!meta.hasPipe) {
if (
meta.astName === 'lang' &&
node.hasAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang')
) {
return false;
}
if (node.hasAttribute(meta.astName)) {
return true;
}
const attrs = node.attributes;
if (!attrs || attrs.length === 0) {
return false;
}
const checkName = isHTML ? meta.astName.toLowerCase() : meta.astName;
for (let i = 0, len = attrs.length; i < len; i++) {
let itemName = attrs[i].name;
if (isHTML) {
itemName = itemName.toLowerCase();
}
const colonIdx = itemName.indexOf(':');
if (colonIdx > -1) {
const itemLocalName = itemName
.substring(colonIdx + 1)
.replace(/^:/, '');
if (checkName === itemLocalName) {
return true;
}
} else if (checkName === itemName) {
return true;
}
}
return false;
}
}
const { attributes } = node;
if (!attributes || !attributes.length) {
return false;
}
if (meta.caseInsensitive === undefined) {
let caseInsensitive = false;
if (typeof astFlags === 'string') {
caseInsensitive = /^i$/i.test(astFlags);
} else if (isHTML) {
caseInsensitive = KEYS_ATTR_VALUE_I.has(astName.name);
}
meta.caseInsensitive = caseInsensitive;
let astAttrName = unescapeSelector(astName.name);
meta.hasPipeInName = astAttrName.indexOf('|') > -1;
if ((isHTML && !meta.hasPipeInName) || caseInsensitive) {
astAttrName = astAttrName.toLowerCase();
}
meta.astAttrName = astAttrName;
if (meta.hasPipeInName) {
const { prefix, localName } = parseAstName(astAttrName);
meta.astPrefix = prefix;
meta.astLocalName = localName;
}
const { name: astIdentValue, value: astStringValue } = astValue ?? {};
let attrValue;
if (astIdentValue) {
attrValue = unescapeSelector(astIdentValue);
} else if (astStringValue !== undefined) {
attrValue = astStringValue;
}
if (caseInsensitive && typeof attrValue === 'string') {
attrValue = attrValue.toLowerCase();
}
meta.cachedAttrValue = attrValue;
if (astMatcher === '~=' && attrValue && typeof attrValue === 'string') {
meta.tildeTarget = /\s/.test(attrValue) ? null : ` ${attrValue} `;
}
}
const caseInsensitive = meta.caseInsensitive;
const astAttrName = meta.astAttrName;
const attrValue = meta.cachedAttrValue;
const attrValues = meta.attrValues;
if (attrValues.size) {
attrValues.clear();
}
if (meta.hasPipeInName) {
const astPrefix = meta.astPrefix;
const astLocalName = meta.astLocalName;
for (let i = 0, len = attributes.length; i < len; i++) {
const item = attributes[i];
let itemName = item.name;
let itemValue = item.value;
if (caseInsensitive) {
itemName = itemName.toLowerCase();
itemValue = itemValue.toLowerCase();
}
const colonIdx = itemName.indexOf(':');
switch (astPrefix) {
case '': {
if (astLocalName === itemName) {
attrValues.add(itemValue);
}
break;
}
case '*': {
if (colonIdx > -1) {
const itemLocalName = itemName
.substring(colonIdx + 1)
.replace(/^:/, '');
if (itemLocalName === astLocalName) {
attrValues.add(itemValue);
}
} else if (astLocalName === itemName) {
attrValues.add(itemValue);
}
break;
}
default: {
if (!check) {
if (forgive) {
return false;
}
const css = generateCSS(ast);
throw generateException(
`Invalid selector ${css}`,
SYNTAX_ERR,
globalObject
);
}
if (colonIdx > -1) {
const itemPrefix = itemName.substring(0, colonIdx);
const itemLocalName = itemName
.substring(colonIdx + 1)
.replace(/^:/, '');
if (itemPrefix === 'xml' && itemLocalName === 'lang') {
continue;
}
if (astPrefix === itemPrefix && astLocalName === itemLocalName) {
if (isNamespaceDeclared(astPrefix, node)) {
attrValues.add(itemValue);
}
}
}
}
}
}
} else {
for (let i = 0, len = attributes.length; i < len; i++) {
const item = attributes[i];
const origName = item.name;
if (
item.namespaceURI === 'http://www.w3.org/XML/1998/namespace' &&
item.localName === 'lang'
) {
continue;
}
const colonIdx = origName.indexOf(':');
if (colonIdx > -1) {
const itemPrefix = origName.substring(0, colonIdx);
const itemLocalName = origName
.substring(colonIdx + 1)
.replace(/^:/, '');
if (itemPrefix === 'xml' && itemLocalName === 'lang') {
continue;
}
}
let isMatch = false;
if (origName === astAttrName) {
isMatch = true;
} else if (caseInsensitive && origName.toLowerCase() === astAttrName) {
isMatch = true;
}
if (isMatch) {
attrValues.add(caseInsensitive ? item.value.toLowerCase() : item.value);
} else if (colonIdx > -1) {
const itemLocalName = origName
.substring(colonIdx + 1)
.replace(/^:/, '');
let isLocalMatch = false;
if (itemLocalName === astAttrName) {
isLocalMatch = true;
} else if (
caseInsensitive &&
itemLocalName.toLowerCase() === astAttrName
) {
isLocalMatch = true;
}
if (isLocalMatch) {
attrValues.add(
caseInsensitive ? item.value.toLowerCase() : item.value
);
}
}
}
}
if (!attrValues.size) {
return false;
}
switch (astMatcher) {
case '=': {
return typeof attrValue === 'string' && attrValues.has(attrValue);
}
case '~=': {
if (meta.tildeTarget === null || !attrValue) {
return false;
}
const target = meta.tildeTarget;
for (const value of attrValues) {
if (` ${value.replace(/[\t\r\n\f]/g, ' ')} `.includes(target)) {
return true;
}
}
return false;
}
case '|=': {
if (!attrValue || typeof attrValue !== 'string') {
return false;
}
const prefix = `${attrValue}-`;
for (const value of attrValues) {
if (value === attrValue || value.startsWith(prefix)) {
return true;
}
}
return false;
}
case '^=': {
if (!attrValue || typeof attrValue !== 'string') {
return false;
}
for (const value of attrValues) {
if (value.startsWith(attrValue)) {
return true;
}
}
return false;
}
case '$=': {
if (!attrValue || typeof attrValue !== 'string') {
return false;
}
for (const value of attrValues) {
if (value.endsWith(attrValue)) {
return true;
}
}
return false;
}
case '*=': {
if (!attrValue || typeof attrValue !== 'string') {
return false;
}
for (const value of attrValues) {
if (value.includes(attrValue)) {
return true;
}
}
return false;
}
case null:
default: {
return true;
}
}
};
/**
* Matches a type selector against an element.
* @param {object} ast - The AST for the type selector.
* @param {object} node - The element node to match against.
* @param {object} [opt] - Optional parameters.
* @param {boolean} [opt.check] - True if running in an internal check.
* @param {boolean} [opt.forgive] - True to forgive undeclared namespace.
* @param {object} [opt.globalObject] - The global object.
* @returns {boolean} - True if the type selector matches, otherwise false.
*/
export const matchTypeSelector = (
ast,
node,
{ check, forgive, globalObject } = {}
) => {
const { localName, namespaceURI, prefix } = node;
let meta = astMetaCache.get(ast);
if (meta === undefined) {
meta = {};
astMetaCache.set(ast, meta);
}
if (meta.astName === undefined) {
const astName = unescapeSelector(ast.name);
const { prefix: parsedPrefix, localName: parsedLocalName } = parseAstName(
astName,
node
);
meta.astName = astName;
meta.astPrefix = parsedPrefix;
meta.astPrefixLowerCased = parsedPrefix.toLowerCase();
meta.astLocalName = parsedLocalName;
meta.astLocalNameLowerCased = parsedLocalName.toLowerCase();
meta.hasPipe = astName.includes('|');
}
const isHTML =
node.ownerDocument.contentType === 'text/html' &&
(!namespaceURI || namespaceURI === 'http://www.w3.org/1999/xhtml');
if (isHTML && localName === meta.astLocalName && !meta.hasPipe) {
return true;
}
let nodePrefix;
let nodeLocalName;
const colonIdx = localName.indexOf(':');
if (colonIdx > -1) {
nodePrefix = localName.substring(0, colonIdx);
nodeLocalName = localName.substring(colonIdx + 1);
} else {
nodePrefix = prefix || '';
nodeLocalName = localName;
}
const astPrefix = isHTML ? meta.astPrefixLowerCased : meta.astPrefix;
const astLocalName = isHTML ? meta.astLocalNameLowerCased : meta.astLocalName;
const isUniversal = astLocalName === '*';
switch (astPrefix) {
case '': {
return (
!nodePrefix &&
!namespaceURI &&
(isUniversal || astLocalName === nodeLocalName)
);
}
case '*': {
return isUniversal || astLocalName === nodeLocalName;
}
default: {
if (!check) {
if (forgive) {
return false;
}
const css = generateCSS(ast);
throw generateException(
`Invalid selector ${css}`,
SYNTAX_ERR,
globalObject
);
}
const astNS = node.lookupNamespaceURI(astPrefix);
const nodeNS = node.lookupNamespaceURI(nodePrefix);
if (astNS === nodeNS && astPrefix === nodePrefix) {
return isUniversal || astLocalName === nodeLocalName;
} else if (!forgive && !astNS) {
throw generateException(
`Undeclared namespace ${astPrefix}`,
SYNTAX_ERR,
globalObject
);
}
return false;
}
}
};
File diff suppressed because it is too large. Load diff
+504
View File
@@ -0,0 +1,504 @@
/**
* parser.js
*/
/* import */
import * as cssTree from 'css-tree';
import { getType } from './utility.js';
/* constants */
import {
ATTR_SELECTOR,
BIT_01,
BIT_02,
BIT_04,
BIT_08,
BIT_16,
BIT_32,
BIT_FFFF,
CLASS_SELECTOR,
COMBINATOR,
DUO,
HEX,
ID_SELECTOR,
KEYS_LOGICAL,
KEYS_PS_CLASS_SUPPORTED,
NEST_SELECTOR,
NTH,
PS_CLASS_SELECTOR,
PS_ELEMENT_SELECTOR,
SELECTOR,
SYNTAX_ERR,
TYPE_SELECTOR
} from './constant.js';
const AST_SORT_ORDER = new Map([
[PS_ELEMENT_SELECTOR, BIT_01],
[ID_SELECTOR, BIT_02],
[CLASS_SELECTOR, BIT_04],
[TYPE_SELECTOR, BIT_08],
[ATTR_SELECTOR, BIT_16],
[PS_CLASS_SELECTOR, BIT_32]
]);
const KEYS_PS_NTH = new Set([
'first-child',
'first-of-type',
'last-child',
'last-of-type',
'only-child',
'only-of-type'
]);
const KEYS_PS_CLASS_STATE = new Set([
'checked',
'closed',
'disabled',
'empty',
'enabled',
'in-range',
'indeterminate',
'invalid',
'open',
'out-of-range',
'placeholder-shown',
'read-only',
'read-write',
'valid',
...KEYS_PS_NTH
]);
const KEYS_SHADOW_HOST = new Set(['host', 'host-context']);
const U_FFFD = '\uFFFD';
/* regexp */
const REG_EMPTY_PS_FUNC =
/(?<=:(?:dir|has|host(?:-context)?|is|lang|not|nth-(?:last-)?(?:child|of-type)|where))\(\s+\)/g;
const REG_SHADOW_PS_ELEMENT = /^part|slotted$/;
/**
* Unescapes a CSS selector string.
* @param {string} selector - The CSS selector to unescape.
* @returns {string} The unescaped selector string.
*/
export const unescapeSelector = (selector = '') => {
if (typeof selector === 'string' && selector.indexOf('\\', 0) >= 0) {
const arr = selector.split('\\');
const selectorItems = [arr[0]];
const l = arr.length;
for (let i = 1; i < l; i++) {
const item = arr[i];
if (item === '' && i === l - 1) {
selectorItems.push(U_FFFD);
} else if (item === '') {
// Empty segment at non-last position means \\ (escaped backslash)
selectorItems.push('\\');
// skip the next segment which is the remainder after \\
i++;
if (i < l) {
selectorItems.push(arr[i]);
}
} else {
const hexExists = /^([\da-f]{1,6}\s?)/i.exec(item);
if (hexExists) {
const [, hex] = hexExists;
let str;
try {
const low = parseInt('D800', HEX);
const high = parseInt('DFFF', HEX);
const deci = parseInt(hex, HEX);
if (deci === 0 || (deci >= low && deci <= high)) {
str = U_FFFD;
} else {
str = String.fromCodePoint(deci);
}
} catch (e) {
str = U_FFFD;
}
let postStr = '';
if (item.length > hex.length) {
postStr = item.substring(hex.length);
}
selectorItems.push(`${str}${postStr}`);
} else if (/^[\n\r\f]/.test(item)) {
// whitespace
selectorItems.push(`\\${item}`);
} else {
selectorItems.push(item);
}
}
}
return selectorItems.join('');
}
return selector;
};
/**
* Preprocesses a selector string according to the specification.
* @see https://drafts.csswg.org/css-syntax-3/#input-preprocessing
* @param {string} value - The value to preprocess.
* @returns {string} The preprocessed selector string.
*/
export const preprocess = value => {
// Non-string values will be converted to string.
if (typeof value !== 'string') {
if (value === undefined || value === null) {
return getType(value).toLowerCase();
} else if (Array.isArray(value)) {
return value.join(',');
} else if (Object.hasOwn(value, 'toString')) {
return value.toString();
} else {
throw new DOMException(`Invalid selector ${value}`, SYNTAX_ERR);
}
}
let selector = value;
let index = 0;
while (index >= 0) {
// @see https://drafts.csswg.org/selectors/#id-selectors
index = selector.indexOf('#', index);
if (index < 0) {
break;
}
const preHash = selector.substring(0, index + 1);
let postHash = selector.substring(index + 1);
const codePoint = postHash.codePointAt(0);
if (codePoint > BIT_FFFF) {
const str = `\\${codePoint.toString(HEX)} `;
if (postHash.length === DUO) {
postHash = str;
} else {
postHash = `${str}${postHash.substring(DUO)}`;
}
}
selector = `${preHash}${postHash}`;
index++;
}
selector = selector
.replace(/\f|\r\n?/g, '\n')
.replace(/[\0\uD800-\uDFFF]|\\$/g, U_FFFD);
if (selector === '&') {
return '';
}
return selector;
};
/**
* Creates an Abstract Syntax Tree (AST) from a CSS selector string.
* @param {string} sel - The CSS selector string.
* @param {string} context - Parse what part of CSS.
* @returns {object} The parsed AST object.
*/
export const parseSelector = (sel, context = 'selectorList') => {
const selector = preprocess(sel);
// invalid selectors
if (/^$|^\s*>|,\s*$/.test(selector)) {
throw new DOMException(`Invalid selector ${selector}`, SYNTAX_ERR);
}
try {
return cssTree.parse(selector, { context });
} catch (e) {
const { message } = e;
if (
/^(?:"\]"|Attribute selector [()\s,=~^$*|]+) is expected$/.test(
message
) &&
!selector.endsWith(']')
) {
const index = selector.lastIndexOf('[');
const selPart = selector.substring(index);
if (selPart.includes('"')) {
const quotes = selPart.match(/"/g).length;
if (quotes % 2) {
return parseSelector(`${selector}"]`);
}
return parseSelector(`${selector}]`);
}
return parseSelector(`${selector}]`);
} else if (message === '")" is expected') {
// workaround for https://github.com/csstree/csstree/issues/283
if (REG_EMPTY_PS_FUNC.test(selector)) {
return parseSelector(`${selector.replaceAll(REG_EMPTY_PS_FUNC, '()')}`);
} else if (!selector.endsWith(')')) {
return parseSelector(`${selector})`);
} else {
throw new DOMException(`Invalid selector ${selector}`, SYNTAX_ERR);
}
} else {
throw new DOMException(`Invalid selector ${selector}`, SYNTAX_ERR);
}
}
};
/**
* Walks the provided AST to collect selector branches and gather information
* about its contents.
* @param {object} ast - The AST to traverse.
* @param {boolean} toObject - True if converts ast to object, false otherwise.
* @param {function(object): void} [callback] - Optional callback for each node.
* @returns {{branches: Array<object>, info: object}} An object containing the selector branches and info.
*/
export const walkAST = (ast = {}, toObject = false, callback = null) => {
const branches = new Set();
const info = {
hasForgivenPseudoFunc: false,
hasHasPseudoFunc: false,
hasLogicalPseudoFunc: false,
hasNestedSelector: false,
hasNestingSelector: false,
hasNotPseudoFunc: false,
hasNthChildOfSelector: false,
hasStatePseudoClass: false,
hasUnsupportedPseudoClass: false
};
const opt = {
enter(node) {
if (typeof callback === 'function') {
callback(node);
}
switch (node.type) {
case CLASS_SELECTOR: {
if (/^-?\d/.test(node.name)) {
throw new DOMException(
`Invalid selector .${node.name}`,
SYNTAX_ERR
);
}
break;
}
case ID_SELECTOR: {
if (/^-?\d/.test(node.name)) {
throw new DOMException(
`Invalid selector #${node.name}`,
SYNTAX_ERR
);
}
break;
}
case PS_CLASS_SELECTOR: {
const name = node.name.toLowerCase();
if (KEYS_LOGICAL.has(name)) {
info.hasNestedSelector = true;
info.hasLogicalPseudoFunc = true;
if (name === 'has') {
info.hasHasPseudoFunc = true;
} else if (name === 'not') {
info.hasNotPseudoFunc = true;
} else {
info.hasForgivenPseudoFunc = true;
}
} else if (KEYS_PS_CLASS_STATE.has(name)) {
info.hasStatePseudoClass = true;
} else if (
KEYS_SHADOW_HOST.has(name) &&
Array.isArray(node.children) &&
node.children.length
) {
info.hasNestedSelector = true;
} else if (!KEYS_PS_CLASS_SUPPORTED.has(name)) {
info.hasUnsupportedPseudoClass = true;
}
break;
}
case PS_ELEMENT_SELECTOR: {
if (REG_SHADOW_PS_ELEMENT.test(node.name)) {
info.hasNestedSelector = true;
}
break;
}
case NTH: {
if (node.selector) {
info.hasNestedSelector = true;
info.hasNthChildOfSelector = true;
}
break;
}
case SELECTOR: {
branches.add(node.children);
break;
}
case NEST_SELECTOR: {
info.hasNestingSelector = true;
break;
}
default:
}
}
};
const clonedAst = cssTree.clone(ast);
cssTree.walk(toObject ? cssTree.toPlainObject(clonedAst) : clonedAst, opt);
if (info.hasNestedSelector === true) {
cssTree.findAll(clonedAst, (node, item, list) => {
if (list) {
if (node.type === PS_CLASS_SELECTOR && KEYS_LOGICAL.has(node.name)) {
const itemList = list.filter(i => {
const { name, type } = i;
return type === PS_CLASS_SELECTOR && KEYS_LOGICAL.has(name);
});
for (const { children } of itemList) {
// SelectorList
for (const { children: grandChildren } of children) {
// Selector
for (const { children: greatGrandChildren } of grandChildren) {
if (branches.has(greatGrandChildren)) {
branches.delete(greatGrandChildren);
}
}
}
}
} else if (
node.type === PS_CLASS_SELECTOR &&
KEYS_SHADOW_HOST.has(node.name) &&
Array.isArray(node.children) &&
node.children.length
) {
const itemList = list.filter(i => {
const { children, name, type } = i;
const res =
type === PS_CLASS_SELECTOR &&
KEYS_SHADOW_HOST.has(name) &&
Array.isArray(children) &&
children.length;
return res;
});
for (const { children } of itemList) {
// Selector
for (const { children: grandChildren } of children) {
if (branches.has(grandChildren)) {
branches.delete(grandChildren);
}
}
}
} else if (
node.type === PS_ELEMENT_SELECTOR &&
REG_SHADOW_PS_ELEMENT.test(node.name)
) {
const itemList = list.filter(i => {
const { name, type } = i;
const res =
type === PS_ELEMENT_SELECTOR && REG_SHADOW_PS_ELEMENT.test(name);
return res;
});
for (const { children } of itemList) {
// Selector
for (const { children: grandChildren } of children) {
if (branches.has(grandChildren)) {
branches.delete(grandChildren);
}
}
}
} else if (node.type === NTH && node.selector) {
const itemList = list.filter(i => {
const { selector, type } = i;
const res = type === NTH && selector;
return res;
});
for (const { selector } of itemList) {
const { children } = selector;
// Selector
for (const { children: grandChildren } of children) {
if (branches.has(grandChildren)) {
branches.delete(grandChildren);
}
}
}
}
}
});
}
return {
info,
branches: [...branches]
};
};
/**
* Comparison function for sorting AST nodes based on specificity.
* @param {object} a - The first AST node.
* @param {object} b - The second AST node.
* @returns {number} -1, 0 or 1, depending on the sort order.
*/
export const compareASTNodes = (a, b) => {
const bitA = AST_SORT_ORDER.get(a.type);
const bitB = AST_SORT_ORDER.get(b.type);
if (bitA === bitB) {
return 0;
} else if (bitA > bitB) {
return 1;
} else {
return -1;
}
};
/**
* Sorts a collection of AST nodes based on CSS specificity rules.
* @param {Array<object>} asts - A collection of AST nodes to sort.
* @returns {Array<object>} A new array containing the sorted AST nodes.
*/
export const sortAST = asts => {
const arr = [...asts];
if (arr.length > 1) {
arr.sort(compareASTNodes);
}
return arr;
};
/**
* Parses a type selector's name, which may include a namespace prefix.
* @param {string} selector - The type selector name (e.g., 'ns|E' or 'E').
* @returns {{prefix: string, localName: string}} An object with `prefix` and
* `localName` properties.
*/
export const parseAstName = selector => {
let prefix;
let localName;
if (selector && typeof selector === 'string') {
if (selector.indexOf('|') > -1) {
[prefix, localName] = selector.split('|');
} else {
prefix = '*';
localName = selector;
}
} else {
throw new DOMException(`Invalid selector ${selector}`, SYNTAX_ERR);
}
return {
prefix,
localName
};
};
/**
* Extracts the rightmost subject keys (id, class, tag) from a CSS selector AST.
* @param {object} ast - The AST representation of the CSS selector.
* @returns {Array<{id: string|null, className: string|null, tag: string|null}>} The list of extracted keys for each selector group.
*/
export const extractSubjectsAst = ast => {
const subjects = [];
if (ast?.type === 'SelectorList') {
for (const selectorNode of ast.children) {
let idKey = null;
let classKey = null;
let tagKey = null;
let current = selectorNode.children.tail;
while (current) {
const node = current.data;
if (node.type === COMBINATOR) {
break;
}
if (node.type === ID_SELECTOR) {
idKey = idKey ?? unescapeSelector(node.name);
} else if (node.type === CLASS_SELECTOR) {
classKey = classKey ?? unescapeSelector(node.name);
} else if (node.type === TYPE_SELECTOR) {
const { localName } = parseAstName(unescapeSelector(node.name));
if (localName !== '*') {
tagKey = tagKey ?? localName.toLowerCase();
}
}
current = current.prev;
}
subjects.push({ id: idKey, className: classKey, tag: tagKey });
}
}
return subjects;
};
/* Re-exported from css-tree. */
export { find as findAST, generate as generateCSS } from 'css-tree';
+86
View File
@@ -0,0 +1,86 @@
/**
* processor.js
*/
/* import */
import { sortAST, unescapeSelector } from './parser.js';
import { isInvalidCombinator } from './selector.js';
import { generateException } from './utility.js';
/* constants */
import { COMBINATOR, SYNTAX_ERR } from './constant.js';
/**
* SelectorProcessor
* Processes raw selector branches into an internal AST format.
*/
export class SelectorProcessor {
#context;
/**
* @param {import('./finder.js').Finder} context - The Finder instance.
*/
constructor(context) {
this.#context = context;
}
/**
* Processes selector branches into the internal AST.
* @param {Array.<object>} branches - The raw selector branches.
* @param {string} selector - The original CSS selector string.
* @returns {object} An object containing the ast and a descendant flag.
*/
process(branches, selector) {
let descendant = false;
const ast = [];
for (const items of branches) {
const branch = [];
let prevType = null;
const itemsLen = items.length;
if (itemsLen) {
const leaves = new Set();
for (let j = 0; j < itemsLen; j++) {
const item = items[j];
const isLast = j === itemsLen - 1;
// Validate combinator syntax.
if (isInvalidCombinator(item.type, prevType, isLast)) {
const msg = `Invalid selector ${selector}`;
this.#context.onError(
generateException(msg, SYNTAX_ERR, this.#context.window)
);
return { ast: [], descendant: false, invalidate: false };
}
if (item.type === COMBINATOR) {
// Flag if the branch contains descendant or child combinators.
if (item.name === ' ' || item.name === '>') {
descendant = true;
}
branch.push({ combo: item, leaves: sortAST(leaves) });
leaves.clear();
} else {
// Unescape selector names and detect namespaces.
if (item.name && typeof item.name === 'string') {
const unescapedName = unescapeSelector(item.name);
if (unescapedName !== item.name) {
item.name = unescapedName;
}
if (/[|:]/.test(unescapedName)) {
item.namespace = true;
}
}
leaves.add(item);
}
prevType = item.type;
// Handle the trailing items after the loop finishes.
if (isLast) {
branch.push({ combo: null, leaves: sortAST(leaves) });
leaves.clear();
}
}
}
// Initialize the structure required for node collection.
ast.push({ branch, dir: null, filtered: false, find: false });
}
return { ast, descendant };
}
}
+334
View File
@@ -0,0 +1,334 @@
/**
* selector.js
*/
/* import */
import * as cssTree from 'css-tree';
import { generateException } from './utility.js';
/* constants */
import {
ATTR_TYPE,
COMBINATOR,
COMBO,
COMPOUND_I,
HAS_COMPOUND,
KEYS_LOGICAL,
KEYS_PS_CLASS_SUPPORTED,
LOGIC_COMPLEX,
LOGIC_COMPOUND,
N_TH,
PSEUDO_CLASS,
PS_CLASS_SELECTOR,
PS_ELEMENT_SELECTOR,
SELECTOR,
SYNTAX_ERR,
TAG_TYPE_WO_UNIVERSAL,
TARGET_ALL
} from './constant.js';
/* regexp */
const REG_EXCLUDE_BASIC =
/[|\\]|::|[^\u0021-\u007F\s]|\[\s*[\w$*=^|~-]+(?:(?:"[\w$*=^|~\s'-]+"|'[\w$*=^|~\s"-]+')?(?:\s+[\w$*=^|~-]+)+|"[^"\]]{1,255}|'[^'\]]{1,255})\s*\]|:(?:is|where)\(\s*\)|\[\s*data-/;
const REG_EXCLUDE_QSA = new RegExp(
`(?:^(?:[A-Z]|\\.)[\\w-]*$|${COMPOUND_I}${COMBO}${COMPOUND_I})`,
'i'
);
const REG_COMPLEX = new RegExp(`${COMPOUND_I}${COMBO}${COMPOUND_I}`, 'i');
const REG_LOGIC_COMPLEX = new RegExp(
`:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPLEX})`
);
const REG_LOGIC_COMPOUND = new RegExp(
`:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPOUND})`
);
const REG_LOGIC_HAS_COMPOUND = new RegExp(
`:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPOUND}|${HAS_COMPOUND})`
);
const REG_END_WITH_HAS = new RegExp(`:${HAS_COMPOUND}$`);
const REG_WO_LOGICAL = new RegExp(`:(?!${PSEUDO_CLASS}|${N_TH})`);
const REG_COMBO = new RegExp(COMBO);
const REG_ID = /#(\D[^#.*]+)/g;
const REG_CLASS = /\.(\D[^#.*]+)/g;
const REG_TAG = /^([^#.]+)/;
const REG_INVALID_SYNTAX =
/[+~>]\s*[+~>]|^\s*[+~>]|[+~>]\s*$|^\s*,|,\s*,|,\s*$/;
const REG_TEST_LIB = new RegExp(
`^(?:[*]?${ATTR_TYPE}(?:\\s*,\\s*${TAG_TYPE_WO_UNIVERSAL}${COMBO}${TAG_TYPE_WO_UNIVERSAL})?)$`
);
/**
* Find a nested :has() pseudo-class.
* @param {object} leaf - The AST leaf to check.
* @returns {?object} The leaf if it's :has, otherwise null.
*/
export const findNestedHas = leaf => leaf.name === 'has';
/**
* Find a logical pseudo-class that contains a nested :has().
* @param {object} leaf - The AST leaf to check.
* @returns {?object} The leaf if it matches, otherwise null.
*/
export const findLogicalWithNestedHas = leaf => {
if (KEYS_LOGICAL.has(leaf.name) && cssTree.find(leaf, findNestedHas)) {
return leaf;
}
return null;
};
/**
* Validates nesting restrictions within :has() arguments.
* @param {Array<object>} astChildren - The AST nodes representing the :has() arguments.
* @returns {boolean} False if there's an invalid nesting constraint violation.
*/
export const validateHasNesting = astChildren => {
const l = astChildren.length;
for (let i = 0; i < l; i++) {
const item = cssTree.find(astChildren[i], findLogicalWithNestedHas);
if (item) {
// If nested :has() is wrapped inside :is() or :where(), it is forgiven.
if (item.name !== 'is' && item.name !== 'where') {
return false;
}
}
}
return true;
};
/**
* Creates a callback function to validate :has() nesting during AST walk.
* @param {object} globalObj - The global window object.
* @returns {function(object): void} The callback function for walkAST.
*/
export const createHasValidator = globalObj => node => {
if (
node.type === PS_CLASS_SELECTOR &&
node.name.toLowerCase() === 'has' &&
!validateHasNesting(Array.from(node.children || []))
) {
const css = cssTree.generate(node);
throw generateException(
`Disallowed nested :has() pseudo-class: ${css}`,
SYNTAX_ERR,
globalObj
);
}
};
/**
* Check if a combinator node is invalid (leading, trailing, or consecutive).
* @param {string} type - The current node type.
* @param {string|null} prevType - The previous node type.
* @param {boolean} isLast - Whether the current node is the last in the list.
* @returns {boolean} True if the combinator is invalid.
*/
export const isInvalidCombinator = (type, prevType, isLast) =>
type === COMBINATOR &&
(prevType === null || prevType === COMBINATOR || isLast);
/**
* Checks if a given AST is supported by the DOMSelector engine.
* @param {object} ast - The AST to validate.
* @returns {boolean} True if the selector is fully supported.
*/
export const isSupportedAST = ast => {
let isSupported = true;
const walk = (
node,
context = { insideHas: false, insideForgiving: false }
) => {
if (!isSupported || !node) {
return;
}
const nextContext = { ...context };
if (node.type === PS_ELEMENT_SELECTOR) {
isSupported = false;
return;
} else if (node.type === PS_CLASS_SELECTOR) {
let name = node.name;
if (name && typeof name === 'string') {
name = name.toLowerCase();
}
if (!KEYS_PS_CLASS_SUPPORTED.has(name)) {
isSupported = false;
return;
}
if (name === 'has') {
if (context.insideHas && !context.insideForgiving) {
isSupported = false;
return;
}
nextContext.insideHas = true;
} else if (name === 'is' || name === 'where') {
nextContext.insideForgiving = true;
}
}
if (node.children) {
let prevType = null;
if (node.children.head !== undefined) {
let current = node.children.head;
while (current) {
if (!current.data) {
current = current.next;
continue;
}
const childType = current.data.type;
if (
node.type === SELECTOR &&
isInvalidCombinator(childType, prevType, !current.next)
) {
if (!(prevType === null && context.insideHas)) {
isSupported = false;
return;
}
}
prevType = childType;
walk(current.data, nextContext);
if (!isSupported) {
return;
}
current = current.next;
}
} else if (Array.isArray(node.children)) {
const l = node.children.length;
for (let i = 0; i < l; i++) {
const child = node.children[i];
if (!child) {
continue;
}
const childType = child.type;
if (
node.type === SELECTOR &&
isInvalidCombinator(childType, prevType, i === l - 1)
) {
if (!(prevType === null && context.insideHas)) {
isSupported = false;
return;
}
}
prevType = childType;
walk(child, nextContext);
if (!isSupported) {
return;
}
}
}
}
if (node.selector) {
walk(node.selector, nextContext);
}
};
walk(ast);
return isSupported;
};
/**
* Extracts the rightmost subject keys (id, class, tag) from a selector.
* @param {string} selector - The CSS selector string to parse.
* @param {boolean} caseSensitive - True if the tag should be case-sensitive.
* @returns {Array<{id: string|null, className: string|null, tag: string|null}>} The list of extracted keys for each selector group.
*/
export const extractSubjectsRegExp = (selector, caseSensitive) => {
const subjects = [];
const groups = selector.split(',');
for (let i = 0; i < groups.length; i++) {
const group = groups[i].trim();
if (!group) {
continue;
}
const compounds = group.split(REG_COMBO);
const rightmost = compounds[compounds.length - 1];
let idKey = null;
let classKey = null;
let tagKey = null;
if (rightmost) {
const idMatch = rightmost.match(REG_ID);
if (idMatch) {
idKey = idMatch[idMatch.length - 1].slice(1);
}
const classMatch = rightmost.match(REG_CLASS);
if (classMatch) {
classKey = classMatch[classMatch.length - 1].slice(1);
}
const tagMatch = rightmost.match(REG_TAG);
if (tagMatch) {
const tag = tagMatch[1];
if (tag !== '*') {
tagKey = caseSensitive ? tag : tag.toLowerCase();
}
}
}
subjects.push({ id: idKey, className: classKey, tag: tagKey });
}
return subjects;
};
/**
* Filter a selector for use with nwsapi.
* @param {string} selector - The selector string.
* @param {string} target - The target type.
* @returns {boolean} - True if the selector is valid for nwsapi.
*/
export const filterSelector = (selector, target) => {
// Basic validation and fast-fail for null/undefined/non-string values.
if (
!selector ||
typeof selector !== 'string' ||
/null|undefined/.test(selector)
) {
return false;
}
// Validate syntax.
if (REG_INVALID_SYNTAX.test(selector)) {
return false;
}
// Target-specific early exits.
if (
target === TARGET_ALL &&
REG_EXCLUDE_QSA.test(selector) &&
!REG_TEST_LIB.test(selector)
) {
return false;
}
// Exclude various complex or unsupported selectors early.
// i.e. non-ASCII, escaped selectors, namespaced selectors, pseudo-elements.
if (
selector.includes('/') ||
selector.includes('&') ||
REG_EXCLUDE_BASIC.test(selector)
) {
return false;
}
// Validate attribute selector integrity.
if (selector.includes('[')) {
const index = selector.lastIndexOf('[');
if (selector.indexOf(']', index) === -1) {
return false;
}
}
// Logic for pseudo-classes.
if (selector.includes(':')) {
// Determine if the selector has complex logical structures.
const isComplex =
target === TARGET_ALL ? false : REG_COMPLEX.test(selector);
// Handle :has() specifically.
if (selector.includes(':has(')) {
if (!isComplex || REG_LOGIC_HAS_COMPOUND.test(selector)) {
return false;
}
return REG_END_WITH_HAS.test(selector);
}
// Handle :is() and :not().
if (/(?:is|not)\(/.test(selector)) {
if (isComplex) {
return !REG_LOGIC_COMPLEX.test(selector);
} else {
return !REG_LOGIC_COMPOUND.test(selector);
}
}
// Default check for other pseudo-classes against known list.
if (REG_WO_LOGICAL.test(selector)) {
return false;
}
}
return true;
};
+949
View File
@@ -0,0 +1,949 @@
/**
* utility.js
*/
/* import */
import bidiFactory from 'bidi-js';
import isCustomElementName from 'is-potential-custom-element-name';
/* constants */
import {
CLASS_SELECTOR,
DIR_NEXT,
DIR_PREV,
DOCUMENT_FRAGMENT_NODE,
DOCUMENT_NODE,
DOCUMENT_POSITION_CONTAINS,
DOCUMENT_POSITION_PRECEDING,
ELEMENT_NODE,
ID_SELECTOR,
INPUT_BUTTON,
INPUT_EDIT,
INPUT_LTR,
INPUT_TEXT,
PS_ELEMENT_SELECTOR,
SHOW_ELEMENT,
TARGET_FIRST,
TEXT_NODE,
TYPE_FROM,
TYPE_SELECTOR,
TYPE_TO
} from './constant.js';
const KEYS_DIR_AUTO = new Set([...INPUT_BUTTON, ...INPUT_TEXT, 'hidden']);
const KEYS_DIR_LTR = new Set(INPUT_LTR);
const KEYS_INPUT_EDIT = new Set(INPUT_EDIT);
const KEYS_NODE_DIR_EXCLUDE = new Set(['bdi', 'script', 'style', 'textarea']);
const KEYS_NODE_FOCUSABLE = new Set(['button', 'select', 'textarea']);
const KEYS_NODE_FOCUSABLE_SVG = new Set([
'clipPath',
'defs',
'desc',
'linearGradient',
'marker',
'mask',
'metadata',
'pattern',
'radialGradient',
'script',
'style',
'symbol',
'title'
]);
/* regexp */
const REG_IS_HTML = /^(?:application\/xhtml\+x|text\/ht)ml$/;
const REG_IS_XML =
/^(?:application\/(?:[\w\-.]+\+)?|image\/[\w\-.]+\+|text\/)xml$/;
/**
* Get type of an object.
* @param {object} o - Object to check.
* @returns {string} - Type of the object.
*/
export const getType = o =>
Object.prototype.toString.call(o).slice(TYPE_FROM, TYPE_TO);
/**
* Verify array contents.
* @param {Array} arr - The array.
* @param {string} type - Expected type, e.g. 'String'.
* @throws {TypeError} - Throws if array or its items are of unexpected type.
* @returns {Array} - The verified array.
*/
export const verifyArray = (arr, type) => {
if (!Array.isArray(arr)) {
throw new TypeError(`Unexpected type ${getType(arr)}`);
}
if (typeof type !== 'string') {
throw new TypeError(`Unexpected type ${getType(type)}`);
}
for (const item of arr) {
if (getType(item) !== type) {
throw new TypeError(`Unexpected type ${getType(item)}`);
}
}
return arr;
};
/**
* Generate a DOMException.
* @param {string} msg - The error message.
* @param {string} name - The error name.
* @param {object} globalObject - The global object (e.g., window).
* @returns {DOMException} The generated DOMException object.
*/
export const generateException = (msg, name, globalObject = globalThis) => {
return new globalObject.DOMException(msg, name);
};
/**
* Resolve content document, root node, and check if it's in a shadow DOM.
* @param {object} node - Document, DocumentFragment, or Element node.
* @returns {Array.<object|boolean>} - [document, root, isInShadow].
*/
export const resolveContent = node => {
if (!node?.nodeType) {
throw new TypeError(`Unexpected type ${getType(node)}`);
}
let document;
let root;
let shadow;
switch (node.nodeType) {
case DOCUMENT_NODE: {
document = node;
root = node;
break;
}
case DOCUMENT_FRAGMENT_NODE: {
const { host, mode, ownerDocument } = node;
document = ownerDocument;
root = node;
shadow = host && (mode === 'close' || mode === 'open');
break;
}
case ELEMENT_NODE: {
document = node.ownerDocument;
let refNode = node;
while (refNode) {
const { host, mode, nodeType, parentNode } = refNode;
if (nodeType === DOCUMENT_FRAGMENT_NODE) {
shadow = host && (mode === 'close' || mode === 'open');
break;
} else if (parentNode) {
refNode = parentNode;
} else {
break;
}
}
root = refNode;
break;
}
default: {
throw new TypeError(`Unexpected node ${node.nodeName}`);
}
}
return [document, root, !!shadow];
};
/**
* Traverse node tree with a TreeWalker.
* @param {object} node - The target node.
* @param {object} walker - The TreeWalker instance.
* @param {boolean} [force] - Traverse only to the next node.
* @returns {?object} - The current node if found, otherwise null.
*/
export const traverseNode = (node, walker, force = false) => {
if (!node?.nodeType) {
throw new TypeError(`Unexpected type ${getType(node)}`);
}
if (!walker) {
return null;
}
let refNode = walker.currentNode;
if (refNode === node) {
return refNode;
} else if (force || refNode.contains(node)) {
refNode = walker.nextNode();
while (refNode) {
if (refNode === node) {
break;
}
refNode = walker.nextNode();
}
return refNode;
} else {
if (refNode !== walker.root) {
let bool;
while (refNode) {
if (refNode === node) {
bool = true;
break;
} else if (refNode === walker.root || refNode.contains(node)) {
break;
}
refNode = walker.parentNode();
}
if (bool) {
return refNode;
}
}
if (node.nodeType === ELEMENT_NODE) {
let bool;
while (refNode) {
if (refNode === node) {
bool = true;
break;
}
refNode = walker.nextNode();
}
if (bool) {
return refNode;
}
}
}
return null;
};
/**
* Check if a node is a custom element.
* @param {object} node - The Element node.
* @param {object} [opt] - Options.
* @param {boolean} [opt.formAssociated] - True if the node is form associated.
* @returns {boolean} - True if it's a custom element.
*/
export const isCustomElement = (node, { formAssociated } = {}) => {
if (!node?.nodeType) {
throw new TypeError(`Unexpected type ${getType(node)}`);
}
if (node.nodeType !== ELEMENT_NODE) {
return false;
}
const { localName, ownerDocument } = node;
const window = ownerDocument.defaultView;
let elmConstructor;
const attr = node.getAttribute('is');
if (attr) {
elmConstructor =
isCustomElementName(attr) && window.customElements.get(attr);
} else {
elmConstructor =
isCustomElementName(localName) && window.customElements.get(localName);
}
if (elmConstructor) {
if (formAssociated) {
return !!elmConstructor.formAssociated;
}
return true;
}
return false;
};
/**
* Get slotted text content.
* @param {object} node - The Element node (likely a <slot>).
* @returns {?string} - The text content.
*/
export const getSlottedTextContent = node => {
if (!node?.nodeType) {
throw new TypeError(`Unexpected type ${getType(node)}`);
}
if (typeof node.assignedNodes !== 'function') {
return null;
}
const nodes = node.assignedNodes();
if (nodes.length) {
let text = '';
const l = nodes.length;
for (let i = 0; i < l; i++) {
const item = nodes[i];
text = item.textContent.trim();
if (text) {
break;
}
}
return text;
}
return node.textContent.trim();
};
/**
* Get directionality of a node.
* @see https://html.spec.whatwg.org/multipage/dom.html#the-dir-attribute
* @param {object} node - The Element node.
* @param {WeakMap} [dirCache] - Cache for directionality.
* @returns {?string} - 'ltr' or 'rtl'.
*/
export const getDirectionality = (node, dirCache = new WeakMap()) => {
if (!node?.nodeType) {
throw new TypeError(`Unexpected type ${getType(node)}`);
}
if (node.nodeType !== ELEMENT_NODE) {
return null;
}
if (dirCache.has(node)) {
return dirCache.get(node);
}
const { dir: dirAttr, localName, parentNode } = node;
const { getEmbeddingLevels } = bidiFactory();
let result = 'ltr';
if (dirAttr === 'ltr' || dirAttr === 'rtl') {
result = dirAttr;
} else if (dirAttr === 'auto') {
let text = '';
switch (localName) {
case 'input': {
if (!node.type || KEYS_DIR_AUTO.has(node.type)) {
text = node.value;
} else if (KEYS_DIR_LTR.has(node.type)) {
result = 'ltr';
text = null; // Flag to skip text evaluation
}
break;
}
case 'slot': {
text = getSlottedTextContent(node);
break;
}
case 'textarea': {
text = node.value;
break;
}
default: {
const items = [].slice.call(node.childNodes);
for (const item of items) {
const {
dir: itemDir,
localName: itemLocalName,
nodeType: itemNodeType,
textContent: itemTextContent
} = item;
if (itemNodeType === TEXT_NODE) {
text = itemTextContent.trim();
} else if (
itemNodeType === ELEMENT_NODE &&
!KEYS_NODE_DIR_EXCLUDE.has(itemLocalName) &&
(!itemDir || (itemDir !== 'ltr' && itemDir !== 'rtl'))
) {
if (itemLocalName === 'slot') {
text = getSlottedTextContent(item);
} else {
text = itemTextContent.trim();
}
}
if (text) {
break;
}
}
}
}
if (text !== null) {
if (text) {
const {
paragraphs: [{ level }]
} = getEmbeddingLevels(text);
if (level % 2 === 1) {
result = 'rtl';
}
} else if (parentNode) {
const { nodeType: parentNodeType } = parentNode;
if (parentNodeType === ELEMENT_NODE) {
result = getDirectionality(parentNode, dirCache);
}
}
}
} else if (localName === 'input' && node.type === 'tel') {
result = 'ltr';
} else if (localName === 'bdi') {
const text = node.textContent.trim();
if (text) {
const {
paragraphs: [{ level }]
} = getEmbeddingLevels(text);
if (level % 2 === 1) {
result = 'rtl';
}
}
} else if (parentNode) {
if (localName === 'slot') {
const text = getSlottedTextContent(node);
if (text) {
const {
paragraphs: [{ level }]
} = getEmbeddingLevels(text);
if (level % 2 === 1) {
result = 'rtl';
} else {
result = 'ltr';
}
} else {
const { nodeType: parentNodeType } = parentNode;
if (parentNodeType === ELEMENT_NODE) {
result = getDirectionality(parentNode, dirCache);
}
}
} else {
const { nodeType: parentNodeType } = parentNode;
if (parentNodeType === ELEMENT_NODE) {
result = getDirectionality(parentNode, dirCache);
}
}
}
dirCache.set(node, result);
return result;
};
/**
* Get language attribute of a node.
* @param {object} node - The Element node.
* @param {WeakMap} [langCache] - Cache for language attributes.
* @returns {?string} - Language attribute value.
*/
export const getLanguageAttribute = (node, langCache = new WeakMap()) => {
if (!node?.nodeType) {
throw new TypeError(`Unexpected type ${getType(node)}`);
}
if (node.nodeType !== ELEMENT_NODE) {
return null;
}
if (langCache.has(node)) {
return langCache.get(node);
}
const { contentType } = node.ownerDocument;
const isHtml = REG_IS_HTML.test(contentType);
const isXml = REG_IS_XML.test(contentType);
let isShadow = false;
let result;
const visited = [];
// Traverse up from the current node to the root.
let current = node;
while (current) {
if (current.nodeType === ELEMENT_NODE && langCache.has(current)) {
result = langCache.get(current);
break;
}
if (current.nodeType === ELEMENT_NODE) {
visited.push(current);
}
// Check if the current node is an element.
switch (current.nodeType) {
case ELEMENT_NODE: {
// Check for and return the language attribute if present.
if (isHtml && current.hasAttribute('lang')) {
result = current.getAttribute('lang');
} else if (isXml && current.hasAttribute('xml:lang')) {
result = current.getAttribute('xml:lang');
}
break;
}
case DOCUMENT_FRAGMENT_NODE: {
// Continue traversal if the current node is a shadow root.
if (current.host) {
isShadow = true;
}
break;
}
case DOCUMENT_NODE:
default: {
// Stop if we reach the root document node.
result = null;
}
}
if (result !== undefined) {
break;
}
if (isShadow) {
current = current.host;
isShadow = false;
} else if (current.parentNode) {
current = current.parentNode;
} else {
break;
}
}
if (result === undefined) {
result = null;
}
for (const visitedNode of visited) {
langCache.set(visitedNode, result);
}
return result;
};
/**
* Check if content is editable.
* NOTE: Not implemented in jsdom https://github.com/jsdom/jsdom/issues/1670
* @param {object} node - The Element node.
* @returns {boolean} - True if content is editable.
*/
export const isContentEditable = node => {
if (!node?.nodeType) {
throw new TypeError(`Unexpected type ${getType(node)}`);
}
if (node.nodeType !== ELEMENT_NODE) {
return false;
}
if (typeof node.isContentEditable === 'boolean') {
return node.isContentEditable;
} else if (node.ownerDocument.designMode === 'on') {
return true;
} else {
let attr;
if (node.hasAttribute('contenteditable')) {
attr = node.getAttribute('contenteditable');
} else {
attr = 'inherit';
}
switch (attr) {
case '':
case 'true': {
return true;
}
case 'plaintext-only': {
// FIXME:
// @see https://github.com/w3c/editing/issues/470
// @see https://github.com/whatwg/html/issues/10651
return true;
}
case 'false': {
return false;
}
default: {
if (node?.parentNode?.nodeType === ELEMENT_NODE) {
return isContentEditable(node.parentNode);
}
return false;
}
}
}
};
/**
* Check if a node is visible.
* @param {object} node - The Element node.
* @returns {boolean} - True if the node is visible.
*/
export const isVisible = node => {
if (node?.nodeType !== ELEMENT_NODE) {
return false;
}
// TODO: switch to node.checkVisibility().
const window = node.ownerDocument.defaultView;
const { display, visibility } = window.getComputedStyle(node);
return display !== 'none' && visibility === 'visible';
};
/**
* Check if focus is visible on the node.
* @param {object} node - The Element node.
* @returns {boolean} - True if focus is visible.
*/
export const isFocusVisible = node => {
if (node?.nodeType !== ELEMENT_NODE) {
return false;
}
const { localName, type } = node;
switch (localName) {
case 'input': {
if (!type || KEYS_INPUT_EDIT.has(type)) {
return true;
}
return false;
}
case 'textarea': {
return true;
}
default: {
return isContentEditable(node);
}
}
};
/**
* Check if an area is focusable.
* @param {object} node - The Element node.
* @returns {boolean} - True if the area is focusable.
*/
export const isFocusableArea = node => {
if (node?.nodeType !== ELEMENT_NODE) {
return false;
}
if (!node.isConnected) {
return false;
}
const window = node.ownerDocument.defaultView;
if (node instanceof window.HTMLElement) {
if (Number.isInteger(parseInt(node.getAttribute('tabindex')))) {
return true;
}
if (isContentEditable(node)) {
return true;
}
const { localName, parentNode } = node;
switch (localName) {
case 'a': {
if (node.href || node.hasAttribute('href')) {
return true;
}
return false;
}
case 'iframe': {
return true;
}
case 'input': {
if (
node.disabled ||
node.hasAttribute('disabled') ||
node.hidden ||
node.hasAttribute('hidden')
) {
return false;
}
return true;
}
case 'summary': {
if (parentNode.localName === 'details') {
let child = parentNode.firstElementChild;
let bool = false;
while (child) {
if (child.localName === 'summary') {
bool = child === node;
break;
}
child = child.nextElementSibling;
}
return bool;
}
return false;
}
default: {
if (
KEYS_NODE_FOCUSABLE.has(localName) &&
!(node.disabled || node.hasAttribute('disabled'))
) {
return true;
}
}
}
} else if (node instanceof window.SVGElement) {
if (Number.isInteger(parseInt(node.getAttributeNS(null, 'tabindex')))) {
const ns = 'http://www.w3.org/2000/svg';
let bool;
let refNode = node;
while (refNode.namespaceURI === ns) {
bool = KEYS_NODE_FOCUSABLE_SVG.has(refNode.localName);
if (bool) {
break;
}
if (refNode?.parentNode?.namespaceURI === ns) {
refNode = refNode.parentNode;
} else {
break;
}
}
if (bool) {
return false;
}
return true;
}
if (
node.localName === 'a' &&
(node.href || node.hasAttributeNS(null, 'href'))
) {
return true;
}
}
return false;
};
/**
* Check if a node is focusable.
* NOTE: Not applied, needs fix in jsdom itself.
* @see https://github.com/whatwg/html/pull/8392
* @see https://phabricator.services.mozilla.com/D156219
* @see https://github.com/jsdom/jsdom/issues/3029
* @see https://github.com/jsdom/jsdom/issues/3464
* @param {object} node - The Element node.
* @returns {boolean} - True if the node is focusable.
*/
export const isFocusable = node => {
if (node?.nodeType !== ELEMENT_NODE) {
return false;
}
const window = node.ownerDocument.defaultView;
let refNode = node;
let res = true;
while (refNode) {
if (refNode.disabled || refNode.hasAttribute('disabled')) {
res = false;
break;
}
if (refNode.hidden || refNode.hasAttribute('hidden')) {
res = false;
}
const { contentVisibility, display, visibility } =
window.getComputedStyle(refNode);
if (
display === 'none' ||
visibility !== 'visible' ||
(contentVisibility === 'hidden' && refNode !== node)
) {
res = false;
} else {
res = true;
}
if (res && refNode?.parentNode?.nodeType === ELEMENT_NODE) {
refNode = refNode.parentNode;
} else {
break;
}
}
return res;
};
/**
* Get namespace URI.
* @param {string} ns - The namespace prefix.
* @param {object} node - The Element node.
* @returns {?string} - The namespace URI.
*/
export const getNamespaceURI = (ns, node) => {
if (typeof ns !== 'string') {
throw new TypeError(`Unexpected type ${getType(ns)}`);
} else if (!node?.nodeType) {
throw new TypeError(`Unexpected type ${getType(node)}`);
}
if (!ns || node.nodeType !== ELEMENT_NODE) {
return null;
}
const { attributes } = node;
let res;
for (const attr of attributes) {
const { name, namespaceURI, prefix, value } = attr;
if (name === `xmlns:${ns}`) {
res = value;
} else if (prefix === ns) {
res = namespaceURI;
}
if (res) {
break;
}
}
return res ?? null;
};
/**
* Check if a namespace is declared.
* @param {string} ns - The namespace.
* @param {object} node - The Element node.
* @returns {boolean} - True if the namespace is declared.
*/
export const isNamespaceDeclared = (ns = '', node = {}) => {
if (!ns || typeof ns !== 'string' || node?.nodeType !== ELEMENT_NODE) {
return false;
}
if (node.lookupNamespaceURI(ns)) {
return true;
}
const root = node.ownerDocument.documentElement;
let parent = node;
let res;
while (parent) {
res = getNamespaceURI(ns, parent);
if (res || parent === root) {
break;
}
parent = parent.parentNode;
}
return !!res;
};
/**
* Check if nodeA precedes and/or contains nodeB.
* @param {object} nodeA - The first Element node.
* @param {object} nodeB - The second Element node.
* @returns {boolean} - True if nodeA precedes nodeB.
*/
export const isPreceding = (nodeA, nodeB) => {
if (!nodeA?.nodeType) {
throw new TypeError(`Unexpected type ${getType(nodeA)}`);
} else if (!nodeB?.nodeType) {
throw new TypeError(`Unexpected type ${getType(nodeB)}`);
}
if (nodeA.nodeType !== ELEMENT_NODE || nodeB.nodeType !== ELEMENT_NODE) {
return false;
}
const posBit = nodeB.compareDocumentPosition(nodeA);
const res =
posBit & DOCUMENT_POSITION_PRECEDING || posBit & DOCUMENT_POSITION_CONTAINS;
return !!res;
};
/**
* Comparison function for sorting nodes based on document position.
* @param {object} a - The first node.
* @param {object} b - The second node.
* @returns {number} - Sort order.
*/
export const compareNodes = (a, b) => {
if (isPreceding(b, a)) {
return 1;
}
return -1;
};
/**
* Sort a collection of nodes.
* @param {Array.<object>|Set.<object>} nodes - Collection of nodes.
* @returns {Array.<object>} - Collection of sorted nodes.
*/
export const sortNodes = (nodes = []) => {
const arr = [...nodes];
if (arr.length > 1) {
arr.sort(compareNodes);
}
return arr;
};
/**
* Traverses AST nodes to find the most optimal seed selector
* (ID > Class > Tag).
* @param {Array} nodes - AST nodes to traverse.
* @param {object} [state] - The current state of the search.
* @returns {object} The search state containing the best seed.
*/
export const findBestSeed = (nodes, state = { seed: null, priority: 0 }) => {
for (const node of nodes) {
if (state.priority === 3) {
return state;
}
if (Array.isArray(node)) {
findBestSeed(node, state);
} else if (node && typeof node === 'object') {
// ID Selector (Fastest: getElementById)
if (node.type === ID_SELECTOR) {
state.seed = { type: 'id', value: node.name };
state.priority = 3;
return state;
} else if (node.type === CLASS_SELECTOR && state.priority < 2) {
// Class Selector (Faster: getElementsByClassName)
state.seed = { type: 'class', value: node.name };
state.priority = 2;
} else if (
node.type === TYPE_SELECTOR &&
state.priority < 1 &&
node.name !== '*'
) {
// Type/Tag Selector (Excludes universal '*')
state.seed = { type: 'tag', value: node.name };
state.priority = 1;
}
if (node.children) {
findBestSeed(node.children, state);
}
}
}
return state;
};
/**
* Traces the DOM tree upwards and sideways from a seed element,
* populating the allowlist with safe paths for :has() evaluation.
* @param {object} current - The starting seed element.
* @param {WeakSet} list - The WeakSet to populate.
* @param {Set} visitedAncestors - The Set to track visited nodes.
* @returns {void}
*/
export const populateHasAllowlist = (current, list, visitedAncestors) => {
list.add(current);
while (
current &&
(current.nodeType === ELEMENT_NODE ||
current.nodeType === DOCUMENT_FRAGMENT_NODE)
) {
if (visitedAncestors.has(current)) {
break;
}
visitedAncestors.add(current);
let sibling = current.previousElementSibling;
while (sibling) {
list.add(sibling);
sibling = sibling.previousElementSibling;
}
sibling = current.nextElementSibling;
while (sibling) {
list.add(sibling);
sibling = sibling.nextElementSibling;
}
current = current.parentNode;
if (current) {
list.add(current);
}
}
};
/**
* Collects all descendant elements of a given node using a TreeWalker.
* @param {Document|DocumentFragment|Element} node - The node to start from.
* @param {Document} document - The Document used to create the TreeWalker.
* @returns {Array<Element>} An array containing all descendant elements.
*/
export const collectAllDescendants = (node, document) => {
if (!node?.nodeType) {
throw new TypeError(`Unexpected type ${getType(node)}`);
}
if (document?.nodeType !== DOCUMENT_NODE) {
throw new TypeError(`Unexpected type ${getType(document)}`);
}
const walker = document.createTreeWalker(node, SHOW_ELEMENT);
const descendants = [];
let refNode = walker.nextNode();
while (refNode) {
descendants.push(refNode);
refNode = walker.nextNode();
}
return descendants;
};
/**
* Gets the traversal direction and starting twig.
* @param {Array.<object>} branch - The selector branch.
* @param {string} targetType - The target type.
* @param {boolean} hasScope - True if selector includes ':scope'.
* @param {boolean} scoped - True if traversal is scoped within target node.
* @returns {object} Object containing dir and twig properties.
*/
export const getTraversalStrategy = (branch, targetType, hasScope, scoped) => {
const branchLen = branch.length;
const firstTwig = branch[0];
if (branchLen === 1) {
return { dir: DIR_PREV, twig: firstTwig };
}
const lastTwig = branch[branchLen - 1];
const {
leaves: [{ name: firstName, type: firstType }]
} = firstTwig;
const {
leaves: [{ name: lastName, type: lastType }]
} = lastTwig;
const lastCombo = branch[branchLen - 2].combo?.name;
if (
hasScope ||
lastType === PS_ELEMENT_SELECTOR ||
lastType === ID_SELECTOR ||
lastCombo === '+' ||
lastCombo === '~'
) {
return { dir: DIR_PREV, twig: lastTwig };
} else if (firstType === ID_SELECTOR) {
return { dir: DIR_NEXT, twig: firstTwig };
} else if (firstName === '*' && firstType === TYPE_SELECTOR) {
return { dir: DIR_PREV, twig: lastTwig };
} else if (lastName === '*' && lastType === TYPE_SELECTOR) {
return { dir: DIR_NEXT, twig: firstTwig };
} else if (branchLen === 1 || branchLen === 2) {
return { dir: DIR_PREV, twig: lastTwig };
} else if (branchLen > 2 && scoped && targetType === TARGET_FIRST) {
return { dir: DIR_PREV, twig: lastTwig };
}
return { dir: DIR_NEXT, twig: firstTwig };
};
+27
View File
@@ -0,0 +1,27 @@
/*!
* DOM Selector - A CSS selector engine.
* @license MIT
* @copyright asamuzaK (Kazz)
* @see {@link https://github.com/asamuzaK/domSelector/blob/main/LICENSE}
*/
export type CheckResult = {
match: boolean;
pseudoElement: string | null;
ast: object | null;
};
export declare class DOMSelector {
#private;
constructor(window: Window, document: Document, opt?: object);
clear: (clearAll?: boolean) => void;
extractSubjects: (selector: string, caseSensitive?: boolean) => Array<{
id: string | null;
className: string | null;
tag: string | null;
}>;
supports: (selector: string) => boolean;
check: (selector: string, node: Element, opt?: object) => CheckResult;
matches: (selector: string, node: Element, opt?: object) => boolean;
closest: (selector: string, node: Element, opt?: object) => Element | null;
querySelector: (selector: string, node: Document | DocumentFragment | Element, opt?: object) => Element | null;
querySelectorAll: (selector: string, node: Document | DocumentFragment | Element, opt?: object) => Array<Element>;
}
+89
View File
@@ -0,0 +1,89 @@
export declare const ATRULE = "Atrule";
export declare const ATTR_SELECTOR = "AttributeSelector";
export declare const CLASS_SELECTOR = "ClassSelector";
export declare const COMBINATOR = "Combinator";
export declare const DIR_NEXT = "next";
export declare const DIR_PREV = "prev";
export declare const IDENT = "Identifier";
export declare const ID_SELECTOR = "IdSelector";
export declare const NEST_SELECTOR = "NestingSelector";
export declare const NOT_SUPPORTED_ERR = "NotSupportedError";
export declare const NTH = "Nth";
export declare const OPERATOR = "Operator";
export declare const PS_CLASS_SELECTOR = "PseudoClassSelector";
export declare const PS_ELEMENT_SELECTOR = "PseudoElementSelector";
export declare const RULE = "Rule";
export declare const SCOPE = "Scope";
export declare const SELECTOR = "Selector";
export declare const SELECTOR_LIST = "SelectorList";
export declare const STRING = "String";
export declare const SYNTAX_ERR = "SyntaxError";
export declare const TARGET_ALL = "all";
export declare const TARGET_FIRST = "first";
export declare const TARGET_LINEAL = "lineal";
export declare const TARGET_SELF = "self";
export declare const TYPE_SELECTOR = "TypeSelector";
export declare const BIT_01 = 1;
export declare const BIT_02 = 2;
export declare const BIT_04 = 4;
export declare const BIT_08 = 8;
export declare const BIT_16 = 16;
export declare const BIT_32 = 32;
export declare const BIT_FFFF = 65535;
export declare const DUO = 2;
export declare const HEX = 16;
export declare const TYPE_FROM = 8;
export declare const TYPE_TO = -1;
export declare const ELEMENT_NODE = 1;
export declare const TEXT_NODE = 3;
export declare const DOCUMENT_NODE = 9;
export declare const DOCUMENT_FRAGMENT_NODE = 11;
export declare const DOCUMENT_POSITION_PRECEDING = 2;
export declare const DOCUMENT_POSITION_CONTAINS = 8;
export declare const DOCUMENT_POSITION_CONTAINED_BY = 16;
export declare const FILTER_ACCEPT = 1;
export declare const FILTER_REJECT = 2;
export declare const FILTER_SKIP = 3;
export declare const SHOW_ALL = 4294967295;
export declare const SHOW_CONTAINER = 1281;
export declare const SHOW_DOCUMENT = 256;
export declare const SHOW_DOCUMENT_FRAGMENT = 1024;
export declare const SHOW_ELEMENT = 1;
export declare const ALPHA_NUM = "[A-Z\\d]+";
export declare const CHILD_IDX = "(?:first|last|only)-(?:child|of-type)";
export declare const DIGIT = "(?:0|[1-9]\\d*)";
export declare const LANG_PART = "(?:-[A-Z\\d]+)*";
export declare const PSEUDO_CLASS = "(?:any-)?link|(?:first|last|only)-(?:child|of-type)|checked|empty|indeterminate|read-(?:only|write)|target";
export declare const ANB = "[+-]?(?:(?:0|[1-9]\\d*)n?|n)|(?:[+-]?(?:0|[1-9]\\d*))?n\\s*[+-]\\s*(?:0|[1-9]\\d*)";
export declare const COMBO = "\\s?[\\s>~+]\\s?";
export declare const DESCEND = "\\s?[\\s>]\\s?";
export declare const SIBLING = "\\s?[+~]\\s?";
export declare const LOGIC_IS = ":is\\(\\s*[^)]+\\s*\\)";
export declare const N_TH = "nth-(?:last-)?(?:child|of-type)\\(\\s*(?:even|odd|[+-]?(?:(?:0|[1-9]\\d*)n?|n)|(?:[+-]?(?:0|[1-9]\\d*))?n\\s*[+-]\\s*(?:0|[1-9]\\d*))\\s*\\)";
export declare const ATTR_TYPE = "\\[[^|\\]]+\\]";
export declare const SUB_TYPE = "\\[[^|\\]]+\\]|[#.:][\\w-]+";
export declare const SUB_TYPE_WO_PSEUDO = "\\[[^|\\]]+\\]|[#.][\\w-]+";
export declare const TAG_TYPE_WO_UNIVERSAL = "[A-Za-z][\\w-]*";
export declare const TAG_TYPE = "\\*|[A-Za-z][\\w-]*";
export declare const TAG_TYPE_I = "\\*|[A-Z][\\w-]*";
export declare const COMPOUND = "(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+)+)";
export declare const COMPOUND_L = "(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+|:is\\(\\s*[^)]+\\s*\\))+)";
export declare const COMPOUND_I = "(?:\\*|[A-Z][\\w-]*|(?:\\*|[A-Z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+)+)";
export declare const COMPOUND_WO_PSEUDO = "(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.][\\w-]+)+)";
export declare const COMPLEX = "(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+)+)(?:\\s?[\\s>~+]\\s?(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+)+))*";
export declare const COMPLEX_L = "(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+|:is\\(\\s*[^)]+\\s*\\))+)(?:\\s?[\\s>~+]\\s?(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+|:is\\(\\s*[^)]+\\s*\\))+))*";
export declare const HAS_COMPOUND = "has\\([\\s>]?\\s*(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.][\\w-]+)+)\\s*\\)";
export declare const LOGIC_COMPOUND = "(?:is|not)\\(\\s*(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+|:is\\(\\s*[^)]+\\s*\\))+)(?:\\s*,\\s*(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+|:is\\(\\s*[^)]+\\s*\\))+))*\\s*\\)";
export declare const LOGIC_COMPLEX = "(?:is|not)\\(\\s*(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+|:is\\(\\s*[^)]+\\s*\\))+)(?:\\s?[\\s>~+]\\s?(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+|:is\\(\\s*[^)]+\\s*\\))+))*(?:\\s*,\\s*(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+|:is\\(\\s*[^)]+\\s*\\))+)(?:\\s?[\\s>~+]\\s?(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+|:is\\(\\s*[^)]+\\s*\\))+))*)*\\s*\\)";
export declare const FORM_PARTS: readonly string[];
export declare const INPUT_BUTTON: readonly string[];
export declare const INPUT_CHECK: readonly string[];
export declare const INPUT_DATE: readonly string[];
export declare const INPUT_TEXT: readonly string[];
export declare const INPUT_EDIT: readonly string[];
export declare const INPUT_LTR: readonly string[];
export declare const ATTR_VALUE_I: readonly string[];
export declare const KEYS_LOGICAL: Set<string>;
export declare const KEYS_PS_CLASS_SUPPORTED: Set<string>;
export declare const KEYS_PS_CLASS_UNSUPPORTED: Set<string>;
export declare const KEYS_PS_ELEMENT_UNSUPPORTED: Set<string>;
+41
View File
@@ -0,0 +1,41 @@
export declare class Evaluator {
#private;
window: object;
documentCache: WeakMap<WeakKey, any>;
check: boolean | undefined;
noexcept: boolean | undefined;
warn: boolean | undefined;
matchOpts: {
warn: boolean;
} | undefined;
node: object | undefined;
pseudoElements: any[] | undefined;
invalidate: boolean | undefined;
constructor(window: object);
setup(selector: string, node: object, opt?: {
check?: boolean;
noexcept?: boolean;
warn?: boolean;
}): object;
onError: (e: Error, opt?: {
noexcept?: boolean;
}) => void;
clearResults: (all?: boolean) => void;
matchSelector: (ast: object, node: object, opt: object) => boolean;
matchLeaves: (leaves: Array<object>, node: object, opt: object) => boolean;
getFilterLeaves: (leaves: Array<object>) => Array<object>;
evaluateShadowHost: (ast: object, node: object) => boolean;
matchPseudoClassSelector: (ast: object, node: object, opt?: {
forgive?: boolean;
warn?: boolean;
}) => Set<object> | boolean;
createTreeWalker: (node: object, opt?: {
force?: boolean;
whatToShow?: number;
}) => object;
yieldCombinatorMatches(twig: object, node: object, opt?: {
dir?: string;
}): Generator<any, void, unknown>;
yieldTraverseAllDescendants(baseNode: object, leaves: Array<object>, opt: object): Generator<any, void, unknown>;
yieldFindDescendantNodes(leaves: Array<object>, baseNode: object, opt: object): Generator<any, void, unknown>;
}
+13
View File
@@ -0,0 +1,13 @@
export declare class EventHandler {
#private;
constructor(window: object);
get currentEvent(): Event | null;
get currentFocus(): Event | null;
get lastFocusVisible(): Node | null;
set lastFocusVisible(node: Node);
handleFocusEvent: (evt: Event) => void;
handleKeyboardEvent: (evt: KeyboardEvent) => void;
handleMouseEvent: (evt: MouseEvent) => void;
registerEventListeners: () => void;
destroy: () => void;
}
+10
View File
@@ -0,0 +1,10 @@
import { Evaluator } from './evaluator.js';
export declare class Finder extends Evaluator {
#private;
setup(selector: string, node: object, opt?: {
check?: boolean;
noexcept?: boolean;
warn?: boolean;
}): object;
find: (targetType: string) => Set<object> | object;
}
+5
View File
@@ -0,0 +1,5 @@
export declare class Mapper {
#private;
constructor(context: import('./finder.js').Finder);
correspond(selector: string): any[];
}
+24
View File
@@ -0,0 +1,24 @@
export declare const matchPseudoElementSelector: (astName: string, astType: string, { forgive, globalObject, warn }?: {
forgive?: boolean;
globalObject?: object;
warn?: boolean;
}) => void;
export declare const matchDirectionPseudoClass: (ast: object, node: object, dirCache?: WeakMap<any, any>) => boolean;
export declare const matchLanguagePseudoClass: (ast: object, node: object, langCache?: WeakMap<any, any>) => boolean;
export declare const matchCheckedPseudoClass: (node: object) => boolean;
export declare const matchLinkPseudoClass: (node: object) => boolean;
export declare const matchOpenPseudoClass: (node: object) => boolean;
export declare const matchPlaceholderShownPseudoClass: (node: object, keys: Set<string>) => boolean;
export declare const matchRangePseudoClass: (astName: string, node: object, keys: Set<string>) => boolean;
export declare const matchReadOnlyPseudoClass: (astName: string, node: object) => boolean;
export declare const matchRequiredPseudoClass: (astName: string, node: object, keys: Set<string>) => boolean;
export declare const matchAttributeSelector: (ast: object, node: object, { check, forgive, globalObject }?: {
check?: boolean;
forgive?: boolean;
globalObject?: object;
}) => boolean;
export declare const matchTypeSelector: (ast: object, node: object, { check, forgive, globalObject }?: {
check?: boolean;
forgive?: boolean;
globalObject?: object;
}) => boolean;
+63
View File
@@ -0,0 +1,63 @@
export declare const concatCall: (nodes: Array<Element> | NodeList, callback?: ((element: Element) => boolean | void) | undefined) => Array<Element>;
export declare const isHTML: (node: Element | Document) => boolean;
export declare const isTarget: (node: Element) => boolean;
export declare const isIndeterminate: (node: Element) => boolean;
export declare const optimize: (selector: string, tokens: Array<string>) => string;
export declare const matchAssert: (factory: Array<(c: Element, f?: ((element: Element) => boolean | void), x?: null, r?: boolean) => boolean>, element: Element, callback?: ((element: Element) => boolean | void) | undefined) => boolean;
export declare const solveNth: (element: Element, dir: boolean | number, state: object, isOfType: boolean) => number;
export declare class Nwsapi {
#private;
hasDupes: boolean | undefined;
constructor(window: object, document: object, cacheSize?: number);
private #documentOrder;
private #unique;
byId(id: string, context: Element | Document): Array<Element>;
byTag(tag: string, context: Element | Document | DocumentFragment): Array<Element>;
byClass(cls: string, context: Element | Document | DocumentFragment): Array<Element>;
private #emit;
private #switchContext;
compileSelector(expression: string, source: string, mode: boolean): string;
compileUniversal(selector: string, source: string): {
source: string;
selector: string;
};
compileId(selector: string, source: string): {
source: string;
selector: string;
};
compileClass(selector: string, source: string): {
source: string;
selector: string;
};
compileTag(selector: string, source: string): {
source: string;
selector: string;
};
compileAttribute(selector: string, source: string): {
source: string;
selector: string;
};
compileCombinator(symbol: string, selector: string, source: string): {
source: string;
selector: string;
};
compilePseudo(selector: string, source: string, selectorString: string): {
source: string;
selector: string;
};
compilePseudoStructural(match: Array<string>, source: string): string;
compilePseudoTreeStruct(match: Array<string>, source: string, selectorString: string): string;
compilePseudoLogical(match: Array<string>, source: string): string;
compilePseudoLocation(match: Array<string>, source: string): string;
compilePseudoInputState(match: Array<string>, source: string): string;
compilePseudoInputValue(match: Array<string>, source: string): string;
compile(selector: string, mode: boolean): (c: Element | Element[] | NodeList, f?: ((element: Element) => boolean | void), x?: Element | Document | null, r?: boolean | Element[]) => boolean | Element[];
collect(selectors: Array<string>, context: Element | Document, callback?: ((element: Element) => boolean | void) | undefined): object;
private #matchCollect;
private #parseSelector;
match(selectors: string, element: Element, callback?: ((element: Element) => boolean | void) | undefined): boolean;
closest(selectors: string, element: Element, callback?: ((element: Element) => boolean | void) | undefined): Element | null;
select(selectors: string, context: Element | Document, callback?: ((element: Element) => boolean | void) | undefined): Array<Element>;
first(selectors: string, context: Element | Document, callback?: ((element: Element) => boolean | void) | undefined): Element | null;
clear(clearAll?: boolean): void;
}
+19
View File
@@ -0,0 +1,19 @@
export declare const unescapeSelector: (selector?: string) => string;
export declare const preprocess: (value: string) => string;
export declare const parseSelector: (sel: string, context?: string) => object;
export declare const walkAST: (ast?: object, toObject?: boolean, callback?: Function) => {
branches: Array<object>;
info: object;
};
export declare const compareASTNodes: (a: object, b: object) => number;
export declare const sortAST: (asts: Array<object>) => Array<object>;
export declare const parseAstName: (selector: string) => {
prefix: string;
localName: string;
};
export declare const extractSubjectsAst: (ast: object) => Array<{
id: string | null;
className: string | null;
tag: string | null;
}>;
export { find as findAST, generate as generateCSS } from 'css-tree';
@@ -0,0 +1,5 @@
export declare class SelectorProcessor {
#private;
constructor(context: import('./finder.js').Finder);
process(branches: Array<object>, selector: string): object;
}
+12
View File
@@ -0,0 +1,12 @@
export declare const findNestedHas: (leaf: object) => object | null;
export declare const findLogicalWithNestedHas: (leaf: object) => object | null;
export declare const validateHasNesting: (astChildren: Array<object>) => boolean;
export declare const createHasValidator: (globalObj: object) => Function;
export declare const isInvalidCombinator: (type: string, prevType: string | null, isLast: boolean) => boolean;
export declare const isSupportedAST: (ast: object) => boolean;
export declare const extractSubjectsRegExp: (selector: string, caseSensitive: boolean) => Array<{
id: string | null;
className: string | null;
tag: string | null;
}>;
export declare const filterSelector: (selector: string, target: string) => boolean;
+25
View File
@@ -0,0 +1,25 @@
export declare const getType: (o: object) => string;
export declare const verifyArray: (arr: any[], type: string) => any[];
export declare const generateException: (msg: string, name: string, globalObject?: object) => DOMException;
export declare const resolveContent: (node: object) => Array<object | boolean>;
export declare const traverseNode: (node: object, walker: object, force?: boolean) => object | null;
export declare const isCustomElement: (node: object, { formAssociated }?: {
formAssociated?: boolean;
}) => boolean;
export declare const getSlottedTextContent: (node: object) => string | null;
export declare const getDirectionality: (node: object, dirCache?: WeakMap<any, any>) => string | null;
export declare const getLanguageAttribute: (node: object, langCache?: WeakMap<any, any>) => string | null;
export declare const isContentEditable: (node: object) => boolean;
export declare const isVisible: (node: object) => boolean;
export declare const isFocusVisible: (node: object) => boolean;
export declare const isFocusableArea: (node: object) => boolean;
export declare const isFocusable: (node: object) => boolean;
export declare const getNamespaceURI: (ns: string, node: object) => string | null;
export declare const isNamespaceDeclared: (ns?: string, node?: object) => boolean;
export declare const isPreceding: (nodeA: object, nodeB: object) => boolean;
export declare const compareNodes: (a: object, b: object) => number;
export declare const sortNodes: (nodes?: Array<object> | Set<object>) => Array<object>;
export declare const findBestSeed: (nodes: any[], state?: object) => object;
export declare const populateHasAllowlist: (current: object, list: WeakSet<any>, visitedAncestors: Set<any>) => void;
export declare const collectAllDescendants: (node: Document | DocumentFragment | Element, document: Document) => Array<Element>;
export declare const getTraversalStrategy: (branch: Array<object>, targetType: string, hasScope: boolean, scoped: boolean) => object;