@il4mb/css-tokenizer 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/registry.d.ts +12 -0
- package/dist/types/tokenizer.d.ts +15 -0
- package/dist/types/tools.d.ts +2 -0
- package/dist/types/tupleList.d.ts +17 -0
- package/package.json +11 -5
- package/readme.md +2 -2
- package/tsconfig.build.json +15 -0
- package/index.test.ts +0 -24
- package/src/global.d.ts +0 -20
- package/src/index.ts +0 -15
- package/src/registry.ts +0 -174
- package/src/tokenizer.ts +0 -144
- package/src/tools.ts +0 -37
- package/src/tupleList.ts +0 -73
- package/src/type.d.ts +0 -35
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var y=["aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgrey","darkgreen","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","indianred","indigo","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgrey","lightgreen","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","turquoise","violet","wheat","white","whitesmoke","yellow","yellowgreen","transparent","currentcolor"],f=[/^rgba?\(/i,/^hsla?\(/i,/^hwb\(/i,/^lab\(/i,/^lch\(/i,/^oklab\(/i,/^oklch\(/i,/^color\(/i,/^color-mix\(/i],k=/^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})\b/i,b=["px","cm","mm","q","in","pt","pc","em","ex","ch","rem","lh","rlh","cap","ic","vw","vh","vi","vb","vmin","vmax","svw","svh","svi","svb","svmin","svmax","lvw","lvh","lvi","lvb","lvmin","lvmax","dvw","dvh","dvi","dvb","dvmin","dvmax","cqw","cqh","cqi","cqb","cqmin","cqmax","%","deg","grad","rad","turn","s","ms","hz","khz","dpi","dpcm","dppx","x","fr"],p=(e,i)=>{let r=0,t=i,s=!1,n="";while(t<e.length){let a=e[t];if(!s&&(a==='"'||a==="'"))s=!0,n=a;else if(s&&a===n)s=!1;if(!s){if(a==="(")r++;else if(a===")"){if(r--,r===0)return t+1}}t++}return e.length},g=(e,i,r)=>{let t=i;while(t<e.length&&r.test(e[t]))t++;return t};class c{items=[{type:"symbol",kind:"char",regex:/[\s\S]/,priority:0},{type:"escape",kind:"class",regex:/^\\[0-9a-f]{1,6}\s?/i,priority:110,reader({index:e,content:i}){let r=e+1,t=0;while(r<i.length&&/[0-9a-f]/i.test(i[r])&&t<6)r++,t++;if(r<i.length&&/\s/.test(i[r]))r++;return[e,r]}},{type:"unicode-range",kind:"keyword",exp:[/^u\+[0-9a-f?]{1,6}(-[0-9a-f]{1,6})?/i],priority:110,reader({index:e,content:i,matched:r}){let t=r instanceof RegExp?r.exec(i.slice(e)):null;if(!t)return[e,e+1];return[e,e+t[0].length]}},{type:"punctuation",kind:"class",regex:/^[{}()\[\],;:.]/,priority:80,reader({index:e,content:i}){return[e,e+1]}},{type:"operator",kind:"class",regex:/^[+\-*/=<>!~^|&]+/,priority:80,reader({index:e,content:i}){let r=e;while(r<i.length&&/[+\-*/=<>!~^|&]/.test(i[r]))r++;return[e,r]}},{type:"variable",kind:"keyword",exp:[/^--[a-zA-Z0-9-_]+/],priority:110,reader({index:e,content:i,matched:r}){let t=r instanceof RegExp?r.exec(i.slice(e)):null;if(!t)return[e,e+1];return[e,e+t[0].length]}},{type:"comment",kind:"class",regex:/^\/\*/,priority:90,reader({index:e,content:i}){let r=e+2;while(r<i.length&&!(i[r]==="*"&&i[r+1]==="/"))r++;if(r<i.length)r+=2;return[e,r]}},{type:"whitespace",kind:"class",regex:/^\s+/,priority:90,reader({index:e,content:i}){let r=e;while(r<i.length&&/\s/.test(i[r]))r++;return[e,r]}},{type:"url",kind:"class",regex:/^\"https?:\/\/[^\"]+/i,priority:50,reader({index:e,content:i}){let r=e+1;while(r<i.length&&i[r]!=='"')r++;if(r<i.length&&i[r]==='"')r++;return[e,r]}},{type:"string",kind:"class",regex:/^\"[^\"]*\"|^\'[^\']*\'/,priority:20,reader({index:e,content:i}){let r=i[e],t=e+1;while(t<i.length&&i[t]!==r)t++;if(t<i.length&&i[t]===r)t++;return[e,t]}},{type:"atrule",kind:"keyword",exp:[/^@[a-z]+/i],priority:100,reader({index:e,content:i,deepReader:r}){let t=/^@[\w-]+/.exec(i.slice(e));if(!t)return[e,e+1];let s=t[0],n=e+s.length;return r([e,n])}},{type:"function",kind:"keyword",exp:[/^[a-z][a-z0-9-]*\(/i],priority:150,reader({index:e,content:i,deepReader:r}){if(!/^[a-z][a-z0-9-]*\(/i.exec(i.slice(e)))return[e,e+1];let s=p(i,e);return r([e,s])}},{type:"dimension",kind:"keyword",exp:[/^[+-]?[0-9]+[a-z]+/],priority:110,reader({index:e,content:i,deepReader:r}){let t=e;if(i[t]==="-"||i[t]==="+")t++;while(t<i.length&&/[0-9]/.test(i[t]))t++;if(i[t]==="."&&/[0-9]/.test(i[t+1]??"")){t++;while(t<i.length&&/[0-9]/.test(i[t]))t++}while(t<i.length&&/[a-zA-Z%]/.test(i[t]))t++;return r([e,t])}},{type:"number",kind:"class",regex:/^[0-9]/,priority:5,reader({index:e,content:i}){let r=e;while(/^[0-9]$/.test(i[r]))r++;return[e,r]}},{type:"unit",kind:"keyword",exp:b.map((e)=>new RegExp(`^${e}\\b`,"i")),priority:10,reader({index:e,content:i,matched:r}){let t=r instanceof RegExp?r.exec(i.slice(e)):null;if(!t)return[e,e+1];return[e,e+t[0].length]}},{type:"word",kind:"class",regex:/^[a-z]/i,priority:5,reader({index:e,content:i}){let r=g(i,e,/[a-z0-9_-]+/);return[e,r]}},{type:"hash",kind:"class",regex:/^#[a-z0-9]+/i,priority:50,reader({index:e,content:i}){let r=e+1;while(r<i.length&&/^[a-z0-9]+/i.test(i[r]))r++;return[e,r]}},{type:"color",kind:"keyword",priority:120,exp:[...y,k,...f],reader({index:e,content:i,matched:r,deepReader:t}){if(r instanceof RegExp){let n=r.exec(i.slice(e));if(!n)return[e,e+1];let a=n[0];if(a.endsWith("(")){let l=p(i,e);return t([e,l],e+a.length-1)}}let s=g(i,e+1,/[a-zA-Z0-9_-]/);return[e,s]}}];constructor(e=[]){this.items.push(...e),this.sort()}all(){return[...this.items]}add(e){this.items.push({priority:0,...e}),this.sort()}get(e){return this.items[e]}get length(){return this.items.length}[Symbol.iterator](){return this.items[Symbol.iterator]()}indexOf(e){return this.items.indexOf(e)}sort(){this.items.sort((e,i)=>(i.priority??0)-(e.priority??0))}sortExps(e){return[...e].sort((i,r)=>{let t=i instanceof RegExp,s=r instanceof RegExp;if(t!==s)return t?-1:1;if(t&&s)return 0;return r.length-i.length})}}class m{registry;items=[];constructor(e){this.registry=e}push(...e){e.forEach((i)=>{this.items.push(i)})}toArray(){return[...this.items]}toTokenList(e){return this.items.map(([i,r,t])=>({type:this.registry.get(i)?.type??"unknown",start:r,end:t,value:e.slice(r,t)}))}toTokenTree(e){let i=this.toTokenList(e);i.sort((s,n)=>{if(s.start!==n.start)return s.start-n.start;return n.end-s.end});let r=[],t=[];for(let s of i){while(t.length>0&&t[t.length-1].end<=s.start)t.pop();let n=t[t.length-1];if(n){if(!n.children)n.children=[];n.children.push(s)}else r.push(s);t.push(s)}return r}}class d{registry;constructor(e){this.registry=e}getReader(e,i){let r=this.registry.indexOf(e);return(t,s)=>{if(e.kind==="char")return[[r,s,s+1]];let n=(l,u)=>{let o=[[r,l[0],l[1]]],h=t.slice(u??l[0],l[1]),w=this.tokenize(h,{ignoreTypes:[r]}).toArray().map(([x,v,T])=>[x,v+(u??l[0]),T+(u??l[0])]);return o.push(...w),o};if(e.kind==="class"){if("reader"in e&&e.reader){let o=e.reader({content:t,index:s,deepReader:n});if(o.length>0&&Array.isArray(o[0]))return o.map((h)=>h.length===2?[r,...h]:h);return[[r,...o]]}let l=s,u=l+1;while(u<t.length){let o=this.findMatch(t,u);if(o&&r===o[0])u++;else break}return[[r,l,u]]}if(!e.reader){let l=typeof i==="string"?i.length:1;return[[r,s,s+l]]}let a=e.reader({content:t,index:s,matched:i,deepReader:n});if(a.length>0&&Array.isArray(a[0]))return a.map((l)=>l.length===2?[r,...l]:l);return[[r,...a]]}}findMatch(e,i,r){for(let t=0;t<this.registry.length;t++){if(r&&r.includes(t))continue;let s=this.tryMatch(this.registry.get(t),e,i);if(s)return[t,s]}return}tryMatch(e,i,r){if(r>=i.length)return!1;if(e.kind==="keyword"){let t=i.slice(r),s=this.registry.sortExps(e.exp).find((n)=>n instanceof RegExp?n.test(t):t.startsWith(n));return s?this.getReader(e,s):!1}if(e.kind==="class"){if(!e.regex.test(i.slice(r)))return!1;return this.getReader(e)}if(e.kind==="char"){if("regex"in e){if(!e.regex.test(i.slice(r)))return!1;return this.getReader(e)}return e.char===i[r]?this.getReader(e):!1}return!1}tokenize(e,i){let r=new m(this.registry),t=0;while(t<e.length){let s=this.findMatch(e,t,i?.ignoreTypes);if(s){let n=s[1](e,t);if(n&&n.length>0){let a=Math.max(t,...n.map((l)=>l[2]));if(a<=t){r.push([-1,t,t+1]),t++;continue}r.push(...n),t=a;continue}}r.push([-1,t,t+1]),t++}return r}isRange(e){return Array.isArray(e)&&e.length===2&&e.every((i)=>typeof i==="number")}isTuple(e){return Array.isArray(e)&&e.length===3&&e.every((i)=>typeof i==="number")}}var R=new d(new c),A=(e)=>R.tokenize(e);export{f as COLOR_FUNCTIONS,y as CSS_NAMED_COLORS,b as CSS_UNITS,k as HEX_COLOR,c as Registry,d as Tokenizer,m as TupleList,p as findFunctionEnd,g as readWhile,A as tokenizeImpl};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ModelDefinition } from "./type";
|
|
2
|
+
export declare class Registry implements Iterable<ModelDefinition> {
|
|
3
|
+
private items;
|
|
4
|
+
constructor(models?: ModelDefinition[]);
|
|
5
|
+
add(def: ModelDefinition): void;
|
|
6
|
+
get(index: number): ModelDefinition;
|
|
7
|
+
get length(): number;
|
|
8
|
+
[Symbol.iterator](): ArrayIterator<ModelDefinition>;
|
|
9
|
+
indexOf(item: ModelDefinition): number;
|
|
10
|
+
sort(): void;
|
|
11
|
+
sortExps(exps: Exp[]): Exp[];
|
|
12
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Registry } from "./registry";
|
|
2
|
+
import { TupleList } from "./tupleList";
|
|
3
|
+
import { ModelDefinition } from "./type";
|
|
4
|
+
export declare class Tokenizer {
|
|
5
|
+
registry: Registry;
|
|
6
|
+
constructor(registry: Registry);
|
|
7
|
+
getReader(def: ModelDefinition, match?: Exp): (content: string, index: number) => Tuple[];
|
|
8
|
+
findMatch(content: string, index: number, ignoreTypes?: number[]): [number, (c: string, i: number) => Tuple[]] | undefined;
|
|
9
|
+
tryMatch(def: ModelDefinition, content: string, index: number): false | ((content: string, index: number) => Tuple[]);
|
|
10
|
+
tokenize(content: string, options?: {
|
|
11
|
+
ignoreTypes?: number[];
|
|
12
|
+
}): TupleList;
|
|
13
|
+
isRange(data: any): data is TRange;
|
|
14
|
+
isTuple(data: any): data is Tuple;
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Registry } from "./registry";
|
|
2
|
+
export type TokenTree = {
|
|
3
|
+
type: string;
|
|
4
|
+
start: number;
|
|
5
|
+
end: number;
|
|
6
|
+
value: string;
|
|
7
|
+
children?: TokenTree[];
|
|
8
|
+
};
|
|
9
|
+
export declare class TupleList {
|
|
10
|
+
protected registry: Registry;
|
|
11
|
+
protected items: Tuple[];
|
|
12
|
+
constructor(registry: Registry);
|
|
13
|
+
push(...items: Tuple[]): void;
|
|
14
|
+
toArray(): Tuple[];
|
|
15
|
+
toTokenList(content: string): TokenTree[];
|
|
16
|
+
toTokenTree(content: string): TokenTree[];
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@il4mb/css-tokenizer",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Modular, extensible CSS tokenizer for parsing CSS-like strings into tuple lists and token trees.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"css",
|
|
@@ -19,15 +19,21 @@
|
|
|
19
19
|
"author": "il4mb",
|
|
20
20
|
"type": "module",
|
|
21
21
|
"exports": {
|
|
22
|
-
"
|
|
22
|
+
"import": "./dist/index.js",
|
|
23
|
+
"default": "./dist/index.js",
|
|
24
|
+
"types": "./dist/types/index.d.ts"
|
|
23
25
|
},
|
|
24
|
-
"main": "index.
|
|
26
|
+
"main": "./dist/index.js",
|
|
27
|
+
"types": "./dist/types/index.d.ts",
|
|
25
28
|
"scripts": {
|
|
26
|
-
"test": "vitest index.test.ts"
|
|
29
|
+
"test": "vitest index.test.ts",
|
|
30
|
+
"build:types": "npx tsc --build tsconfig.build.json",
|
|
31
|
+
"build": "bun build ./src/index.ts --outdir ./dist --minify --format esm",
|
|
32
|
+
"prepublishOnly": "npm run build"
|
|
27
33
|
},
|
|
28
34
|
"devDependencies": {
|
|
29
35
|
"@types/node": "^26.4.1",
|
|
30
|
-
"typescript": "7.0.2",
|
|
36
|
+
"typescript": "^7.0.2",
|
|
31
37
|
"vitest": "^5.0.0"
|
|
32
38
|
}
|
|
33
39
|
}
|
package/readme.md
CHANGED
|
@@ -13,10 +13,10 @@ npm install @il4mb/css-tokenizer
|
|
|
13
13
|
## Quick usage
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
|
-
import {
|
|
16
|
+
import { tokenizeImpl } from "@il4mb/css-tokenizer";
|
|
17
17
|
|
|
18
18
|
const content = "width: 10px";
|
|
19
|
-
const result =
|
|
19
|
+
const result = tokenizeImpl(content);
|
|
20
20
|
|
|
21
21
|
// Tuple list entries are [type, start, end]
|
|
22
22
|
console.log(result.toArray());
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"declaration": true,
|
|
5
|
+
"emitDeclarationOnly": true,
|
|
6
|
+
"declarationMap": false,
|
|
7
|
+
"declarationDir": "dist/types",
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"outDir": "dist",
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"isolatedModules": false
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["node_modules", "dist"]
|
|
15
|
+
}
|
package/index.test.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { Registry } from "@/registry";
|
|
2
|
-
import { Tokenizer } from "@/tokenizer";
|
|
3
|
-
import { readWhile } from "@/tools";
|
|
4
|
-
|
|
5
|
-
const registry = new Registry();
|
|
6
|
-
registry.add({
|
|
7
|
-
type: "var",
|
|
8
|
-
kind: "class",
|
|
9
|
-
regex: /^--[a-z][a-z0-9-_]+/i,
|
|
10
|
-
priority: 10,
|
|
11
|
-
reader({ index, content }) {
|
|
12
|
-
let nextIndex = readWhile(content, index + 2, /[a-z0-9-_]/);
|
|
13
|
-
return [index, nextIndex];
|
|
14
|
-
},
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
const tokenizer = new Tokenizer(registry);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const content = `--primary`;
|
|
21
|
-
const result = tokenizer.tokenize(content);
|
|
22
|
-
|
|
23
|
-
console.log(result.toArray());
|
|
24
|
-
console.log(JSON.stringify(result.toTokenTree(content), null, 2));
|
package/src/global.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
export declare global {
|
|
2
|
-
|
|
3
|
-
export type Exp = string | RegExp;
|
|
4
|
-
|
|
5
|
-
export interface TokenPlain {
|
|
6
|
-
id: string;
|
|
7
|
-
type: string;
|
|
8
|
-
value: string;
|
|
9
|
-
start: number;
|
|
10
|
-
end: number;
|
|
11
|
-
number?: number;
|
|
12
|
-
unit?: string;
|
|
13
|
-
children?: TokenPlain[]
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export type Tuple = [type: number, start: number, end: number];
|
|
17
|
-
export type TRange = [start: number, end: number];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export { };
|
package/src/index.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/// <reference path="./global.d.ts" />
|
|
2
|
-
|
|
3
|
-
import { Registry } from "./registry";
|
|
4
|
-
import { Tokenizer } from "./tokenizer";
|
|
5
|
-
|
|
6
|
-
export * from "./type";
|
|
7
|
-
|
|
8
|
-
export { Registry } from "./registry";
|
|
9
|
-
export { Tokenizer } from "./tokenizer";
|
|
10
|
-
export * from "./tools";
|
|
11
|
-
export * from "./tupleList";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const tokenizer = new Tokenizer(new Registry());
|
|
15
|
-
export const tokenize = (content: string) => tokenizer.tokenize(content);
|
package/src/registry.ts
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import { findFunctionEnd, readWhile } from "./tools";
|
|
2
|
-
import { ModelDefinition } from "./type";
|
|
3
|
-
|
|
4
|
-
const CSS_NAMED_COLORS = [
|
|
5
|
-
"aliceblue", "antiquewhite", "aqua", "aquamarine", "azure",
|
|
6
|
-
"beige", "bisque", "black", "blanchedalmond", "blue", "blueviolet",
|
|
7
|
-
"brown", "burlywood", "cadetblue", "chartreuse", "chocolate",
|
|
8
|
-
"coral", "cornflowerblue", "cornsilk", "crimson", "cyan",
|
|
9
|
-
"darkblue", "darkcyan", "darkgoldenrod", "darkgray", "darkgrey",
|
|
10
|
-
"darkgreen", "darkkhaki", "darkmagenta", "darkolivegreen",
|
|
11
|
-
"darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen",
|
|
12
|
-
"darkslateblue", "darkslategray", "darkslategrey", "darkturquoise",
|
|
13
|
-
"darkviolet", "deeppink", "deepskyblue", "dimgray", "dimgrey",
|
|
14
|
-
"dodgerblue", "firebrick", "floralwhite", "forestgreen", "fuchsia",
|
|
15
|
-
"gainsboro", "ghostwhite", "gold", "goldenrod", "gray", "grey",
|
|
16
|
-
"green", "greenyellow", "honeydew", "hotpink", "indianred",
|
|
17
|
-
"indigo", "ivory", "khaki", "lavender", "lavenderblush",
|
|
18
|
-
"lawngreen", "lemonchiffon", "lightblue", "lightcoral", "lightcyan",
|
|
19
|
-
"lightgoldenrodyellow", "lightgray", "lightgrey", "lightgreen",
|
|
20
|
-
"lightpink", "lightsalmon", "lightseagreen", "lightskyblue",
|
|
21
|
-
"lightslategray", "lightslategrey", "lightsteelblue", "lightyellow",
|
|
22
|
-
"lime", "limegreen", "linen", "magenta", "maroon",
|
|
23
|
-
"mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple",
|
|
24
|
-
"mediumseagreen", "mediumslateblue", "mediumspringgreen",
|
|
25
|
-
"mediumturquoise", "mediumvioletred", "midnightblue", "mintcream",
|
|
26
|
-
"mistyrose", "moccasin", "navajowhite", "navy", "oldlace",
|
|
27
|
-
"olive", "olivedrab", "orange", "orangered", "orchid",
|
|
28
|
-
"palegoldenrod", "palegreen", "paleturquoise", "palevioletred",
|
|
29
|
-
"papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue",
|
|
30
|
-
"purple", "rebeccapurple", "red", "rosybrown", "royalblue",
|
|
31
|
-
"saddlebrown", "salmon", "sandybrown", "seagreen", "seashell",
|
|
32
|
-
"sienna", "silver", "skyblue", "slateblue", "slategray",
|
|
33
|
-
"slategrey", "snow", "springgreen", "steelblue", "tan", "teal",
|
|
34
|
-
"thistle", "tomato", "turquoise", "violet", "wheat", "white",
|
|
35
|
-
"whitesmoke", "yellow", "yellowgreen", "transparent", "currentcolor",
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
const COLOR_FUNCTIONS = [
|
|
39
|
-
/^rgba?\(/i, /^hsla?\(/i, /^hwb\(/i, /^lab\(/i, /^lch\(/i,
|
|
40
|
-
/^oklab\(/i, /^oklch\(/i, /^color\(/i, /^color-mix\(/i
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
const HEX_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})\b/i;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
export class Registry implements Iterable<ModelDefinition> {
|
|
48
|
-
|
|
49
|
-
private items: ModelDefinition[] = [
|
|
50
|
-
{
|
|
51
|
-
type: "symbol",
|
|
52
|
-
kind: "char",
|
|
53
|
-
regex: /[\s\S]/,
|
|
54
|
-
priority: 0
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
type: "dimension",
|
|
58
|
-
kind: "keyword",
|
|
59
|
-
exp: [/^[+-]?[0-9]+[a-z]+/],
|
|
60
|
-
priority: 110,
|
|
61
|
-
reader({ index, content, deepReader }) {
|
|
62
|
-
let end = index;
|
|
63
|
-
if (content[end] === "-" || content[end] === "+") end++;
|
|
64
|
-
while (end < content.length && /[0-9]/.test(content[end])) end++;
|
|
65
|
-
|
|
66
|
-
if (content[end] === "." && /[0-9]/.test(content[end + 1] ?? "")) {
|
|
67
|
-
end++;
|
|
68
|
-
while (end < content.length && /[0-9]/.test(content[end])) end++;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
while (end < content.length && /[a-zA-Z%]/.test(content[end])) end++;
|
|
72
|
-
return deepReader([index, end]);
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
type: "number",
|
|
77
|
-
kind: "class",
|
|
78
|
-
regex: /^[0-9]/,
|
|
79
|
-
priority: 5,
|
|
80
|
-
reader({ index, content }) {
|
|
81
|
-
let nextIndex = index;
|
|
82
|
-
while (/^[0-9]$/.test(content[nextIndex])) {
|
|
83
|
-
nextIndex++;
|
|
84
|
-
}
|
|
85
|
-
return [index, nextIndex];
|
|
86
|
-
}
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
type: "word",
|
|
90
|
-
kind: "class",
|
|
91
|
-
regex: /^[a-z]/i, // Constrained to single char match
|
|
92
|
-
priority: 5,
|
|
93
|
-
reader({ index, content }) {
|
|
94
|
-
let nextIndex = readWhile(content, index, /[a-z0-9_-]+/);
|
|
95
|
-
return [index, nextIndex];
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
type: "hash",
|
|
100
|
-
kind: "class",
|
|
101
|
-
regex: /^#[a-z0-9]+/i,
|
|
102
|
-
priority: 50,
|
|
103
|
-
reader({ index, content }) {
|
|
104
|
-
let nextIndex = index + 1;
|
|
105
|
-
while (nextIndex < content.length && /^[a-z0-9]+/i.test(content[nextIndex])) {
|
|
106
|
-
nextIndex++
|
|
107
|
-
}
|
|
108
|
-
return [index, nextIndex];
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
type: "color",
|
|
113
|
-
kind: "keyword",
|
|
114
|
-
priority: 120,
|
|
115
|
-
exp: [...CSS_NAMED_COLORS, HEX_COLOR, ...COLOR_FUNCTIONS],
|
|
116
|
-
reader({ index, content, matched, deepReader }) {
|
|
117
|
-
if (matched instanceof RegExp) {
|
|
118
|
-
const match = matched.exec(content.slice(index));
|
|
119
|
-
if (!match) return [index, index + 1];
|
|
120
|
-
const value = match[0];
|
|
121
|
-
if (value.endsWith('(')) {
|
|
122
|
-
const end = findFunctionEnd(content, index);
|
|
123
|
-
const fnName = value.slice(0, -1);
|
|
124
|
-
const innerStart = index + value.length;
|
|
125
|
-
const innerEnd = Math.max(innerStart, end);
|
|
126
|
-
return deepReader([index, end]);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
const nextIndex = readWhile(content, index + 1, /[a-zA-Z0-9_-]/);
|
|
130
|
-
return [index, nextIndex];
|
|
131
|
-
},
|
|
132
|
-
}
|
|
133
|
-
];
|
|
134
|
-
|
|
135
|
-
constructor(models: ModelDefinition[] = []) {
|
|
136
|
-
this.items.push(...models);
|
|
137
|
-
this.sort();
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
add(def: ModelDefinition) {
|
|
141
|
-
this.items.push({ priority: 0, ...def });
|
|
142
|
-
this.sort();
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
get(index: number) {
|
|
146
|
-
return this.items[index];
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
get length() {
|
|
150
|
-
return this.items.length;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
[Symbol.iterator]() {
|
|
154
|
-
return this.items[Symbol.iterator]();
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
indexOf(item: ModelDefinition) {
|
|
158
|
-
return this.items.indexOf(item);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
sort() {
|
|
162
|
-
this.items.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
sortExps(exps: Exp[]): Exp[] {
|
|
166
|
-
return [...exps].sort((a, b) => {
|
|
167
|
-
const aIsRegex = a instanceof RegExp;
|
|
168
|
-
const bIsRegex = b instanceof RegExp;
|
|
169
|
-
if (aIsRegex !== bIsRegex) return aIsRegex ? -1 : 1;
|
|
170
|
-
if (aIsRegex && bIsRegex) return 0;
|
|
171
|
-
return (b as string).length - (a as string).length;
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
}
|
package/src/tokenizer.ts
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { Registry } from "./registry";
|
|
2
|
-
import { TupleList } from "./tupleList";
|
|
3
|
-
import { ModelDefinition } from "./type";
|
|
4
|
-
|
|
5
|
-
export class Tokenizer {
|
|
6
|
-
|
|
7
|
-
constructor(public registry: Registry) { }
|
|
8
|
-
|
|
9
|
-
getReader(def: ModelDefinition, match?: Exp) {
|
|
10
|
-
const type = this.registry.indexOf(def);
|
|
11
|
-
return ((content: string, index: number): Tuple[] => {
|
|
12
|
-
if (def.kind === "char") {
|
|
13
|
-
// char only single character so just + 1 for nextIndex
|
|
14
|
-
return [[type, index, index + 1]];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const deepReader = (range: TRange): Tuple[] => {
|
|
18
|
-
const tuples: Tuple[] = [[type, range[0], range[1]]];
|
|
19
|
-
const rangeContent = content.slice(range[0], range[1]);
|
|
20
|
-
const others = this.tokenize(rangeContent, { ignoreTypes: [type] })
|
|
21
|
-
.toArray()
|
|
22
|
-
.map(([type, start, end]) => [type, start + range[0], end + range[0]] as Tuple);
|
|
23
|
-
|
|
24
|
-
tuples.push(...others);
|
|
25
|
-
return tuples;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (def.kind === "class") {
|
|
29
|
-
if ('reader' in def && def.reader) {
|
|
30
|
-
let ranges = def.reader({ content, index, deepReader });
|
|
31
|
-
if (ranges.length > 0 && Array.isArray(ranges[0])) {
|
|
32
|
-
return ranges.map((range: any) => range.length === 2 ? [type, ...range] : range) as Tuple[];
|
|
33
|
-
}
|
|
34
|
-
return [[type, ...(ranges as TRange)]];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const start = index;
|
|
38
|
-
let nextIndex = start + 1;
|
|
39
|
-
|
|
40
|
-
// Added length check and optional chaining to prevent infinite loops and crashes
|
|
41
|
-
while (nextIndex < content.length) {
|
|
42
|
-
const nextMatch = this.findMatch(content, nextIndex);
|
|
43
|
-
if (nextMatch && type === nextMatch[0]) {
|
|
44
|
-
nextIndex++;
|
|
45
|
-
} else {
|
|
46
|
-
break;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return [[type, start, nextIndex]];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Keyword fallback check (in case reader is undefined)
|
|
54
|
-
if (!def.reader) {
|
|
55
|
-
const length = typeof match === "string" ? match.length : 1;
|
|
56
|
-
return [[type, index, index + length]];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const ranges = def.reader({ content, index, matched: match, deepReader });
|
|
60
|
-
if (ranges.length > 0 && Array.isArray(ranges[0])) {
|
|
61
|
-
return ranges.map((range: any) => range.length === 2 ? [type, ...range] : range) as Tuple[];
|
|
62
|
-
}
|
|
63
|
-
return [[type, ...(ranges as TRange)]];
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
findMatch(content: string, index: number, ignoreTypes?: number[]): [number, (c: string, i: number) => Tuple[]] | undefined {
|
|
68
|
-
for (let i = 0; i < this.registry.length; i++) {
|
|
69
|
-
// Bypass ignored rules so fallback tokens get a chance
|
|
70
|
-
if (ignoreTypes && ignoreTypes.includes(i)) {
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const match = this.tryMatch(this.registry.get(i), content, index);
|
|
75
|
-
if (match) return [i, match];
|
|
76
|
-
}
|
|
77
|
-
return undefined;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
tryMatch(def: ModelDefinition, content: string, index: number) {
|
|
81
|
-
// Guard against out-of-bounds indexing
|
|
82
|
-
if (index >= content.length) return false;
|
|
83
|
-
|
|
84
|
-
if (def.kind === "keyword") {
|
|
85
|
-
const right = content.slice(index);
|
|
86
|
-
const matched = this.registry.sortExps(def.exp).find(exp =>
|
|
87
|
-
exp instanceof RegExp ? exp.test(right) : right.startsWith(exp as string)
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
return matched ? this.getReader(def, matched) : false;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (def.kind === "class") {
|
|
94
|
-
if (!def.regex.test(content.slice(index))) return false;
|
|
95
|
-
return this.getReader(def);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (def.kind === "char") {
|
|
99
|
-
if ('regex' in def) {
|
|
100
|
-
if (!def.regex.test(content.slice(index))) return false;
|
|
101
|
-
return this.getReader(def);
|
|
102
|
-
}
|
|
103
|
-
return def.char === content[index] ? this.getReader(def) : false;
|
|
104
|
-
}
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
tokenize(content: string, options?: { ignoreTypes?: number[] }) {
|
|
109
|
-
const tupleList = new TupleList(this.registry);
|
|
110
|
-
let i = 0;
|
|
111
|
-
|
|
112
|
-
while (i < content.length) {
|
|
113
|
-
let hit = this.findMatch(content, i, options?.ignoreTypes);
|
|
114
|
-
if (hit) {
|
|
115
|
-
const results = hit[1](content, i);
|
|
116
|
-
if (results && results.length > 0) {
|
|
117
|
-
const nextIndex = Math.max(i, ...results.map(tuple => tuple[2]));
|
|
118
|
-
if (nextIndex <= i) {
|
|
119
|
-
tupleList.push([-1, i, i + 1]);
|
|
120
|
-
i++;
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
tupleList.push(...results);
|
|
125
|
-
i = nextIndex;
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
tupleList.push([-1, i, i + 1]);
|
|
131
|
-
i++;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return tupleList;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
isRange(data: any): data is TRange {
|
|
138
|
-
return Array.isArray(data) && data.length === 2 && data.every(t => typeof t === "number");
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
isTuple(data: any): data is Tuple {
|
|
142
|
-
return Array.isArray(data) && data.length === 3 && data.every(t => typeof t === "number");
|
|
143
|
-
}
|
|
144
|
-
}
|
package/src/tools.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
export function findFunctionEnd(source: string, index: number): number {
|
|
2
|
-
let depth = 0;
|
|
3
|
-
let cursor = index;
|
|
4
|
-
let inString = false;
|
|
5
|
-
let stringChar = '';
|
|
6
|
-
|
|
7
|
-
while (cursor < source.length) {
|
|
8
|
-
const char = source[cursor];
|
|
9
|
-
|
|
10
|
-
// Handle strings
|
|
11
|
-
if (!inString && (char === '"' || char === "'")) {
|
|
12
|
-
inString = true;
|
|
13
|
-
stringChar = char;
|
|
14
|
-
} else if (inString && char === stringChar) {
|
|
15
|
-
inString = false;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Only count parentheses outside of strings
|
|
19
|
-
if (!inString) {
|
|
20
|
-
if (char === '(') depth++;
|
|
21
|
-
else if (char === ')') {
|
|
22
|
-
depth--;
|
|
23
|
-
if (depth === 0) return cursor + 1;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
cursor++;
|
|
28
|
-
}
|
|
29
|
-
return source.length;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
export const readWhile = (content: string, index: number, test: RegExp) => {
|
|
34
|
-
let nextIndex = index;
|
|
35
|
-
while (nextIndex < content.length && test.test(content[nextIndex])) nextIndex++;
|
|
36
|
-
return nextIndex;
|
|
37
|
-
}
|
package/src/tupleList.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { Registry } from "./registry";
|
|
2
|
-
|
|
3
|
-
export type TokenTree = {
|
|
4
|
-
type: string;
|
|
5
|
-
start: number;
|
|
6
|
-
end: number
|
|
7
|
-
value: string;
|
|
8
|
-
children?: TokenTree[]
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export class TupleList {
|
|
12
|
-
protected items: Tuple[] = [];
|
|
13
|
-
|
|
14
|
-
constructor(protected registry: Registry) { }
|
|
15
|
-
|
|
16
|
-
push(...items: Tuple[]) {
|
|
17
|
-
items.forEach(item => {
|
|
18
|
-
this.items.push(item);
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
toArray() {
|
|
23
|
-
return [...this.items];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
toTokenList(content: string): TokenTree[] {
|
|
27
|
-
return this.items.map(([type, start, end]) => {
|
|
28
|
-
const model = this.registry.get(type);
|
|
29
|
-
return {
|
|
30
|
-
type: model?.type ?? "unknown",
|
|
31
|
-
start,
|
|
32
|
-
end,
|
|
33
|
-
value: content.slice(start, end),
|
|
34
|
-
};
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
toTokenTree(content: string): TokenTree[] {
|
|
39
|
-
const tokens = this.toTokenList(content);
|
|
40
|
-
|
|
41
|
-
tokens.sort((a, b) => {
|
|
42
|
-
if (a.start !== b.start) {
|
|
43
|
-
return a.start - b.start;
|
|
44
|
-
}
|
|
45
|
-
return b.end - a.end;
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const roots: TokenTree[] = [];
|
|
49
|
-
const stack: TokenTree[] = [];
|
|
50
|
-
|
|
51
|
-
for (const token of tokens) {
|
|
52
|
-
/*
|
|
53
|
-
* FIXED: Robust parent exit check.
|
|
54
|
-
* Pop parents whose physical range strictly ends before or exactly where the new token starts.
|
|
55
|
-
* (e.g. `parent.end <= token.start`)
|
|
56
|
-
*/
|
|
57
|
-
while (stack.length > 0 && stack[stack.length - 1].end <= token.start) {
|
|
58
|
-
stack.pop();
|
|
59
|
-
}
|
|
60
|
-
const parent = stack[stack.length - 1];
|
|
61
|
-
|
|
62
|
-
if (parent) {
|
|
63
|
-
if (!parent.children) parent.children = [];
|
|
64
|
-
parent.children.push(token);
|
|
65
|
-
} else {
|
|
66
|
-
roots.push(token);
|
|
67
|
-
}
|
|
68
|
-
stack.push(token);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return roots;
|
|
72
|
-
}
|
|
73
|
-
}
|
package/src/type.d.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
export type ReaderContext = {
|
|
2
|
-
content: string;
|
|
3
|
-
index: number;
|
|
4
|
-
deepReader: (ranges: TRange) => Tuple[]
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export interface CharModel {
|
|
10
|
-
kind: "char";
|
|
11
|
-
char: string;
|
|
12
|
-
}
|
|
13
|
-
export interface CharRegexModel {
|
|
14
|
-
kind: "char";
|
|
15
|
-
regex: RegExp;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface KeywordModel {
|
|
19
|
-
kind: "keyword";
|
|
20
|
-
exp: Exp[];
|
|
21
|
-
reader: (ctx: ReaderContext & { matched: Exp }) => TRange[] | TRange | Tuple[];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface ClassModel {
|
|
25
|
-
kind: "class";
|
|
26
|
-
regex: RegExp;
|
|
27
|
-
reader?: (ctx: ReaderContext) => TRange[] | TRange | Tuple[];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export type Model = CharModel | CharRegexModel | KeywordModel | ClassModel;
|
|
31
|
-
|
|
32
|
-
export type ModelDefinition<T extends Model = Model> = {
|
|
33
|
-
type: string;
|
|
34
|
-
priority?: number;
|
|
35
|
-
} & T;
|