Initial commit
This commit is contained in:
commit
afed023152
8152 files changed
+2086461
No files matched your search
+21
@@ -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
@@ -0,0 +1,128 @@
|
||||
# CSS color
|
||||
|
||||
[](https://github.com/asamuzaK/cssColor/actions/workflows/node.js.yml)
|
||||
[](https://github.com/asamuzaK/cssColor/actions/workflows/github-code-scanning/codeql)
|
||||
[](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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 {};
|
||||
+3521
File diff suppressed because it is too large.
Load diff
+1
@@ -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
@@ -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
@@ -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
|
||||
+1
@@ -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
@@ -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
@@ -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
|
||||
+1
@@ -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
@@ -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
@@ -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
|
||||
+1
@@ -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
|
||||
+122
@@ -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
@@ -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
|
||||
+1
@@ -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
|
||||
+79
@@ -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
@@ -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
|
||||
+1
@@ -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
|
||||
+66
@@ -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 {};
|
||||
+462
@@ -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
|
||||
+1
@@ -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
@@ -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
@@ -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
|
||||
+1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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}`;
|
||||
};
|
||||
+3458
File diff suppressed because it is too large.
Load diff
+31
@@ -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
@@ -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
@@ -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
|
||||
};
|
||||
+1311
File diff suppressed because it is too large.
Load diff
+371
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,259 @@
|
||||
# DOM Selector
|
||||
|
||||
[](https://github.com/asamuzaK/domSelector/actions/workflows/node.js.yml)
|
||||
[](https://github.com/asamuzaK/domSelector/actions/workflows/github-code-scanning/codeql)
|
||||
[](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
@@ -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
@@ -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
@@ -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'
|
||||
]);
|
||||
+2218
File diff suppressed because it is too large.
Load diff
+154
@@ -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;
|
||||
};
|
||||
}
|
||||
+1033
File diff suppressed because it is too large.
Load diff
+101
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
+1351
File diff suppressed because it is too large.
Load diff
+504
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
|
||||
export declare class Mapper {
|
||||
#private;
|
||||
constructor(context: import('./finder.js').Finder);
|
||||
correspond(selector: string): any[];
|
||||
}
|
||||
+24
@@ -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
@@ -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
@@ -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';
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export declare class SelectorProcessor {
|
||||
#private;
|
||||
constructor(context: import('./finder.js').Finder);
|
||||
process(branches: Array<object>, selector: string): object;
|
||||
}
|
||||
+12
@@ -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
@@ -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;
|
||||
Reference in new issue
Block a user