First commit.

Signed-off-by: Chen Xiao <abigwc@gmail.com>
This commit is contained in:
Chen Xiao
2026-05-08 14:43:16 +08:00
commit 0b64e2de94
10989 changed files with 2253791 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
import { Color, RGBColor } from "./types.js";
/**
* String to color
*/
declare function stringToColor(value: string): Color | null;
/**
* Check if colors are identical
*/
declare function compareColors(color1: Color, color2: Color): boolean;
/**
* Color to hex
*/
declare function colorToHexString(color: RGBColor, canCompact?: boolean): string | null;
/**
* Convert color to string
*/
declare function colorToString(color: Color): string;
export { colorToHexString, colorToString, compareColors, stringToColor };
+290
View File
@@ -0,0 +1,290 @@
import { colorKeywords } from "./keywords.js";
/**
* Convert RGB to HSL
*/
function rgb2hsl(rgb) {
const c1 = rgb.r / 255, c2 = rgb.g / 255, c3 = rgb.b / 255, kmin = Math.min(c1, Math.min(c2, c3)), kmax = Math.max(c1, Math.max(c2, c3)), l = (kmax + kmin) / 2;
let s, h, delta;
if (kmax === kmin) s = h = 0;
else {
if (l < .5) s = (kmax - kmin) / (kmax + kmin);
else s = (kmax - kmin) / (2 - kmax - kmin);
delta = kmax - kmin;
if (kmax === c1) h = (c2 - c3) / delta;
else if (kmax === c2) h = 2 + (c3 - c1) / delta;
else h = 4 + (c1 - c2) / delta;
h = h * 60;
if (h < 0) h += 360;
}
return {
type: "hsl",
h,
s: s * 100,
l: l * 100,
alpha: rgb.alpha
};
}
/**
* Color from function
*/
function fromFunction(value) {
if (value.slice(-1) !== ")") return null;
const parts = value.slice(0, value.length - 1).split("(");
if (parts.length !== 2) return null;
const func = parts[0].trim();
const content = parts[1].trim();
let values;
let alphaStr;
switch (func) {
case "lch":
case "lab": {
const parts = content.split("/");
switch (parts.length) {
case 2:
alphaStr = parts[1].trim();
break;
case 1: break;
default: return null;
}
values = parts[0].trim().split(/[\s,]+/);
break;
}
case "rgb":
case "rgba":
case "hsl":
case "hsla":
values = content.trim().split(/[\s,]+/);
if (values.length === 4) alphaStr = values.pop().trim();
break;
default: return {
type: "function",
func,
value: content
};
}
let alpha = 1;
if (typeof alphaStr === "string") {
alpha = parseFloat(alphaStr);
const index = alphaStr.indexOf("%");
const hasPercentage = index !== -1;
if (isNaN(alpha) || hasPercentage && index !== alphaStr.length - 1) return null;
if (hasPercentage) alpha /= 100;
}
if (alpha < 0 || alpha > 1 || values.length !== 3) return null;
if (alpha === 0) return { type: "transparent" };
const isPercentage = [];
const numbers = [];
for (let i = 0; i < 3; i++) {
const colorStr = values[i];
const index = colorStr.indexOf("%");
const hasPercentage = index !== -1;
if (hasPercentage && index !== colorStr.length - 1) return null;
const colorNum = parseFloat(colorStr);
if (isNaN(colorNum)) return null;
isPercentage.push(hasPercentage);
numbers.push(colorNum);
}
switch (func) {
case "rgb":
case "rgba": {
const hasPercengage = isPercentage[0];
if (hasPercengage !== isPercentage[1] || hasPercengage !== isPercentage[2]) return null;
let r = numbers[0];
let g = numbers[1];
let b = numbers[2];
if (hasPercengage) {
r = r * 255 / 100;
g = g * 255 / 100;
b = b * 255 / 100;
}
return {
type: "rgb",
r,
g,
b,
alpha
};
}
case "hsl":
case "hsla":
if (isPercentage[0] || !isPercentage[1] || !isPercentage[2]) return null;
return {
type: "hsl",
h: numbers[0],
s: numbers[1],
l: numbers[2],
alpha
};
case "lab":
case "lch":
if (!isPercentage[0] || isPercentage[1] || isPercentage[2]) return null;
return func === "lab" ? {
type: "lab",
l: numbers[0],
a: numbers[1],
b: numbers[2],
alpha
} : {
type: "lch",
l: numbers[0],
c: numbers[1],
h: numbers[2],
alpha
};
}
return null;
}
/**
* From hexadecimal
*/
function fromHex(value) {
if (value.slice(0, 1) === "#") value = value.slice(1);
if (!/^[\da-f]+$/i.test(value)) return null;
let alpha = 1;
const hex = [
"",
"",
""
];
switch (value.length) {
case 4: alpha = parseInt(value[3] + value[3], 16) / 255;
case 3:
hex[0] = value[0] + value[0];
hex[1] = value[1] + value[1];
hex[2] = value[2] + value[2];
break;
case 8: alpha = parseInt(value[6] + value[7], 16) / 255;
case 6:
hex[0] = value[0] + value[1];
hex[1] = value[2] + value[3];
hex[2] = value[4] + value[5];
break;
default: return null;
}
return alpha === 0 ? { type: "transparent" } : {
type: "rgb",
r: parseInt(hex[0], 16),
g: parseInt(hex[1], 16),
b: parseInt(hex[2], 16),
alpha
};
}
/**
* String to color
*/
function stringToColor(value) {
value = value.toLowerCase().trim();
if (colorKeywords[value]) return { ...colorKeywords[value] };
if (value.indexOf("(") !== -1) return fromFunction(value);
return fromHex(value);
}
/**
* Check if colors are identical
*/
function compareColors(color1, color2) {
if (color1.type === color2.type) {
let testKeys = new Set(Object.keys(color1));
switch (color1.type) {
case "hsl":
if (color1.s === 0) testKeys.delete("h");
if (color1.l === 0 || color1.l === 100) {
testKeys.delete("h");
testKeys.delete("s");
}
case "rgb": if (color1.alpha === 0) testKeys = new Set(["a"]);
}
const keys = Array.from(testKeys);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (color1[key] !== color2[key]) return false;
}
return true;
}
const list = [color1, color2].sort((a, b) => a.type.localeCompare(b.type));
const item1 = list[0];
const item2 = list[1];
switch (item1.type) {
case "hsl":
switch (item2.type) {
case "rgb": return compareColors(item1, rgb2hsl(item2));
case "transparent": return item1.alpha === 0;
}
return false;
case "rgb": switch (item2.type) {
case "transparent": return item1.alpha === 0;
}
}
return false;
}
/**
* Color to hex
*/
function colorToHexString(color, canCompact = true) {
if (color.alpha !== 1) return null;
let result = "";
const attrs = [
"r",
"g",
"b"
];
for (let i = 0; i < attrs.length; i++) {
const value = color[attrs[i]];
if (Math.round(value) !== value) return null;
const hex = value.toString(16);
result += (value < 16 ? "0" : "") + hex;
}
if (result.length !== 6) return null;
if (canCompact && result[0] === result[1] && result[2] === result[3] && result[4] === result[5]) result = result[0] + result[2] + result[4];
return "#" + result;
}
/**
* Convert color to string
*/
function colorToString(color) {
if (color.alpha === 0) return "transparent";
switch (color.type) {
case "none":
case "transparent": return color.type;
case "current": return "currentColor";
case "rgb": {
const hex = colorToHexString(color);
if (hex !== null) return hex;
const list = [
color.r,
color.g,
color.b
];
if (color.alpha !== 1) list.push(color.alpha);
return "rgb" + (list.length === 4 ? "a(" : "(") + list.join(", ") + ")";
}
case "hsl": {
const list = [
color.h,
color.s.toString() + "%",
color.l.toString() + "%"
];
if (color.alpha !== 1) list.push(color.alpha);
return "hsl" + (list.length === 4 ? "a(" : "(") + list.join(", ") + ")";
}
case "lab": {
const list = [
color.l.toString() + "%",
color.a,
color.b
];
if (color.alpha !== 1) list.push("/ " + color.alpha.toString());
return "lab(" + list.join(" ") + ")";
}
case "lch": {
const list = [
color.l.toString() + "%",
color.c,
color.h
];
if (color.alpha !== 1) list.push("/ " + color.alpha.toString());
return "lch(" + list.join(" ") + ")";
}
case "function": return color.func + "(" + color.value + ")";
}
}
export { colorToHexString, colorToString, compareColors, stringToColor };
+6
View File
@@ -0,0 +1,6 @@
import { Color } from "./types.js";
/**
* Color keywords
*/
declare const colorKeywords: Record<string, Color>;
export { colorKeywords };
+701
View File
@@ -0,0 +1,701 @@
/**
* Color keywords
*/
const colorKeywords = {
transparent: { type: "transparent" },
none: { type: "none" },
currentcolor: { type: "current" }
};
/**
* Add color
*/
function add(keyword, colors) {
const type = "rgb";
const r = colors[0];
const length = colors.length;
colorKeywords[keyword] = {
type,
r,
g: length > 1 ? colors[1] : r,
b: length > 2 ? colors[2] : r,
alpha: length > 3 ? colors[3] : 1
};
}
/**
* List of base colors. From https://www.w3.org/TR/css3-color/
*/
add("silver", [192]);
add("gray", [128]);
add("white", [255]);
add("maroon", [
128,
0,
0
]);
add("red", [
255,
0,
0
]);
add("purple", [128, 0]);
add("fuchsia", [255, 0]);
add("green", [0, 128]);
add("lime", [0, 255]);
add("olive", [
128,
128,
0
]);
add("yellow", [
255,
255,
0
]);
add("navy", [
0,
0,
128
]);
add("blue", [
0,
0,
255
]);
add("teal", [
0,
128,
128
]);
add("aqua", [
0,
255,
255
]);
/**
* List of extended colors. From https://drafts.csswg.org/css-color/
*/
add("aliceblue", [
240,
248,
255
]);
add("antiquewhite", [
250,
235,
215
]);
add("aqua", [
0,
255,
255
]);
add("aquamarine", [
127,
255,
212
]);
add("azure", [
240,
255,
255
]);
add("beige", [
245,
245,
220
]);
add("bisque", [
255,
228,
196
]);
add("black", [0]);
add("blanchedalmond", [
255,
235,
205
]);
add("blue", [
0,
0,
255
]);
add("blueviolet", [
138,
43,
226
]);
add("brown", [
165,
42,
42
]);
add("burlywood", [
222,
184,
135
]);
add("cadetblue", [
95,
158,
160
]);
add("chartreuse", [
127,
255,
0
]);
add("chocolate", [
210,
105,
30
]);
add("coral", [
255,
127,
80
]);
add("cornflowerblue", [
100,
149,
237
]);
add("cornsilk", [
255,
248,
220
]);
add("crimson", [
220,
20,
60
]);
add("cyan", [
0,
255,
255
]);
add("darkblue", [
0,
0,
139
]);
add("darkcyan", [
0,
139,
139
]);
add("darkgoldenrod", [
184,
134,
11
]);
add("darkgray", [169]);
add("darkgreen", [0, 100]);
add("darkgrey", [169]);
add("darkkhaki", [
189,
183,
107
]);
add("darkmagenta", [139, 0]);
add("darkolivegreen", [
85,
107,
47
]);
add("darkorange", [
255,
140,
0
]);
add("darkorchid", [
153,
50,
204
]);
add("darkred", [
139,
0,
0
]);
add("darksalmon", [
233,
150,
122
]);
add("darkseagreen", [143, 188]);
add("darkslateblue", [
72,
61,
139
]);
add("darkslategray", [
47,
79,
79
]);
add("darkslategrey", [
47,
79,
79
]);
add("darkturquoise", [
0,
206,
209
]);
add("darkviolet", [
148,
0,
211
]);
add("deeppink", [
255,
20,
147
]);
add("deepskyblue", [
0,
191,
255
]);
add("dimgray", [105]);
add("dimgrey", [105]);
add("dodgerblue", [
30,
144,
255
]);
add("firebrick", [
178,
34,
34
]);
add("floralwhite", [
255,
250,
240
]);
add("forestgreen", [34, 139]);
add("fuchsia", [255, 0]);
add("gainsboro", [220]);
add("ghostwhite", [
248,
248,
255
]);
add("gold", [
255,
215,
0
]);
add("goldenrod", [
218,
165,
32
]);
add("gray", [128]);
add("green", [0, 128]);
add("greenyellow", [
173,
255,
47
]);
add("grey", [128]);
add("honeydew", [240, 255]);
add("hotpink", [
255,
105,
180
]);
add("indianred", [
205,
92,
92
]);
add("indigo", [
75,
0,
130
]);
add("ivory", [
255,
255,
240
]);
add("khaki", [
240,
230,
140
]);
add("lavender", [
230,
230,
250
]);
add("lavenderblush", [
255,
240,
245
]);
add("lawngreen", [
124,
252,
0
]);
add("lemonchiffon", [
255,
250,
205
]);
add("lightblue", [
173,
216,
230
]);
add("lightcoral", [
240,
128,
128
]);
add("lightcyan", [
224,
255,
255
]);
add("lightgoldenrodyellow", [
250,
250,
210
]);
add("lightgray", [211]);
add("lightgreen", [144, 238]);
add("lightgrey", [211]);
add("lightpink", [
255,
182,
193
]);
add("lightsalmon", [
255,
160,
122
]);
add("lightseagreen", [
32,
178,
170
]);
add("lightskyblue", [
135,
206,
250
]);
add("lightslategray", [
119,
136,
153
]);
add("lightslategrey", [
119,
136,
153
]);
add("lightsteelblue", [
176,
196,
222
]);
add("lightyellow", [
255,
255,
224
]);
add("lime", [0, 255]);
add("limegreen", [50, 205]);
add("linen", [
250,
240,
230
]);
add("magenta", [255, 0]);
add("maroon", [
128,
0,
0
]);
add("mediumaquamarine", [
102,
205,
170
]);
add("mediumblue", [
0,
0,
205
]);
add("mediumorchid", [
186,
85,
211
]);
add("mediumpurple", [
147,
112,
219
]);
add("mediumseagreen", [
60,
179,
113
]);
add("mediumslateblue", [
123,
104,
238
]);
add("mediumspringgreen", [
0,
250,
154
]);
add("mediumturquoise", [
72,
209,
204
]);
add("mediumvioletred", [
199,
21,
133
]);
add("midnightblue", [
25,
25,
112
]);
add("mintcream", [
245,
255,
250
]);
add("mistyrose", [
255,
228,
225
]);
add("moccasin", [
255,
228,
181
]);
add("navajowhite", [
255,
222,
173
]);
add("navy", [
0,
0,
128
]);
add("oldlace", [
253,
245,
230
]);
add("olive", [
128,
128,
0
]);
add("olivedrab", [
107,
142,
35
]);
add("orange", [
255,
165,
0
]);
add("orangered", [
255,
69,
0
]);
add("orchid", [
218,
112,
214
]);
add("palegoldenrod", [
238,
232,
170
]);
add("palegreen", [152, 251]);
add("paleturquoise", [
175,
238,
238
]);
add("palevioletred", [
219,
112,
147
]);
add("papayawhip", [
255,
239,
213
]);
add("peachpuff", [
255,
218,
185
]);
add("peru", [
205,
133,
63
]);
add("pink", [
255,
192,
203
]);
add("plum", [221, 160]);
add("powderblue", [
176,
224,
230
]);
add("purple", [128, 0]);
add("rebeccapurple", [
102,
51,
153
]);
add("red", [
255,
0,
0
]);
add("rosybrown", [
188,
143,
143
]);
add("royalblue", [
65,
105,
225
]);
add("saddlebrown", [
139,
69,
19
]);
add("salmon", [
250,
128,
114
]);
add("sandybrown", [
244,
164,
96
]);
add("seagreen", [
46,
139,
87
]);
add("seashell", [
255,
245,
238
]);
add("sienna", [
160,
82,
45
]);
add("silver", [192]);
add("skyblue", [
135,
206,
235
]);
add("slateblue", [
106,
90,
205
]);
add("slategray", [
112,
128,
144
]);
add("slategrey", [
112,
128,
144
]);
add("snow", [
255,
250,
250
]);
add("springgreen", [
0,
255,
127
]);
add("steelblue", [
70,
130,
180
]);
add("tan", [
210,
180,
140
]);
add("teal", [
0,
128,
128
]);
add("thistle", [216, 191]);
add("tomato", [
255,
99,
71
]);
add("turquoise", [
64,
224,
208
]);
add("violet", [238, 130]);
add("wheat", [
245,
222,
179
]);
add("white", [255]);
add("whitesmoke", [245]);
add("yellow", [
255,
255,
0
]);
add("yellowgreen", [
154,
205,
50
]);
export { colorKeywords };
+44
View File
@@ -0,0 +1,44 @@
interface RGBColor {
type: 'rgb';
r: number;
g: number;
b: number;
alpha: number;
}
interface HSLColor {
type: 'hsl';
h: number;
s: number;
l: number;
alpha: number;
}
interface LABColor {
type: 'lab';
l: number;
a: number;
b: number;
alpha: number;
}
interface LCHColor {
type: 'lch';
l: number;
c: number;
h: number;
alpha: number;
}
interface FunctionColor {
type: 'function';
func: string;
value: string;
}
interface TransparentColor {
type: 'transparent';
}
interface NoColor {
type: 'none';
}
interface CurrentColor {
type: 'current';
}
type Color = RGBColor | HSLColor | LABColor | LCHColor | FunctionColor | TransparentColor | NoColor | CurrentColor;
export { Color, CurrentColor, FunctionColor, HSLColor, LABColor, LCHColor, NoColor, RGBColor, TransparentColor };
+1
View File
@@ -0,0 +1 @@
export {};
+17
View File
@@ -0,0 +1,17 @@
import { IconCSSCommonCodeOptions, IconCSSItemOptions, IconContentItemOptions } from "./types.js";
import { IconifyIcon } from "@iconify/types";
/**
* Generates common CSS rules for multiple icons, rendered as background/mask
*/
declare function getCommonCSSRules(options: IconCSSCommonCodeOptions): Record<string, string>;
/**
* Generate CSS rules for one icon, rendered as background/mask
*
* This function excludes common rules
*/
declare function generateItemCSSRules(icon: Required<IconifyIcon>, options: IconCSSItemOptions): Record<string, string>;
/**
* Generate content for one icon, rendered as content of pseudo-selector
*/
declare function generateItemContent(icon: Required<IconifyIcon>, options: IconContentItemOptions): string;
export { generateItemCSSRules, generateItemContent, getCommonCSSRules };
+74
View File
@@ -0,0 +1,74 @@
import { makeViewBoxSquare } from "../icon/square.js";
import { calculateSize } from "../svg/size.js";
import { iconToSVG } from "../svg/build.js";
import { svgToURL } from "../svg/url.js";
import { iconToHTML } from "../svg/html.js";
/**
* Generates common CSS rules for multiple icons, rendered as background/mask
*/
function getCommonCSSRules(options) {
const result = {
display: "inline-block",
width: "1em",
height: "1em"
};
const varName = options.varName;
if (options.pseudoSelector) result["content"] = "''";
switch (options.mode) {
case "background":
if (varName) result["background-image"] = "var(--" + varName + ")";
result["background-repeat"] = "no-repeat";
result["background-size"] = "100% 100%";
break;
case "mask":
result["background-color"] = "currentColor";
if (varName) result["mask-image"] = result["-webkit-mask-image"] = "var(--" + varName + ")";
result["mask-repeat"] = result["-webkit-mask-repeat"] = "no-repeat";
result["mask-size"] = result["-webkit-mask-size"] = "100% 100%";
break;
}
return result;
}
/**
* Generate CSS rules for one icon, rendered as background/mask
*
* This function excludes common rules
*/
function generateItemCSSRules(icon, options) {
const result = {};
const varName = options.varName;
const buildResult = iconToSVG(icon);
let viewBox = buildResult.viewBox;
if (viewBox[2] !== viewBox[3]) if (options.forceSquare) viewBox = makeViewBoxSquare(viewBox);
else result["width"] = calculateSize("1em", viewBox[2] / viewBox[3]);
const url = svgToURL(iconToHTML(buildResult.body.replace(/currentColor/g, options.color || "black"), {
viewBox: `${viewBox[0]} ${viewBox[1]} ${viewBox[2]} ${viewBox[3]}`,
width: `${viewBox[2]}`,
height: `${viewBox[3]}`
}));
if (varName) result["--" + varName] = url;
else switch (options.mode) {
case "background":
result["background-image"] = url;
break;
case "mask":
result["mask-image"] = result["-webkit-mask-image"] = url;
break;
}
return result;
}
/**
* Generate content for one icon, rendered as content of pseudo-selector
*/
function generateItemContent(icon, options) {
const buildResult = iconToSVG(icon);
const viewBox = buildResult.viewBox;
const height = options.height;
const width = options.width ?? calculateSize(height, viewBox[2] / viewBox[3]);
return svgToURL(iconToHTML(buildResult.body.replace(/currentColor/g, options.color || "black"), {
viewBox: `${viewBox[0]} ${viewBox[1]} ${viewBox[2]} ${viewBox[3]}`,
width: width.toString(),
height: height.toString()
}));
}
export { generateItemCSSRules, generateItemContent, getCommonCSSRules };
+8
View File
@@ -0,0 +1,8 @@
import { CSSFormatMode, CSSUnformattedItem } from "./types.js";
/**
* Format data
*
* Key is selector, value is list of rules
*/
declare function formatCSS(data: CSSUnformattedItem[], mode?: CSSFormatMode): string;
export { formatCSS };
+39
View File
@@ -0,0 +1,39 @@
const format = {
selectorStart: {
compressed: "{",
compact: " {",
expanded: " {"
},
selectorEnd: {
compressed: "}",
compact: "; }\n",
expanded: ";\n}\n"
},
rule: {
compressed: "{key}:",
compact: " {key}: ",
expanded: "\n {key}: "
}
};
/**
* Format data
*
* Key is selector, value is list of rules
*/
function formatCSS(data, mode = "expanded") {
const results = [];
for (let i = 0; i < data.length; i++) {
const { selector, rules } = data[i];
let entry = (selector instanceof Array ? selector.join(mode === "compressed" ? "," : ", ") : selector) + format.selectorStart[mode];
let firstRule = true;
for (const key in rules) {
if (!firstRule) entry += ";";
entry += format.rule[mode].replace("{key}", key) + rules[key];
firstRule = false;
}
entry += format.selectorEnd[mode];
results.push(entry);
}
return results.join(mode === "compressed" ? "" : "\n");
}
export { formatCSS };
+11
View File
@@ -0,0 +1,11 @@
import { IconCSSIconOptions, IconContentIconOptions } from "./types.js";
import { IconifyIcon } from "@iconify/types";
/**
* Get CSS for icon, rendered as background or mask
*/
declare function getIconCSS(icon: IconifyIcon, options?: IconCSSIconOptions): string;
/**
* Get CSS for icon, rendered as content
*/
declare function getIconContentCSS(icon: IconifyIcon, options: IconContentIconOptions): string;
export { getIconCSS, getIconContentCSS };
+50
View File
@@ -0,0 +1,50 @@
import { defaultIconProps } from "../icon/defaults.js";
import { generateItemCSSRules, generateItemContent, getCommonCSSRules } from "./common.js";
import { formatCSS } from "./format.js";
/**
* Get CSS for icon, rendered as background or mask
*/
function getIconCSS(icon, options = {}) {
const body = options.customise ? options.customise(icon.body) : icon.body;
const mode = options.mode || (options.color || !body.includes("currentColor") ? "background" : "mask");
let varName = options.varName;
if (varName === void 0 && mode === "mask") varName = "svg";
const newOptions = {
...options,
mode,
varName
};
if (mode === "background") delete newOptions.varName;
const rules = {
...options.rules,
...getCommonCSSRules(newOptions),
...generateItemCSSRules({
...defaultIconProps,
...icon,
body
}, newOptions)
};
return formatCSS([{
selector: options.iconSelector || ".icon",
rules
}], newOptions.format);
}
/**
* Get CSS for icon, rendered as content
*/
function getIconContentCSS(icon, options) {
const body = options.customise ? options.customise(icon.body) : icon.body;
const content = generateItemContent({
...defaultIconProps,
...icon,
body
}, options);
return formatCSS([{
selector: options.iconSelector || ".icon::after",
rules: {
...options.rules,
content
}
}], options.format);
}
export { getIconCSS, getIconContentCSS };
+20
View File
@@ -0,0 +1,20 @@
import { CSSUnformattedItem, IconCSSIconSetOptions, IconContentIconSetOptions } from "./types.js";
import { IconifyJSON } from "@iconify/types";
interface CSSData {
common?: CSSUnformattedItem;
css: CSSUnformattedItem[];
errors: string[];
}
/**
* Get data for getIconsCSS()
*/
declare function getIconsCSSData(iconSet: IconifyJSON, names: string[], options?: IconCSSIconSetOptions): CSSData;
/**
* Get CSS for icons as background/mask
*/
declare function getIconsCSS(iconSet: IconifyJSON, names: string[], options?: IconCSSIconSetOptions): string;
/**
* Get CSS for icons as content
*/
declare function getIconsContentCSS(iconSet: IconifyJSON, names: string[], options: IconContentIconSetOptions): string;
export { getIconsCSS, getIconsCSSData, getIconsContentCSS };
+131
View File
@@ -0,0 +1,131 @@
import { defaultIconProps } from "../icon/defaults.js";
import { getIconData } from "../icon-set/get-icon.js";
import { generateItemCSSRules, generateItemContent, getCommonCSSRules } from "./common.js";
import { formatCSS } from "./format.js";
const commonSelector = ".icon--{prefix}";
const iconSelector = ".icon--{prefix}--{name}";
const contentSelector = ".icon--{prefix}--{name}::after";
const defaultSelectors = {
commonSelector,
iconSelector,
overrideSelector: commonSelector + iconSelector
};
/**
* Get data for getIconsCSS()
*/
function getIconsCSSData(iconSet, names, options = {}) {
const css = [];
const errors = [];
const palette = options.color ? true : void 0;
let mode = options.mode || typeof palette === "boolean" && (palette ? "background" : "mask");
if (!mode) {
for (let i = 0; i < names.length; i++) {
const name = names[i];
const icon = getIconData(iconSet, name);
if (icon) {
mode = (options.customise ? options.customise(icon.body, name) : icon.body).includes("currentColor") ? "mask" : "background";
break;
}
}
if (!mode) {
mode = "mask";
errors.push("/* cannot detect icon mode: not set in options and icon set is missing info, rendering as " + mode + " */");
}
}
let varName = options.varName;
if (varName === void 0 && mode === "mask") varName = "svg";
const newOptions = {
...options,
mode,
varName
};
const { commonSelector, iconSelector, overrideSelector } = newOptions.iconSelector ? newOptions : defaultSelectors;
const iconSelectorWithPrefix = iconSelector.replace(/{prefix}/g, iconSet.prefix);
const commonRules = {
...options.rules,
...getCommonCSSRules(newOptions)
};
const hasCommonRules = commonSelector && commonSelector !== iconSelector;
const commonSelectors = /* @__PURE__ */ new Set();
if (hasCommonRules) css.push({
selector: commonSelector.replace(/{prefix}/g, iconSet.prefix),
rules: commonRules
});
for (let i = 0; i < names.length; i++) {
const name = names[i];
const iconData = getIconData(iconSet, name);
if (!iconData) {
errors.push("/* Could not find icon: " + name + " */");
continue;
}
const body = options.customise ? options.customise(iconData.body, name) : iconData.body;
const rules = generateItemCSSRules({
...defaultIconProps,
...iconData,
body
}, newOptions);
let requiresOverride = false;
if (hasCommonRules && overrideSelector) {
for (const key in rules) if (key in commonRules) requiresOverride = true;
}
const selector = (requiresOverride && overrideSelector ? overrideSelector.replace(/{prefix}/g, iconSet.prefix) : iconSelectorWithPrefix).replace(/{name}/g, name);
css.push({
selector,
rules
});
if (!hasCommonRules) commonSelectors.add(selector);
}
const result = {
css,
errors
};
if (!hasCommonRules && commonSelectors.size) result.common = {
selector: Array.from(commonSelectors).join(newOptions.format === "compressed" ? "," : ", "),
rules: commonRules
};
return result;
}
/**
* Get CSS for icons as background/mask
*/
function getIconsCSS(iconSet, names, options = {}) {
const { css, errors, common } = getIconsCSSData(iconSet, names, options);
if (common) if (css.length === 1 && css[0].selector === common.selector) css[0].rules = {
...common.rules,
...css[0].rules
};
else css.unshift(common);
return formatCSS(css, options.format) + (errors.length ? "\n" + errors.join("\n") + "\n" : "");
}
/**
* Get CSS for icons as content
*/
function getIconsContentCSS(iconSet, names, options) {
const errors = [];
const css = [];
const iconSelectorWithPrefix = (options.iconSelector ?? contentSelector).replace(/{prefix}/g, iconSet.prefix);
for (let i = 0; i < names.length; i++) {
const name = names[i];
const iconData = getIconData(iconSet, name);
if (!iconData) {
errors.push("/* Could not find icon: " + name + " */");
continue;
}
const body = options.customise ? options.customise(iconData.body, name) : iconData.body;
const content = generateItemContent({
...defaultIconProps,
...iconData,
body
}, options);
const selector = iconSelectorWithPrefix.replace(/{name}/g, name);
css.push({
selector,
rules: {
...options.rules,
content
}
});
}
return formatCSS(css, options.format) + (errors.length ? "\n" + errors.join("\n") + "\n" : "");
}
export { getIconsCSS, getIconsCSSData, getIconsContentCSS };
+104
View File
@@ -0,0 +1,104 @@
/**
* Icon mode
*/
type IconCSSMode = 'mask' | 'background';
/**
* Selector for icon
*/
interface IconCSSIconSelectorOptions {
pseudoSelector?: boolean;
iconSelector?: string;
}
/**
* Selector for icon when generating data from icon set
*/
interface IconCSSSelectorOptions extends IconCSSIconSelectorOptions {
commonSelector?: string;
overrideSelector?: string;
}
/**
* Options common for both multiple icons and single icon
*/
interface IconCSSSharedOptions {
varName?: string | null;
forceSquare?: boolean;
color?: string;
rules?: Record<string, string>;
}
/**
* Mode
*/
interface IconCSSModeOptions {
mode?: IconCSSMode;
}
/**
* Options for generating common code
*
* Requires mode
*/
interface IconCSSCommonCodeOptions extends IconCSSSharedOptions, IconCSSIconSelectorOptions, Required<IconCSSModeOptions> {}
/**
* Options for generating data for one icon
*/
interface IconCSSItemOptions extends IconCSSSharedOptions, Required<IconCSSModeOptions> {}
/**
* Selector for icon
*/
interface IconContentIconSelectorOptions {
iconSelector?: string;
}
/**
* Options common for both multiple icons and single icon
*/
interface IconContentSharedOptions {
height: number;
width?: number;
color?: string;
rules?: Record<string, string>;
}
/**
* Options for generating data for one icon
*/
type IconContentItemOptions = IconContentSharedOptions;
/**
* Formatting modes. Same as in SASS
*/
type CSSFormatMode = 'expanded' | 'compact' | 'compressed';
/**
* Item to format
*/
interface CSSUnformattedItem {
selector: string | string[];
rules: Record<string, string>;
}
/**
* Formatting options
*/
interface IconCSSFormatOptions {
format?: CSSFormatMode;
}
/**
* Options for generating data for one icon as background/mask
*/
interface IconCSSIconOptions extends IconCSSSharedOptions, IconCSSIconSelectorOptions, IconCSSModeOptions, IconCSSFormatOptions {
customise?: (content: string) => string;
}
/**
* Options for generating data for one icon as content
*/
interface IconContentIconOptions extends IconContentSharedOptions, IconContentIconSelectorOptions, IconCSSFormatOptions {
customise?: (content: string) => string;
}
/**
* Options for generating multiple icons as background/mask
*/
interface IconCSSIconSetOptions extends IconCSSSharedOptions, IconCSSSelectorOptions, IconCSSModeOptions, IconCSSFormatOptions {
customise?: (content: string, name: string) => string;
}
/**
* Options for generating multiple icons as content
*/
interface IconContentIconSetOptions extends IconContentSharedOptions, IconContentIconSelectorOptions, IconCSSFormatOptions {
customise?: (content: string, name: string) => string;
}
export { CSSFormatMode, CSSUnformattedItem, IconCSSCommonCodeOptions, IconCSSFormatOptions, IconCSSIconOptions, IconCSSIconSelectorOptions, IconCSSIconSetOptions, IconCSSItemOptions, IconCSSMode, IconCSSModeOptions, IconCSSSelectorOptions, IconCSSSharedOptions, IconContentIconOptions, IconContentIconSelectorOptions, IconContentIconSetOptions, IconContentItemOptions, IconContentSharedOptions };
+1
View File
@@ -0,0 +1 @@
export {};
+5
View File
@@ -0,0 +1,5 @@
/**
* Get boolean customisation value from attribute
*/
declare function toBoolean(name: string, value: unknown, defaultValue: boolean): boolean;
export { toBoolean };
+19
View File
@@ -0,0 +1,19 @@
/**
* Get boolean customisation value from attribute
*/
function toBoolean(name, value, defaultValue) {
switch (typeof value) {
case "boolean": return value;
case "number": return !!value;
case "string": switch (value.toLowerCase()) {
case "1":
case "true":
case name.toLowerCase(): return true;
case "0":
case "false":
case "": return false;
}
}
return defaultValue;
}
export { toBoolean };
+23
View File
@@ -0,0 +1,23 @@
import { IconifyTransformations } from "@iconify/types";
/**
* Icon size
*/
type IconifyIconSize = null | string | number;
/**
* Dimensions
*/
interface IconifyIconSizeCustomisations {
width?: IconifyIconSize;
height?: IconifyIconSize;
}
/**
* Icon customisations
*/
interface IconifyIconCustomisations extends IconifyTransformations, IconifyIconSizeCustomisations {}
type FullIconCustomisations = Required<IconifyIconCustomisations>;
/**
* Default icon customisations values
*/
declare const defaultIconSizeCustomisations: Required<IconifyIconSizeCustomisations>;
declare const defaultIconCustomisations: FullIconCustomisations;
export { FullIconCustomisations, IconifyIconCustomisations, IconifyIconSize, IconifyIconSizeCustomisations, defaultIconCustomisations, defaultIconSizeCustomisations };
+13
View File
@@ -0,0 +1,13 @@
import { defaultIconTransformations } from "../icon/defaults.js";
/**
* Default icon customisations values
*/
const defaultIconSizeCustomisations = Object.freeze({
width: null,
height: null
});
const defaultIconCustomisations = Object.freeze({
...defaultIconSizeCustomisations,
...defaultIconTransformations
});
export { defaultIconCustomisations, defaultIconSizeCustomisations };
+12
View File
@@ -0,0 +1,12 @@
import { IconifyIconCustomisations } from "./defaults.js";
/**
* Additional shorthand customisations
*/
interface ShorthandIconCustomisations {
flip?: string;
}
/**
* Apply "flip" string to icon customisations
*/
declare function flipFromString(custom: IconifyIconCustomisations, flip: string): void;
export { ShorthandIconCustomisations, flipFromString };
+17
View File
@@ -0,0 +1,17 @@
const separator = /[\s,]+/;
/**
* Apply "flip" string to icon customisations
*/
function flipFromString(custom, flip) {
flip.split(separator).forEach((str) => {
switch (str.trim()) {
case "horizontal":
custom.hFlip = true;
break;
case "vertical":
custom.vFlip = true;
break;
}
});
}
export { flipFromString };
+6
View File
@@ -0,0 +1,6 @@
import { FullIconCustomisations, IconifyIconCustomisations } from "./defaults.js";
/**
* Convert IconifyIconCustomisations to FullIconCustomisations, checking value types
*/
declare function mergeCustomisations<T extends FullIconCustomisations>(defaults: T, item: IconifyIconCustomisations): T;
export { mergeCustomisations };
+16
View File
@@ -0,0 +1,16 @@
import { defaultIconSizeCustomisations } from "./defaults.js";
/**
* Convert IconifyIconCustomisations to FullIconCustomisations, checking value types
*/
function mergeCustomisations(defaults, item) {
const result = { ...defaults };
for (const key in item) {
const value = item[key];
const valueType = typeof value;
if (key in defaultIconSizeCustomisations) {
if (value === null || value && (valueType === "string" || valueType === "number")) result[key] = value;
} else if (valueType === typeof result[key]) result[key] = key === "rotate" ? value % 4 : value;
}
return result;
}
export { mergeCustomisations };
+5
View File
@@ -0,0 +1,5 @@
/**
* Get rotation value
*/
declare function rotateFromString(value: string, defaultValue?: number): number;
export { rotateFromString };
+30
View File
@@ -0,0 +1,30 @@
/**
* Get rotation value
*/
function rotateFromString(value, defaultValue = 0) {
const units = value.replace(/^-?[0-9.]*/, "");
function cleanup(value) {
while (value < 0) value += 4;
return value % 4;
}
if (units === "") {
const num = parseInt(value);
return isNaN(num) ? 0 : cleanup(num);
} else if (units !== value) {
let split = 0;
switch (units) {
case "%":
split = 25;
break;
case "deg": split = 90;
}
if (split) {
let num = parseFloat(value.slice(0, value.length - units.length));
if (isNaN(num)) return 0;
num = num / split;
return num % 1 === 0 ? cleanup(num) : 0;
}
}
return defaultValue;
}
export { rotateFromString };
+42
View File
@@ -0,0 +1,42 @@
/**
* Get emoji sequence from string
*
* @example
* // shows same emoji sequence formatted differently
* - '1F441 FE0F 200D 1F5E8 FE0F' => [0x1f441, 0xfe0f, 0x200d, 0x1f5e8, 0xfe0f]
* - '1f441-fe0f-200d-1f5e8-fe0f' => [0x1f441, 0xfe0f, 0x200d, 0x1f5e8, 0xfe0f]
* - '\\uD83D\\uDC41\\uFE0F\\u200D\\uD83D\\uDDE8\\uFE0F' => [0x1f441, 0xfe0f, 0x200d, 0x1f5e8, 0xfe0f]
*/
declare function getEmojiSequenceFromString(value: string): number[];
/**
* Convert emoji sequence or keyword
*
* If sequence is characters list, like '1f441-fe0f', it will be converted to [0x1f441, 0xfe0f]
* If sequence contains anything other than [0-9A-F-\s], it will be converted character by character
*
* This is used to treat keywords, like ':cat:' differently when converting strings to sequences
*/
declare function getSequenceFromEmojiStringOrKeyword(value: string): number[];
/**
* Split emoji sequence by joiner
*
* Result represents one emoji, split in smaller sequences separated by 0x200D
*
* @example
* [0x1FAF1, 0x1F3FB, 0x200D, 0x1FAF2, 0x1F3FC] => [[0x1FAF1, 0x1F3FB], [0x1FAF2, 0x1F3FC]]
*/
declare function splitEmojiSequences(sequence: number[], separator?: number): number[][];
/**
* Join emoji sequences
*
* Parameter represents one emoji, split in smaller sequences
*
* @example
* [[0x1FAF1, 0x1F3FB], [0x1FAF2, 0x1F3FC]] => [0x1FAF1, 0x1F3FB, 0x200D, 0x1FAF2, 0x1F3FC]
*/
declare function joinEmojiSequences(sequences: number[][], separator?: number): number[];
/**
* Get unqualified sequence
*/
declare function getUnqualifiedEmojiSequence(sequence: number[]): number[];
export { getEmojiSequenceFromString, getSequenceFromEmojiStringOrKeyword, getUnqualifiedEmojiSequence, joinEmojiSequences, splitEmojiSequences };
+78
View File
@@ -0,0 +1,78 @@
import { joinerEmoji, vs16Emoji } from "./data.js";
import { getEmojiCodePoint } from "./convert.js";
/**
* Get emoji sequence from string
*
* @example
* // shows same emoji sequence formatted differently
* - '1F441 FE0F 200D 1F5E8 FE0F' => [0x1f441, 0xfe0f, 0x200d, 0x1f5e8, 0xfe0f]
* - '1f441-fe0f-200d-1f5e8-fe0f' => [0x1f441, 0xfe0f, 0x200d, 0x1f5e8, 0xfe0f]
* - '\\uD83D\\uDC41\\uFE0F\\u200D\\uD83D\\uDDE8\\uFE0F' => [0x1f441, 0xfe0f, 0x200d, 0x1f5e8, 0xfe0f]
*/
function getEmojiSequenceFromString(value) {
return value.trim().split(/[^0-9A-F]+/i).filter((item) => item.length > 0).map(getEmojiCodePoint);
}
/**
* Convert emoji sequence or keyword
*
* If sequence is characters list, like '1f441-fe0f', it will be converted to [0x1f441, 0xfe0f]
* If sequence contains anything other than [0-9A-F-\s], it will be converted character by character
*
* This is used to treat keywords, like ':cat:' differently when converting strings to sequences
*/
function getSequenceFromEmojiStringOrKeyword(value) {
if (!value.match(/^[0-9a-fA-F-\s]+$/)) {
const results = [];
for (const codePoint of value) {
const code = codePoint.codePointAt(0);
if (code) results.push(code);
else return getEmojiSequenceFromString(value);
}
return results;
}
return getEmojiSequenceFromString(value);
}
/**
* Split emoji sequence by joiner
*
* Result represents one emoji, split in smaller sequences separated by 0x200D
*
* @example
* [0x1FAF1, 0x1F3FB, 0x200D, 0x1FAF2, 0x1F3FC] => [[0x1FAF1, 0x1F3FB], [0x1FAF2, 0x1F3FC]]
*/
function splitEmojiSequences(sequence, separator = joinerEmoji) {
const results = [];
let queue = [];
for (let i = 0; i < sequence.length; i++) {
const code = sequence[i];
if (code === separator) {
results.push(queue);
queue = [];
} else queue.push(code);
}
results.push(queue);
return results;
}
/**
* Join emoji sequences
*
* Parameter represents one emoji, split in smaller sequences
*
* @example
* [[0x1FAF1, 0x1F3FB], [0x1FAF2, 0x1F3FC]] => [0x1FAF1, 0x1F3FB, 0x200D, 0x1FAF2, 0x1F3FC]
*/
function joinEmojiSequences(sequences, separator = joinerEmoji) {
let results = [];
for (let i = 0; i < sequences.length; i++) {
if (i > 0) results.push(separator);
results = results.concat(sequences[i]);
}
return results;
}
/**
* Get unqualified sequence
*/
function getUnqualifiedEmojiSequence(sequence) {
return sequence.filter((num) => num !== vs16Emoji);
}
export { getEmojiSequenceFromString, getSequenceFromEmojiStringOrKeyword, getUnqualifiedEmojiSequence, joinEmojiSequences, splitEmojiSequences };
+34
View File
@@ -0,0 +1,34 @@
/**
* Convert string to number
*/
declare function getEmojiCodePoint(code: string): number;
/**
* Get UTF-32 as UTF-16 sequence
*/
declare function splitUTF32Number(code: number): [number, number] | undefined;
/**
* Check if number is UTF-32 split as UTF-16
*
* @returns
* - 1 if number fits first number in sequence
* - 2 if number fits second number in sequence
* - false on failure
*/
declare function isUTF32SplitNumber(value: number): 1 | 2 | false;
/**
* Get UTF-16 sequence as UTF-32
*/
declare function mergeUTF32Numbers(part1: number, part2: number): number | undefined;
/**
* Convert hexadecimal string or number to unicode
*/
declare function getEmojiUnicode(code: number | string): string;
/**
* Convert sequence to UTF-16
*/
declare function convertEmojiSequenceToUTF16(numbers: number[]): number[];
/**
* Convert sequence to UTF-32
*/
declare function convertEmojiSequenceToUTF32(numbers: number[], throwOnError?: boolean): number[];
export { convertEmojiSequenceToUTF16, convertEmojiSequenceToUTF32, getEmojiCodePoint, getEmojiUnicode, isUTF32SplitNumber, mergeUTF32Numbers, splitUTF32Number };
+100
View File
@@ -0,0 +1,100 @@
import { minUTF32, startUTF32Pair1, startUTF32Pair2 } from "./data.js";
/**
* Convert string to number
*/
function getEmojiCodePoint(code) {
return parseInt(code, 16);
}
/**
* First part of UTF-32 to UTF-16
*/
function utf32FirstNum(code) {
return (code - minUTF32 >> 10 | 0) + startUTF32Pair1;
}
/**
* First part of UTF-32 to UTF-16
*/
function utf32SecondNum(code) {
return (code - minUTF32 & 1023) + startUTF32Pair2;
}
/**
* Get UTF-32 as UTF-16 sequence
*/
function splitUTF32Number(code) {
if (code >= 65536) return [utf32FirstNum(code), utf32SecondNum(code)];
}
/**
* Check if number is UTF-32 split as UTF-16
*
* @returns
* - 1 if number fits first number in sequence
* - 2 if number fits second number in sequence
* - false on failure
*/
function isUTF32SplitNumber(value) {
if (value >= 55296) {
if (value < 56320) return 1;
if (value < 57344) return 2;
}
return false;
}
/**
* Get UTF-16 sequence as UTF-32
*/
function mergeUTF32Numbers(part1, part2) {
if (part1 < 55296 || part1 >= 56320 || part2 < 56320 || part2 >= 57344) return;
return (part1 - startUTF32Pair1 << 10) + (part2 - startUTF32Pair2) + minUTF32;
}
/**
* Convert hexadecimal string or number to unicode
*/
function getEmojiUnicode(code) {
return String.fromCodePoint(typeof code === "number" ? code : getEmojiCodePoint(code));
}
/**
* Convert sequence to UTF-16
*/
function convertEmojiSequenceToUTF16(numbers) {
const results = [];
for (let i = 0; i < numbers.length; i++) {
const code = numbers[i];
if (code >= 65536) {
results.push(utf32FirstNum(code));
results.push(utf32SecondNum(code));
} else results.push(code);
}
return results;
}
/**
* Convert sequence to UTF-32
*/
function convertEmojiSequenceToUTF32(numbers, throwOnError = true) {
const results = [];
for (let i = 0; i < numbers.length; i++) {
const code = numbers[i];
if (code >= 65536) {
results.push(code);
continue;
}
const part = isUTF32SplitNumber(code);
if (!part) {
results.push(code);
continue;
}
if (part === 1 && numbers.length > i + 1) {
const merged = mergeUTF32Numbers(code, numbers[i + 1]);
if (merged) {
i++;
results.push(merged);
continue;
}
}
if (throwOnError) {
const nextCode = numbers[i + 1];
throw new Error(`Invalid UTF-16 sequence: ${code.toString(16)}-${nextCode ? nextCode.toString(16) : "undefined"}`);
}
results.push(code);
}
return results;
}
export { convertEmojiSequenceToUTF16, convertEmojiSequenceToUTF32, getEmojiCodePoint, getEmojiUnicode, isUTF32SplitNumber, mergeUTF32Numbers, splitUTF32Number };
+32
View File
@@ -0,0 +1,32 @@
/** Joiner in emoji sequences */
declare const joinerEmoji = 8205;
/** Emoji as icon */
declare const vs16Emoji = 65039;
/** Keycap, preceeded by mandatory VS16 for full emoji */
declare const keycapEmoji = 8419;
/**
* Variations, UTF-32
*
* First value in array is minimum, second value is maximum+1
*/
type EmojiComponentType = 'skin-tone' | 'hair-style';
type Range = [number, number];
declare const emojiComponents: Record<EmojiComponentType, Range>;
/**
* Minimum UTF-32 number
*/
declare const minUTF32 = 65536;
/**
* Codes for UTF-32 characters presented as UTF-16
*
* startUTF32Pair1 <= code < startUTF32Pair2 -> code for first character in pair
* startUTF32Pair2 <= code < endUTF32Pair -> code for second character in pair
*/
declare const startUTF32Pair1 = 55296;
declare const startUTF32Pair2 = 56320;
declare const endUTF32Pair = 57344;
/**
* Emoji version as string
*/
declare const emojiVersion = "17.0";
export { EmojiComponentType, emojiComponents, emojiVersion, endUTF32Pair, joinerEmoji, keycapEmoji, minUTF32, startUTF32Pair1, startUTF32Pair2, vs16Emoji };
+28
View File
@@ -0,0 +1,28 @@
/** Joiner in emoji sequences */
const joinerEmoji = 8205;
/** Emoji as icon */
const vs16Emoji = 65039;
/** Keycap, preceeded by mandatory VS16 for full emoji */
const keycapEmoji = 8419;
const emojiComponents = {
"hair-style": [129456, 129460],
"skin-tone": [127995, 128e3]
};
/**
* Minimum UTF-32 number
*/
const minUTF32 = 65536;
/**
* Codes for UTF-32 characters presented as UTF-16
*
* startUTF32Pair1 <= code < startUTF32Pair2 -> code for first character in pair
* startUTF32Pair2 <= code < endUTF32Pair -> code for second character in pair
*/
const startUTF32Pair1 = 55296;
const startUTF32Pair2 = 56320;
const endUTF32Pair = 57344;
/**
* Emoji version as string
*/
const emojiVersion = "17.0";
export { emojiComponents, emojiVersion, endUTF32Pair, joinerEmoji, keycapEmoji, minUTF32, startUTF32Pair1, startUTF32Pair2, vs16Emoji };
+29
View File
@@ -0,0 +1,29 @@
interface UnicodeFormattingOptions {
prefix: string;
separator: string;
case: 'upper' | 'lower';
format: 'utf-32' | 'utf-16';
add0: boolean;
throwOnError: boolean;
}
/**
* Convert unicode number to string
*
* Example:
* 0x1F600 => '1F600'
*/
declare function getEmojiUnicodeString(code: number, options?: Partial<UnicodeFormattingOptions>): string;
/**
* Convert unicode numbers sequence to string
*
* Example:
* [0x1f441, 0xfe0f] => '1f441-fe0f'
*/
declare function getEmojiSequenceString(sequence: number[], options?: Partial<UnicodeFormattingOptions>): string;
/**
* Convert unicode numbers sequence to string
*
* Simple version of `getEmojiSequenceString()` without options that otherwise add to bundle
*/
declare function getEmojiSequenceKeyword(sequence: number[]): string;
export { UnicodeFormattingOptions, getEmojiSequenceKeyword, getEmojiSequenceString, getEmojiUnicodeString };
+58
View File
@@ -0,0 +1,58 @@
import { convertEmojiSequenceToUTF16, convertEmojiSequenceToUTF32 } from "./convert.js";
const defaultUnicodeOptions = {
prefix: "",
separator: "",
case: "lower",
format: "utf-32",
add0: false,
throwOnError: true
};
/**
* Convert number to string
*/
function convert(sequence, options) {
const prefix = options.prefix;
const func = options.case === "upper" ? "toUpperCase" : "toLowerCase";
return (options.format === "utf-16" ? convertEmojiSequenceToUTF16(sequence) : convertEmojiSequenceToUTF32(sequence, options.throwOnError)).map((code) => {
let str = code.toString(16);
if (options.add0 && str.length < 4) str = "0".repeat(4 - str.length) + str;
return prefix + str[func]();
}).join(options.separator);
}
/**
* Convert unicode number to string
*
* Example:
* 0x1F600 => '1F600'
*/
function getEmojiUnicodeString(code, options = {}) {
return convert([code], {
...defaultUnicodeOptions,
...options
});
}
const defaultSequenceOptions = {
...defaultUnicodeOptions,
separator: "-"
};
/**
* Convert unicode numbers sequence to string
*
* Example:
* [0x1f441, 0xfe0f] => '1f441-fe0f'
*/
function getEmojiSequenceString(sequence, options = {}) {
return convert(sequence, {
...defaultSequenceOptions,
...options
});
}
/**
* Convert unicode numbers sequence to string
*
* Simple version of `getEmojiSequenceString()` without options that otherwise add to bundle
*/
function getEmojiSequenceKeyword(sequence) {
return sequence.map((code) => code.toString(16)).join("-");
}
export { getEmojiSequenceKeyword, getEmojiSequenceString, getEmojiUnicodeString };
+32
View File
@@ -0,0 +1,32 @@
import { IconifyJSON } from "@iconify/types";
/** Parsed icon */
interface PreparedEmojiIcon {
/** Icon name */
icon: string;
/** Emoji sequence as string */
sequence: string;
}
/**
* Parse
*/
interface PreparedEmojiResult {
/** List of icons */
icons: PreparedEmojiIcon[];
/** Regular expression */
regex: string;
}
/**
* Prepare emoji for icons list
*
* Test data should be fetched from 'https://unicode.org/Public/emoji/17.0/emoji-test.txt'
* It is used to detect missing emojis and optimise regular expression
*/
declare function prepareEmojiForIconsList(icons: Record<string, string>, rawTestData?: string): PreparedEmojiResult;
/**
* Prepare emoji for an icon set
*
* Test data should be fetched from 'https://unicode.org/Public/emoji/15.1/emoji-test.txt'
* It is used to detect missing emojis and optimise regular expression
*/
declare function prepareEmojiForIconSet(iconSet: IconifyJSON, rawTestData?: string): PreparedEmojiResult;
export { PreparedEmojiIcon, PreparedEmojiResult, prepareEmojiForIconSet, prepareEmojiForIconsList };
+48
View File
@@ -0,0 +1,48 @@
import { getEmojiSequenceFromString, getUnqualifiedEmojiSequence } from "./cleanup.js";
import { getEmojiSequenceKeyword } from "./format.js";
import { parseEmojiTestFile } from "./test/parse.js";
import { getQualifiedEmojiVariations } from "./test/variations.js";
import { findMissingEmojis } from "./test/missing.js";
import { createOptimisedRegexForEmojiSequences } from "./regex/create.js";
import { combineSimilarEmojiTestData } from "./test/similar.js";
import { getEmojiTestDataTree } from "./test/tree.js";
/**
* Prepare emoji for icons list
*
* Test data should be fetched from 'https://unicode.org/Public/emoji/17.0/emoji-test.txt'
* It is used to detect missing emojis and optimise regular expression
*/
function prepareEmojiForIconsList(icons, rawTestData) {
const testData = rawTestData ? parseEmojiTestFile(rawTestData) : void 0;
let iconsList = [];
for (const char in icons) {
const sequence = getEmojiSequenceFromString(char);
iconsList.push({
icon: icons[char],
sequence
});
}
iconsList = getQualifiedEmojiVariations(iconsList);
if (testData) iconsList = iconsList.concat(findMissingEmojis(iconsList, getEmojiTestDataTree(combineSimilarEmojiTestData(testData))));
const preparedIcons = iconsList.map((item) => {
const sequence = getEmojiSequenceKeyword(getUnqualifiedEmojiSequence(item.sequence));
return {
icon: item.icon,
sequence
};
});
return {
regex: createOptimisedRegexForEmojiSequences(iconsList.map((item) => item.sequence)),
icons: preparedIcons
};
}
/**
* Prepare emoji for an icon set
*
* Test data should be fetched from 'https://unicode.org/Public/emoji/15.1/emoji-test.txt'
* It is used to detect missing emojis and optimise regular expression
*/
function prepareEmojiForIconSet(iconSet, rawTestData) {
return prepareEmojiForIconsList(iconSet.chars || {}, rawTestData);
}
export { prepareEmojiForIconSet, prepareEmojiForIconsList };
+74
View File
@@ -0,0 +1,74 @@
/**
* Regex in item
*/
interface BaseEmojiItemRegex {
type: 'utf16' | 'sequence' | 'set' | 'optional';
regex: string;
group: boolean;
length: number;
}
interface EmojiItemRegexWithNumbers {
numbers?: number[];
}
interface UTF16EmojiItemRegex extends BaseEmojiItemRegex, Required<EmojiItemRegexWithNumbers> {
type: 'utf16';
group: true;
}
type SequenceEmojiItemRegexItem = UTF16EmojiItemRegex | SetEmojiItemRegex | OptionalEmojiItemRegex;
interface SequenceEmojiItemRegex extends BaseEmojiItemRegex, EmojiItemRegexWithNumbers {
type: 'sequence';
items: SequenceEmojiItemRegexItem[];
}
type SetEmojiItemRegexItem = UTF16EmojiItemRegex | SequenceEmojiItemRegex | OptionalEmojiItemRegex;
interface SetEmojiItemRegex extends BaseEmojiItemRegex, EmojiItemRegexWithNumbers {
type: 'set';
sets: SetEmojiItemRegexItem[];
}
type OptionalEmojiItemRegexItem = UTF16EmojiItemRegex | SequenceEmojiItemRegex | SetEmojiItemRegex;
interface OptionalEmojiItemRegex extends BaseEmojiItemRegex {
type: 'optional';
item: OptionalEmojiItemRegexItem;
group: true;
}
type EmojiItemRegex = UTF16EmojiItemRegex | SequenceEmojiItemRegex | SetEmojiItemRegex | OptionalEmojiItemRegex;
/**
* Wrap regex in group
*/
declare function wrapRegexInGroup(regex: string): string;
/**
* Update UTF16 item, return regex
*/
declare function updateUTF16EmojiRegexItem(item: UTF16EmojiItemRegex): string;
/**
* Create UTF-16 regex
*/
declare function createUTF16EmojiRegexItem(numbers: number[]): UTF16EmojiItemRegex;
/**
* Update sequence regex. Does not update group
*/
declare function updateSequenceEmojiRegexItem(item: SequenceEmojiItemRegex): string;
/**
* Create sequence regex
*/
declare function createSequenceEmojiRegexItem(sequence: EmojiItemRegex[], numbers?: number[]): SequenceEmojiItemRegex;
/**
* Update set regex and group
*/
declare function updateSetEmojiRegexItem(item: SetEmojiItemRegex): string;
/**
* Create set regex
*/
declare function createSetEmojiRegexItem(set: EmojiItemRegex[]): SetEmojiItemRegex;
/**
* Update optional regex
*/
declare function updateOptionalEmojiRegexItem(item: OptionalEmojiItemRegex): string;
/**
* Create optional item
*/
declare function createOptionalEmojiRegexItem(item: EmojiItemRegex): OptionalEmojiItemRegex;
/**
* Clone item
*/
declare function cloneEmojiRegexItem<T extends BaseEmojiItemRegex>(item: T, shallow?: boolean): T;
export { EmojiItemRegex, OptionalEmojiItemRegex, SequenceEmojiItemRegex, SetEmojiItemRegex, SetEmojiItemRegexItem, UTF16EmojiItemRegex, cloneEmojiRegexItem, createOptionalEmojiRegexItem, createSequenceEmojiRegexItem, createSetEmojiRegexItem, createUTF16EmojiRegexItem, updateOptionalEmojiRegexItem, updateSequenceEmojiRegexItem, updateSetEmojiRegexItem, updateUTF16EmojiRegexItem, wrapRegexInGroup };
+203
View File
@@ -0,0 +1,203 @@
/**
* Convert number to string
*/
function toString(number) {
if (number < 255) {
if (number > 32 && number < 127) {
const char = String.fromCharCode(number);
if (number > 47 && number < 58 || number > 64 && number < 91 || number > 94 && number < 123) return char;
return "\\" + char;
}
return "\\x" + (number < 16 ? "0" : "") + number.toString(16).toUpperCase();
}
return "\\u" + number.toString(16).toUpperCase();
}
/**
* Typescript stuff
*/
function assertNever(v) {}
/**
* Wrap regex in group
*/
function wrapRegexInGroup(regex) {
return "(?:" + regex + ")";
}
/**
* Update UTF16 item, return regex
*/
function updateUTF16EmojiRegexItem(item) {
const numbers = item.numbers;
if (numbers.length === 1) {
const num = numbers[0];
return item.regex = toString(num);
}
numbers.sort((a, b) => a - b);
const chars = [];
let range = null;
const addRange = () => {
if (range) {
const { start, last, numbers } = range;
range = null;
if (last > start + 1) chars.push(toString(start) + "-" + toString(last));
else for (let i = 0; i < numbers.length; i++) chars.push(toString(numbers[i]));
}
};
for (let i = 0; i < numbers.length; i++) {
const num = numbers[i];
if (range) {
if (range.last === num) continue;
if (range.last === num - 1) {
range.numbers.push(num);
range.last = num;
continue;
}
}
addRange();
range = {
start: num,
last: num,
numbers: [num]
};
}
addRange();
if (!chars.length) throw new Error("Unexpected empty range");
return item.regex = "[" + chars.join("") + "]";
}
/**
* Create UTF-16 regex
*/
function createUTF16EmojiRegexItem(numbers) {
const result = {
type: "utf16",
regex: "",
numbers,
length: 1,
group: true
};
updateUTF16EmojiRegexItem(result);
return result;
}
/**
* Update sequence regex. Does not update group
*/
function updateSequenceEmojiRegexItem(item) {
return item.regex = item.items.map((childItem) => {
if (!childItem.group && childItem.type === "set") return wrapRegexInGroup(childItem.regex);
return childItem.regex;
}).join("");
}
/**
* Create sequence regex
*/
function createSequenceEmojiRegexItem(sequence, numbers) {
let items = [];
sequence.forEach((item) => {
if (item.type === "sequence") items = items.concat(item.items);
else items.push(item);
});
if (!items.length) throw new Error("Empty sequence");
const result = {
type: "sequence",
items,
regex: "",
length: items.reduce((length, item) => item.length + length, 0),
group: false
};
if (sequence.length === 1) {
const firstItem = sequence[0];
result.group = firstItem.group;
if (firstItem.type !== "optional") {
const numbers = firstItem.numbers;
if (numbers) result.numbers = numbers;
}
}
if (numbers) result.numbers = numbers;
updateSequenceEmojiRegexItem(result);
return result;
}
/**
* Update set regex and group
*/
function updateSetEmojiRegexItem(item) {
if (item.sets.length === 1) {
const firstItem = item.sets[0];
item.group = firstItem.group;
return item.regex = firstItem.regex;
}
item.group = false;
return item.regex = item.sets.map((childItem) => childItem.regex).join("|");
}
/**
* Create set regex
*/
function createSetEmojiRegexItem(set) {
let sets = [];
let numbers = [];
set.forEach((item) => {
if (item.type === "set") sets = sets.concat(item.sets);
else sets.push(item);
if (numbers) if (item.type === "optional" || !item.numbers) numbers = null;
else numbers = [...numbers, ...item.numbers];
});
sets.sort((a, b) => {
if (a.length === b.length) return a.regex.localeCompare(b.regex);
return b.length - a.length;
});
const result = {
type: "set",
sets,
regex: "",
length: sets.reduce((length, item) => length ? Math.min(length, item.length) : item.length, 0),
group: false
};
if (numbers) result.numbers = numbers;
if (set.length === 1) result.group = set[0].group;
updateSetEmojiRegexItem(result);
return result;
}
/**
* Update optional regex
*/
function updateOptionalEmojiRegexItem(item) {
const childItem = item.item;
return item.regex = (childItem.group ? childItem.regex : wrapRegexInGroup(childItem.regex)) + "?";
}
/**
* Create optional item
*/
function createOptionalEmojiRegexItem(item) {
if (item.type === "optional") return item;
const result = {
type: "optional",
item,
regex: "",
length: item.length,
group: true
};
updateOptionalEmojiRegexItem(result);
return result;
}
/**
* Clone item
*/
function cloneEmojiRegexItem(item, shallow = false) {
const result = { ...item };
if (result.type !== "optional" && result.numbers) result.numbers = [...result.numbers];
switch (result.type) {
case "utf16": break;
case "sequence":
if (shallow) result.items = [...result.items];
else result.items = result.items.map((item) => cloneEmojiRegexItem(item, false));
break;
case "set":
if (shallow) result.sets = [...result.sets];
else result.sets = result.sets.map((item) => cloneEmojiRegexItem(item, false));
break;
case "optional":
if (!shallow) result.item = cloneEmojiRegexItem(result.item, false);
break;
default: assertNever(result);
}
return result;
}
export { cloneEmojiRegexItem, createOptionalEmojiRegexItem, createSequenceEmojiRegexItem, createSetEmojiRegexItem, createUTF16EmojiRegexItem, updateOptionalEmojiRegexItem, updateSequenceEmojiRegexItem, updateSetEmojiRegexItem, updateUTF16EmojiRegexItem, wrapRegexInGroup };
+20
View File
@@ -0,0 +1,20 @@
/**
* Create optimised regex
*/
declare function createOptimisedRegexForEmojiSequences(sequences: number[][]): string;
/**
* Create optimised regex for emojis
*
* First parameter is array of emojis, entry can be either list of
* code points or emoji sequence as a string
*
* Examples of acceptable strings (case insensitive):
* '1F636 200D 1F32B FE0F' - space separated UTF32 sequence
* '1f636-200d-1f32b-fe0f' - dash separated UTF32 sequence
* 'd83d-de36-200d-d83c-df2b-fe0f' - dash separated UTF16 sequence
* '\\uD83D\\uDE36\\u200D\\uD83C\\uDF2B\\uFE0F' - UTF16 sequence escaped with '\\u'
*
* All examples above refer to the same emoji and will generate the same regex result
*/
declare function createOptimisedRegex(emojis: (string | number[])[]): string;
export { createOptimisedRegex, createOptimisedRegexForEmojiSequences };
+33
View File
@@ -0,0 +1,33 @@
import { convertEmojiSequenceToUTF32 } from "../convert.js";
import { getSequenceFromEmojiStringOrKeyword } from "../cleanup.js";
import { getQualifiedEmojiVariations } from "../test/variations.js";
import { createEmojisTree, parseEmojiTree } from "./tree.js";
/**
* Create optimised regex
*/
function createOptimisedRegexForEmojiSequences(sequences) {
sequences = sequences.map((item) => convertEmojiSequenceToUTF32(item));
return parseEmojiTree(createEmojisTree(sequences)).regex;
}
/**
* Create optimised regex for emojis
*
* First parameter is array of emojis, entry can be either list of
* code points or emoji sequence as a string
*
* Examples of acceptable strings (case insensitive):
* '1F636 200D 1F32B FE0F' - space separated UTF32 sequence
* '1f636-200d-1f32b-fe0f' - dash separated UTF32 sequence
* 'd83d-de36-200d-d83c-df2b-fe0f' - dash separated UTF16 sequence
* '\\uD83D\\uDE36\\u200D\\uD83C\\uDF2B\\uFE0F' - UTF16 sequence escaped with '\\u'
*
* All examples above refer to the same emoji and will generate the same regex result
*/
function createOptimisedRegex(emojis) {
let sequences = emojis.map((item) => typeof item === "string" ? getSequenceFromEmojiStringOrKeyword(item) : item);
sequences = getQualifiedEmojiVariations(sequences.map((sequence) => {
return { sequence };
})).map((item) => item.sequence);
return createOptimisedRegexForEmojiSequences(sequences);
}
export { createOptimisedRegex, createOptimisedRegexForEmojiSequences };
+14
View File
@@ -0,0 +1,14 @@
import { EmojiItemRegex, OptionalEmojiItemRegex, SequenceEmojiItemRegex, SetEmojiItemRegex, UTF16EmojiItemRegex } from "./base.js";
/**
* Create regex item for set of numbers
*/
declare function createEmojiRegexItemForNumbers(numbers: number[]): UTF16EmojiItemRegex | SequenceEmojiItemRegex | SetEmojiItemRegex;
/**
* Create sequence of numbers
*/
declare function createRegexForNumbersSequence(numbers: number[], optionalVariations?: boolean): SequenceEmojiItemRegex | UTF16EmojiItemRegex | OptionalEmojiItemRegex;
/**
* Attempt to optimise numbers in a set
*/
declare function optimiseNumbersSet(set: SetEmojiItemRegex): EmojiItemRegex;
export { createEmojiRegexItemForNumbers, createRegexForNumbersSequence, optimiseNumbersSet };
+132
View File
@@ -0,0 +1,132 @@
import "../data.js";
import { splitUTF32Number } from "../convert.js";
import { createOptionalEmojiRegexItem, createSequenceEmojiRegexItem, createSetEmojiRegexItem, createUTF16EmojiRegexItem } from "./base.js";
/**
* Create regex item for set of numbers
*/
function createEmojiRegexItemForNumbers(numbers) {
const utf32 = [];
const utf16 = [];
numbers.sort((a, b) => a - b);
let lastNumber;
for (let i = 0; i < numbers.length; i++) {
const number = numbers[i];
if (number === lastNumber) continue;
lastNumber = number;
const split = splitUTF32Number(number);
if (!split) {
utf16.push(number);
continue;
}
const [first, second] = split;
const item = utf32.find((item) => item.first === first);
if (item) {
item.second.push(second);
item.numbers.push(number);
} else utf32.push({
first,
second: [second],
numbers: [number]
});
}
const results = [];
if (utf16.length) results.push(createUTF16EmojiRegexItem(utf16));
if (utf32.length) {
const utf32Set = [];
for (let i = 0; i < utf32.length; i++) {
const item = utf32[i];
const secondRegex = createUTF16EmojiRegexItem(item.second);
const listItem = utf32Set.find((item) => item.second.regex === secondRegex.regex);
if (listItem) {
listItem.first.push(item.first);
listItem.numbers = [...listItem.numbers, ...item.numbers];
} else utf32Set.push({
second: secondRegex,
first: [item.first],
numbers: [...item.numbers]
});
}
for (let i = 0; i < utf32Set.length; i++) {
const item = utf32Set[i];
const firstRegex = createUTF16EmojiRegexItem(item.first);
const secondRegex = item.second;
results.push(createSequenceEmojiRegexItem([firstRegex, secondRegex], item.numbers));
}
}
return results.length === 1 ? results[0] : createSetEmojiRegexItem(results);
}
/**
* Create sequence of numbers
*/
function createRegexForNumbersSequence(numbers, optionalVariations = true) {
const items = [];
for (let i = 0; i < numbers.length; i++) {
const num = numbers[i];
const split = splitUTF32Number(num);
if (!split) {
const item = createUTF16EmojiRegexItem([num]);
if (optionalVariations && num === 65039) items.push(createOptionalEmojiRegexItem(item));
else items.push(item);
} else {
items.push(createUTF16EmojiRegexItem([split[0]]));
items.push(createUTF16EmojiRegexItem([split[1]]));
}
}
if (items.length === 1) return items[0];
const result = createSequenceEmojiRegexItem(items);
if (numbers.length === 1 && items[0].type === "utf16") result.numbers = [...numbers];
return result;
}
/**
* Attempt to optimise numbers in a set
*/
function optimiseNumbersSet(set) {
const mandatoryMatches = {
numbers: [],
items: []
};
const optionalMatches = {
numbers: [],
items: []
};
const filteredItems = set.sets.filter((item) => {
if (item.type === "optional") {
const parentItem = item.item;
if (parentItem.numbers) {
optionalMatches.items.push(item);
optionalMatches.numbers = optionalMatches.numbers.concat(parentItem.numbers);
return false;
}
return true;
}
if (item.numbers) {
mandatoryMatches.items.push(item);
mandatoryMatches.numbers = mandatoryMatches.numbers.concat(item.numbers);
return false;
}
return true;
});
if (mandatoryMatches.items.length + optionalMatches.items.length < 2) return set;
const optionalNumbers = new Set(optionalMatches.numbers);
let foundMatches = false;
mandatoryMatches.numbers = mandatoryMatches.numbers.filter((number) => {
if (optionalNumbers.has(number)) {
foundMatches = true;
return false;
}
return true;
});
if (mandatoryMatches.items.length) {
if (!foundMatches && mandatoryMatches.items.length === 1) filteredItems.push(mandatoryMatches.items[0]);
else if (mandatoryMatches.numbers.length) filteredItems.push(createEmojiRegexItemForNumbers(mandatoryMatches.numbers));
}
switch (optionalMatches.items.length) {
case 0: break;
case 1:
filteredItems.push(optionalMatches.items[0]);
break;
default: filteredItems.push(createOptionalEmojiRegexItem(createEmojiRegexItemForNumbers(optionalMatches.numbers)));
}
return filteredItems.length === 1 ? filteredItems[0] : createSetEmojiRegexItem(filteredItems);
}
export { createEmojiRegexItemForNumbers, createRegexForNumbersSequence, optimiseNumbersSet };
+46
View File
@@ -0,0 +1,46 @@
import { EmojiItemRegex, SetEmojiItemRegex } from "./base.js";
type SlicePosition = 'start' | 'end';
type SliceValue = number | 'full';
/**
* Slice of sequence
*/
interface SimilarRegexItemSlice {
index: number;
slice: SliceValue;
}
/**
* Similar sequence
*/
interface SimilarRegexItemSequence {
type: SlicePosition;
slices: SimilarRegexItemSlice[];
}
/**
* Result if findSimilarRegexItemSequences()
*/
interface SimilarRegexItemSequenceResult {
score: number;
sequences: SimilarRegexItemSequence[];
}
/**
* Find similar item sequences
*
* Returns sequence(s) with highest score. Only one of results should be
* applied to items. If there are multiple sequences, clone items list,
* attempt to apply each sequence, run further optimisations on each fork
* and see which one returns better result.
*
* Returns undefined if no common sequences found
*/
declare function findSimilarRegexItemSequences(items: EmojiItemRegex[]): SimilarRegexItemSequenceResult | undefined;
/**
* Merge similar sequences
*
* Accepts callback to run optimisation on created subset
*/
declare function mergeSimilarRegexItemSequences(items: EmojiItemRegex[], merge: SimilarRegexItemSequence, optimise?: (set: SetEmojiItemRegex) => EmojiItemRegex): EmojiItemRegex[];
/**
* Merge similar items
*/
declare function mergeSimilarItemsInSet(set: SetEmojiItemRegex): EmojiItemRegex;
export { findSimilarRegexItemSequences, mergeSimilarItemsInSet, mergeSimilarRegexItemSequences };
+165
View File
@@ -0,0 +1,165 @@
import { cloneEmojiRegexItem, createOptionalEmojiRegexItem, createSequenceEmojiRegexItem, createSetEmojiRegexItem } from "./base.js";
import { optimiseNumbersSet } from "./numbers.js";
/**
* Typescript stuff
*/
function assertNever(v) {}
/**
* Find similar item sequences
*
* Returns sequence(s) with highest score. Only one of results should be
* applied to items. If there are multiple sequences, clone items list,
* attempt to apply each sequence, run further optimisations on each fork
* and see which one returns better result.
*
* Returns undefined if no common sequences found
*/
function findSimilarRegexItemSequences(items) {
const startRegex = Object.create(null);
const endRegex = Object.create(null);
const addMapItem = (target, index, regex, slice) => {
if (!target[regex]) {
target[regex] = {
score: 0,
slices: [{
index,
slice
}]
};
return;
}
const item = target[regex];
item.score += regex.length;
item.slices.push({
index,
slice
});
};
for (let index = 0; index < items.length; index++) {
const baseItem = items[index];
switch (baseItem.type) {
case "optional":
case "utf16":
addMapItem(startRegex, index, baseItem.regex, "full");
addMapItem(endRegex, index, baseItem.regex, "full");
break;
case "sequence": {
addMapItem(startRegex, index, baseItem.regex, "full");
addMapItem(endRegex, index, baseItem.regex, "full");
const sequence = baseItem.items;
for (let i = 1; i < sequence.length; i++) {
const startSequence = createSequenceEmojiRegexItem(sequence.slice(0, i));
addMapItem(startRegex, index, startSequence.regex, i);
const endSequence = createSequenceEmojiRegexItem(sequence.slice(i));
addMapItem(endRegex, index, endSequence.regex, i);
}
break;
}
case "set": throw new Error("Unexpected set within a set");
default: assertNever(baseItem);
}
}
let result;
const checkResults = (target, type) => {
for (const regex in target) {
const item = target[regex];
if (!item.score) continue;
if (!result || result.score < item.score) {
result = {
score: item.score,
sequences: [{
type,
slices: item.slices
}]
};
continue;
}
if (result.score === item.score) result.sequences.push({
type,
slices: item.slices
});
}
};
checkResults(startRegex, "start");
checkResults(endRegex, "end");
return result;
}
/**
* Merge similar sequences
*
* Accepts callback to run optimisation on created subset
*/
function mergeSimilarRegexItemSequences(items, merge, optimise) {
const { type, slices } = merge;
const indexes = /* @__PURE__ */ new Set();
let hasFullSequence = false;
let longestMatch = 0;
let longestMatchIndex = -1;
const differentSequences = [];
for (let i = 0; i < slices.length; i++) {
const { index, slice } = slices[i];
const item = items[index];
let length;
if (slice === "full") {
hasFullSequence = true;
if (item.type === "sequence") length = item.items.length;
else length = 1;
} else {
if (item.type !== "sequence") throw new Error(`Unexpected partial match for type "${item.type}"`);
length = type === "start" ? slice : item.items.length - slice;
differentSequences.push(type === "start" ? item.items.slice(slice) : item.items.slice(0, slice));
}
if (length > longestMatch) {
longestMatchIndex = index;
longestMatch = length;
}
indexes.add(index);
}
if (longestMatch < 1 || longestMatchIndex < 0) throw new Error("Cannot find common sequence");
const commonItem = items[longestMatchIndex];
let sequence;
if (commonItem.type !== "sequence") {
if (longestMatch !== 1) throw new Error("Something went wrong. Cannot have long match in non-sequence");
sequence = [commonItem];
} else sequence = type === "start" ? commonItem.items.slice(0, longestMatch) : commonItem.items.slice(commonItem.items.length - longestMatch);
const setItems = [];
for (let i = 0; i < differentSequences.length; i++) {
const list = differentSequences[i];
if (list.length === 1) setItems.push(list[0]);
else setItems.push(createSequenceEmojiRegexItem(list));
}
const set = createSetEmojiRegexItem(setItems);
let mergedChunk = set.sets.length === 1 ? set.sets[0] : optimise ? optimise(set) : set;
if (hasFullSequence) mergedChunk = createOptionalEmojiRegexItem(mergedChunk);
sequence[type === "start" ? "push" : "unshift"](mergedChunk);
return [createSequenceEmojiRegexItem(sequence), ...items.filter((item, index) => !indexes.has(index))];
}
/**
* Merge similar items
*/
function mergeSimilarItemsInSet(set) {
const updatedSet = optimiseNumbersSet(set);
if (updatedSet.type !== "set") return updatedSet;
set = updatedSet;
let merges;
while (merges = findSimilarRegexItemSequences(set.sets)) {
const sequences = merges.sequences;
if (sequences.length === 1) {
const merged = mergeSimilarRegexItemSequences(set.sets.map((item) => cloneEmojiRegexItem(item, true)), sequences[0], mergeSimilarItemsInSet);
if (merged.length === 1) return merged[0];
set = createSetEmojiRegexItem(merged);
continue;
}
let newItem;
for (let i = 0; i < sequences.length; i++) {
const merged = mergeSimilarRegexItemSequences(set.sets.map((item) => cloneEmojiRegexItem(item, true)), sequences[i], mergeSimilarItemsInSet);
const mergedItem = merged.length === 1 ? merged[0] : createSetEmojiRegexItem(merged);
if (!newItem || mergedItem.regex.length < newItem.regex.length) newItem = mergedItem;
}
if (!newItem) throw new Error("Empty sequences list");
if (newItem.type !== "set") return newItem;
set = newItem;
}
return set;
}
export { findSimilarRegexItemSequences, mergeSimilarItemsInSet, mergeSimilarRegexItemSequences };
+18
View File
@@ -0,0 +1,18 @@
import { EmojiItemRegex } from "./base.js";
/**
* Tree item
*/
interface TreeItem {
regex: EmojiItemRegex;
end?: true;
children?: TreeItem[];
}
/**
* Create tree
*/
declare function createEmojisTree(sequences: number[][]): TreeItem[];
/**
* Parse tree
*/
declare function parseEmojiTree(items: TreeItem[]): EmojiItemRegex;
export { createEmojisTree, parseEmojiTree };
+79
View File
@@ -0,0 +1,79 @@
import { joinerEmoji } from "../data.js";
import { convertEmojiSequenceToUTF32 } from "../convert.js";
import { splitEmojiSequences } from "../cleanup.js";
import { createOptionalEmojiRegexItem, createSequenceEmojiRegexItem, createSetEmojiRegexItem, createUTF16EmojiRegexItem } from "./base.js";
import { createRegexForNumbersSequence } from "./numbers.js";
import { mergeSimilarItemsInSet } from "./similar.js";
/**
* Create tree
*/
function createEmojisTree(sequences) {
const root = [];
for (let i = 0; i < sequences.length; i++) {
const split = splitEmojiSequences(convertEmojiSequenceToUTF32(sequences[i]));
let parent = root;
for (let j = 0; j < split.length; j++) {
const regex = createRegexForNumbersSequence(split[j]);
let item;
const match = parent.find((item) => item.regex.regex === regex.regex);
if (!match) {
item = { regex };
parent.push(item);
} else item = match;
if (j === split.length - 1) {
item.end = true;
break;
}
parent = item.children || (item.children = []);
}
}
return root;
}
/**
* Parse tree
*/
function parseEmojiTree(items) {
function mergeParsedChildren(items) {
const parsedItems = [];
const mapWithoutEnd = Object.create(null);
const mapWithEnd = Object.create(null);
for (let i = 0; i < items.length; i++) {
const item = items[i];
const children = item.children;
if (children) {
const fullItem = item;
const target = item.end ? mapWithEnd : mapWithoutEnd;
const regex = children.regex;
if (!target[regex]) target[regex] = [fullItem];
else target[regex].push(fullItem);
} else parsedItems.push(item.regex);
}
[mapWithEnd, mapWithoutEnd].forEach((source) => {
for (const regex in source) {
const items = source[regex];
const firstItem = items[0];
let childSequence = [createUTF16EmojiRegexItem([joinerEmoji]), firstItem.children];
if (firstItem.end) childSequence = [createOptionalEmojiRegexItem(createSequenceEmojiRegexItem(childSequence))];
let mergedRegex;
if (items.length === 1) mergedRegex = firstItem.regex;
else mergedRegex = mergeSimilarItemsInSet(createSetEmojiRegexItem(items.map((item) => item.regex)));
const sequence = createSequenceEmojiRegexItem([mergedRegex, ...childSequence]);
parsedItems.push(sequence);
}
});
if (parsedItems.length === 1) return parsedItems[0];
return mergeSimilarItemsInSet(createSetEmojiRegexItem(parsedItems));
}
function parseItemChildren(item) {
const result = {
regex: item.regex,
end: !!item.end
};
const children = item.children;
if (!children) return result;
result.children = mergeParsedChildren(children.map(parseItemChildren));
return result;
}
return mergeParsedChildren(items.map(parseItemChildren));
}
export { createEmojisTree, parseEmojiTree };
+34
View File
@@ -0,0 +1,34 @@
/**
* Create regular expression instance
*/
declare function createEmojiRegExp(regexp: string): RegExp;
/**
* Match
*/
interface EmojiRegexMatch {
match: string;
sequence: number[];
keyword: string;
regexp: number;
}
/**
* Add prev/next
*/
interface PrevMatch {
match: EmojiRegexMatch;
prev: string;
}
interface PrevNextMatch extends PrevMatch {
next: string;
}
/**
* Find emojis in text
*
* Returns only one entry per match
*/
declare function getEmojiMatchesInText(regexp: string | RegExp | (string | RegExp)[], content: string): EmojiRegexMatch[];
/**
* Sort emojis, get prev and next text
*/
declare function sortEmojiMatchesInText(content: string, matches: EmojiRegexMatch[]): PrevNextMatch[];
export { EmojiRegexMatch, createEmojiRegExp, getEmojiMatchesInText, sortEmojiMatchesInText };
+92
View File
@@ -0,0 +1,92 @@
import "../data.js";
import { convertEmojiSequenceToUTF32 } from "../convert.js";
import { getEmojiSequenceKeyword } from "../format.js";
/**
* Create regular expression instance
*/
function createEmojiRegExp(regexp) {
return new RegExp(regexp, "g");
}
/**
* Find emojis in text
*
* Returns only one entry per match
*/
function getEmojiMatchesInText(regexp, content) {
const results = [];
const found = /* @__PURE__ */ new Set();
(regexp instanceof Array ? regexp : [regexp]).forEach((regexp, index) => {
const matches = content.match(typeof regexp === "string" ? createEmojiRegExp(regexp) : regexp);
if (matches) for (let i = 0; i < matches.length; i++) {
const match = matches[i];
if (found.has(match)) continue;
found.add(match);
const sequence = [];
for (const codePoint of match) {
const num = codePoint.codePointAt(0);
if (num !== 65039) sequence.push(num);
}
results.push({
match,
sequence,
keyword: getEmojiSequenceKeyword(convertEmojiSequenceToUTF32(sequence)),
regexp: index
});
}
});
results.sort((a, b) => {
const match1 = a.match;
const match2 = b.match;
if (match2.length === match1.length) return match1.localeCompare(match2);
return match2.length - match1.length;
});
return results;
}
/**
* Sort emojis, get prev and next text
*/
function sortEmojiMatchesInText(content, matches) {
const ranges = [];
const check = (start, end) => {
for (let i = 0; i < ranges.length; i++) if (start < ranges[i].end && end > ranges[i].start) return false;
return true;
};
for (let i = 0; i < matches.length; i++) {
const match = matches[i];
const search = match.match;
let startFrom = 0;
let start;
while ((start = content.indexOf(search, startFrom)) !== -1) {
const end = start + search.length;
startFrom = end;
if (check(start, end)) ranges.push({
start,
end,
match
});
}
}
ranges.sort((a, b) => a.start - b.start);
const list = [];
let prevRange;
let lastEnd;
for (let i = 0; i < ranges.length; i++) {
const range = ranges[i];
const prev = content.slice(prevRange ? prevRange.end : 0, range.start);
list.push({
match: range.match,
prev
});
prevRange = range;
lastEnd = range.end;
}
if (!lastEnd) return [];
return list.map((item, index) => {
const nextItem = list[index + 1];
return {
...item,
next: nextItem ? nextItem.prev : content.slice(lastEnd)
};
});
}
export { createEmojiRegExp, getEmojiMatchesInText, sortEmojiMatchesInText };
+14
View File
@@ -0,0 +1,14 @@
import { EmojiRegexMatch } from "./find.js";
/**
* Callback for replacing emoji in text
*
* Returns text to replace emoji with, undefined to skip replacement
*/
type FindAndReplaceEmojisInTextCallback = (match: EmojiRegexMatch, prev: string) => string | undefined;
/**
* Find and replace emojis in text
*
* Returns null if nothing was replaced
*/
declare function findAndReplaceEmojisInText(regexp: string | RegExp | (string | RegExp)[], content: string, callback: FindAndReplaceEmojisInTextCallback): string | null;
export { FindAndReplaceEmojisInTextCallback, findAndReplaceEmojisInText };
+26
View File
@@ -0,0 +1,26 @@
import { getEmojiMatchesInText, sortEmojiMatchesInText } from "./find.js";
/**
* Find and replace emojis in text
*
* Returns null if nothing was replaced
*/
function findAndReplaceEmojisInText(regexp, content, callback) {
const matches = getEmojiMatchesInText(regexp, content);
if (!matches.length) return null;
const sortedMatches = sortEmojiMatchesInText(content, matches);
let result = "";
let replaced = false;
for (let i = 0; i < sortedMatches.length; i++) {
const item = sortedMatches[i];
result += item.prev;
const replacement = callback({ ...item.match }, result);
if (replacement === void 0) result += item.match.match;
else {
result += replacement;
replaced = true;
}
}
result += sortedMatches[sortedMatches.length - 1].next;
return replaced ? result : null;
}
export { findAndReplaceEmojisInText };
+41
View File
@@ -0,0 +1,41 @@
import { EmojiComponentType } from "../data.js";
import { EmojiTestData, EmojiTestDataItem } from "./parse.js";
interface EmojiTestDataComponentsMap {
converted: Map<number, string>;
items: Map<string | number, EmojiTestDataItem>;
names: Map<string | number, string>;
types: Record<string, EmojiComponentType>;
keywords: Record<string, string>;
}
/**
* Map components from test data
*/
declare function mapEmojiTestDataComponents(testSequences: EmojiTestData): EmojiTestDataComponentsMap;
/**
* Sequence with components
*/
type EmojiSequenceWithComponents = (EmojiComponentType | number)[];
/**
* Convert to string
*/
declare function emojiSequenceWithComponentsToString(sequence: EmojiSequenceWithComponents): string;
/**
* Entry in sequence
*/
interface EmojiSequenceComponentEntry {
index: number;
type: EmojiComponentType;
}
/**
* Find variations in sequence
*/
declare function findEmojiComponentsInSequence(sequence: number[]): EmojiSequenceComponentEntry[];
/**
* Component values
*/
type EmojiSequenceComponentValues = Partial<Record<EmojiComponentType, number[]>>;
/**
* Replace components in sequence
*/
declare function replaceEmojiComponentsInCombinedSequence(sequence: EmojiSequenceWithComponents, values: EmojiSequenceComponentValues): number[];
export { EmojiSequenceComponentEntry, EmojiSequenceComponentValues, EmojiSequenceWithComponents, EmojiTestDataComponentsMap, emojiSequenceWithComponentsToString, findEmojiComponentsInSequence, mapEmojiTestDataComponents, replaceEmojiComponentsInCombinedSequence };
+76
View File
@@ -0,0 +1,76 @@
import { emojiComponents } from "../data.js";
import { getEmojiSequenceKeyword } from "../format.js";
/**
* Map components from test data
*/
function mapEmojiTestDataComponents(testSequences) {
const results = {
converted: /* @__PURE__ */ new Map(),
items: /* @__PURE__ */ new Map(),
names: /* @__PURE__ */ new Map(),
types: {},
keywords: {}
};
for (const key in emojiComponents) {
const type = key;
const range = emojiComponents[type];
for (let number = range[0]; number < range[1]; number++) {
const keyword = getEmojiSequenceKeyword([number]);
const item = testSequences[keyword];
if (!item) throw new Error(`Missing emoji component in test sequence: "${keyword}"`);
results.converted.set(number, keyword);
results.items.set(number, item);
results.items.set(keyword, item);
const name = item.name;
results.names.set(number, name);
results.names.set(keyword, name);
results.types[name] = type;
results.keywords[name] = keyword;
}
}
return results;
}
/**
* Convert to string
*/
function emojiSequenceWithComponentsToString(sequence) {
return sequence.map((item) => typeof item === "number" ? item.toString(16) : item).join("-");
}
/**
* Find variations in sequence
*/
function findEmojiComponentsInSequence(sequence) {
const components = [];
for (let index = 0; index < sequence.length; index++) {
const code = sequence[index];
for (const key in emojiComponents) {
const type = key;
const range = emojiComponents[type];
if (code >= range[0] && code < range[1]) {
components.push({
index,
type
});
break;
}
}
}
return components;
}
/**
* Replace components in sequence
*/
function replaceEmojiComponentsInCombinedSequence(sequence, values) {
const indexes = {
"hair-style": 0,
"skin-tone": 0
};
return sequence.map((item) => {
if (typeof item === "number") return item;
const index = indexes[item]++;
const list = values[item];
if (!list || !list.length) throw new Error(`Cannot replace ${item}: no valid values provided`);
return list[index >= list.length ? list.length - 1 : index];
});
}
export { emojiSequenceWithComponentsToString, findEmojiComponentsInSequence, mapEmojiTestDataComponents, replaceEmojiComponentsInCombinedSequence };
+17
View File
@@ -0,0 +1,17 @@
import { EmojiComponentsTree } from "./tree.js";
/**
* Base type to extend
*/
interface BaseSequenceItem {
sequence: number[];
sequenceKey?: string;
}
/**
* Find missing emojis
*
* Result includes missing items, which are extended from items that needs to
* be copied. To identify which emojis to copy, source object should include
* something like `iconName` key that points to icon sequence represents.
*/
declare function findMissingEmojis<T extends BaseSequenceItem>(sequences: T[], testDataTree: EmojiComponentsTree): T[];
export { findMissingEmojis };
+66
View File
@@ -0,0 +1,66 @@
import { emojiComponents } from "../data.js";
import { getUnqualifiedEmojiSequence } from "../cleanup.js";
import { getEmojiSequenceKeyword } from "../format.js";
import { replaceEmojiComponentsInCombinedSequence } from "./components.js";
/**
* Find missing emojis
*
* Result includes missing items, which are extended from items that needs to
* be copied. To identify which emojis to copy, source object should include
* something like `iconName` key that points to icon sequence represents.
*/
function findMissingEmojis(sequences, testDataTree) {
const results = [];
const existingItems = Object.create(null);
const copiedItems = Object.create(null);
sequences.forEach((item) => {
const key = getEmojiSequenceKeyword(getUnqualifiedEmojiSequence(item.sequence));
if (!existingItems[key] || existingItems[key].sequence.length < item.sequence.length) existingItems[key] = item;
});
const iterate = (type, parentTree, parentValues, parentItem, deep) => {
const childTree = parentTree.children?.[type];
if (!childTree) return;
const range = emojiComponents[type];
for (let number = range[0]; number < range[1]; number++) {
const values = {
"hair-style": [...parentValues["hair-style"]],
"skin-tone": [...parentValues["skin-tone"]]
};
values[type].push(number);
const sequence = replaceEmojiComponentsInCombinedSequence(childTree.item.sequence, values);
const key = getEmojiSequenceKeyword(getUnqualifiedEmojiSequence(sequence));
const oldItem = existingItems[key];
let item;
if (oldItem) item = oldItem;
else {
item = copiedItems[key];
if (!item) {
item = {
...parentItem,
sequence
};
if (item.sequenceKey) item.sequenceKey = key;
copiedItems[key] = item;
results.push(item);
}
}
if (deep || oldItem) for (const key in values) iterate(key, childTree, values, item, deep);
}
};
const parse = (key, deep) => {
const treeItem = testDataTree[key];
const rootItem = existingItems[treeItem.item.sequenceKey];
if (!rootItem) return;
const values = {
"skin-tone": [],
"hair-style": []
};
for (const key in values) iterate(key, treeItem, values, rootItem, deep);
};
for (const key in testDataTree) {
parse(key, false);
parse(key, true);
}
return results;
}
export { findMissingEmojis };
+19
View File
@@ -0,0 +1,19 @@
import { EmojiSequenceComponentEntry, EmojiTestDataComponentsMap } from "./components.js";
/**
* Split emoji name in base name and variations
*
* Variations are also split in strings and emoji components with indexes pointing to sequence
*/
interface SplitEmojiName {
base: string;
key: string;
variations?: (string | EmojiSequenceComponentEntry)[];
components?: number;
}
/**
* Split emoji name to base name and variations
*
* Also finds indexes of each variation
*/
declare function splitEmojiNameVariations(name: string, sequence: number[], componentsData: EmojiTestDataComponentsMap): SplitEmojiName;
export { SplitEmojiName, splitEmojiNameVariations };
+45
View File
@@ -0,0 +1,45 @@
import { emojiComponents } from "../data.js";
const nameSplit = ": ";
const variationSplit = ", ";
const ignoredVariations = new Set(["person"]);
/**
* Split emoji name to base name and variations
*
* Also finds indexes of each variation
*/
function splitEmojiNameVariations(name, sequence, componentsData) {
const parts = name.split(nameSplit);
const base = parts.shift();
if (!parts.length) return {
base,
key: base
};
const baseVariations = parts.join(nameSplit).split(variationSplit).filter((text) => {
if (!componentsData.types[text]) return !ignoredVariations.has(text);
return false;
});
const result = {
base,
key: base + (baseVariations.length ? nameSplit + baseVariations.join(variationSplit) : "")
};
let components = 0;
const variations = [...baseVariations];
for (let index = 0; index < sequence.length; index++) {
const num = sequence[index];
for (const key in emojiComponents) {
const type = key;
const range = emojiComponents[type];
if (num >= range[0] && num < range[1]) {
variations.push({
index,
type
});
components++;
}
}
}
if (variations.length) result.variations = variations;
if (components) result.components = components;
return result;
}
export { splitEmojiNameVariations };
+45
View File
@@ -0,0 +1,45 @@
type EmojiStatus = 'component' | 'fully-qualified' | 'minimally-qualified' | 'unqualified';
declare const componentStatus: EmojiStatus;
/**
* Base item
*/
interface BaseEmojiTestDataItem {
group: string;
subgroup: string;
version: string;
}
/**
* Test data item
*/
interface EmojiTestDataItem extends BaseEmojiTestDataItem {
sequence: number[];
emoji: string;
status: EmojiStatus;
name: string;
}
type EmojiTestData = Record<string, EmojiTestDataItem>;
/**
* Get all emoji sequences from test file
*
* Returns all emojis as UTF-32 sequences, where:
* key = unqualified sequence (without \uFE0F)
* value = qualified sequence (with \uFE0F)
*
* Duplicate items that have different versions with and without \uFE0F are
* listed only once, with unqualified sequence as key and longest possible
* qualified sequence as value
*
* Example of 3 identical entries:
* '1F441 FE0F 200D 1F5E8 FE0F'
* '1F441 200D 1F5E8 FE0F'
* '1F441 FE0F 200D 1F5E8'
* '1F441 200D 1F5E8'
*
* Out of these entries, only one item will be returned with:
* key = '1f441-200d-1f5e8' (converted to lower case, separated with dash)
* value.sequence = [0x1F441, 0xFE0F, 0x200D, 0x1F5E8, 0xFE0F]
* value.status = 'fully-qualified'
* other properties in value are identical for all versions
*/
declare function parseEmojiTestFile(data: string): EmojiTestData;
export { BaseEmojiTestDataItem, EmojiStatus, EmojiTestData, EmojiTestDataItem, componentStatus, parseEmojiTestFile };
+103
View File
@@ -0,0 +1,103 @@
import { getEmojiSequenceFromString, getUnqualifiedEmojiSequence } from "../cleanup.js";
import { getEmojiSequenceKeyword } from "../format.js";
const componentStatus = "component";
const allowedStatus = new Set([
componentStatus,
"fully-qualified",
"minimally-qualified",
"unqualified"
]);
/**
* Get qualified variations from parsed test file
*
* Key is unqualified emoji, value is longest fully qualified emoji
*/
function getQualifiedTestData(data) {
const results = Object.create(null);
for (const key in data) {
const item = data[key];
const sequence = getUnqualifiedEmojiSequence(item.sequence);
const shortKey = getEmojiSequenceKeyword(sequence);
if (!results[shortKey] || results[shortKey].sequence.length < sequence.length) results[shortKey] = item;
}
return results;
}
/**
* Get all emoji sequences from test file
*
* Returns all emojis as UTF-32 sequences, where:
* key = unqualified sequence (without \uFE0F)
* value = qualified sequence (with \uFE0F)
*
* Duplicate items that have different versions with and without \uFE0F are
* listed only once, with unqualified sequence as key and longest possible
* qualified sequence as value
*
* Example of 3 identical entries:
* '1F441 FE0F 200D 1F5E8 FE0F'
* '1F441 200D 1F5E8 FE0F'
* '1F441 FE0F 200D 1F5E8'
* '1F441 200D 1F5E8'
*
* Out of these entries, only one item will be returned with:
* key = '1f441-200d-1f5e8' (converted to lower case, separated with dash)
* value.sequence = [0x1F441, 0xFE0F, 0x200D, 0x1F5E8, 0xFE0F]
* value.status = 'fully-qualified'
* other properties in value are identical for all versions
*/
function parseEmojiTestFile(data) {
const results = Object.create(null);
let group;
let subgroup;
data.split("\n").forEach((line) => {
line = line.trim();
const parts = line.split("#");
if (parts.length < 2) return;
const firstChunk = parts.shift().trim();
const secondChunk = parts.join("#").trim();
if (!firstChunk) {
const commentParts = secondChunk.split(":");
if (commentParts.length === 2) {
const key = commentParts[0].trim();
const value = commentParts[1].trim();
switch (key) {
case "group":
group = value;
subgroup = void 0;
break;
case "subgroup":
subgroup = value;
break;
}
}
return;
}
if (!group || !subgroup) return;
const firstChunkParts = firstChunk.split(";");
if (firstChunkParts.length !== 2) return;
const code = firstChunkParts[0].trim();
if (!code || !code.match(/^[A-F0-9]+[A-F0-9\s]*[A-F0-9]+$/)) return;
const status = firstChunkParts[1].trim();
if (!allowedStatus.has(status)) throw new Error(`Bad emoji type: ${status}`);
const secondChunkParts = secondChunk.split(/\s+/);
if (secondChunkParts.length < 3) throw new Error(`Bad emoji comment for: ${code}`);
const emoji = secondChunkParts.shift();
const version = secondChunkParts.shift();
if (version.slice(0, 1) !== "E") throw new Error(`Bad unicode version "${version}" for: ${code}`);
const name = secondChunkParts.join(" ");
const sequence = getEmojiSequenceFromString(code);
const key = getEmojiSequenceKeyword(sequence);
if (results[key]) throw new Error(`Duplicate entry for "${code}"`);
results[key] = {
group,
subgroup,
sequence,
emoji,
status,
version,
name
};
});
return getQualifiedTestData(results);
}
export { componentStatus, parseEmojiTestFile };
+21
View File
@@ -0,0 +1,21 @@
import { BaseEmojiTestDataItem, EmojiTestData, EmojiTestDataItem } from "./parse.js";
import { EmojiSequenceWithComponents, EmojiTestDataComponentsMap } from "./components.js";
import { SplitEmojiName } from "./name.js";
/**
* Similar test data items as one item
*/
interface CombinedEmojiTestDataItem extends BaseEmojiTestDataItem {
name: SplitEmojiName;
sequenceKey: string;
sequence: EmojiSequenceWithComponents;
}
type SimilarEmojiTestData = Record<string, CombinedEmojiTestDataItem>;
/**
* Find components in item, generate CombinedEmojiTestDataItem
*/
declare function findComponentsInEmojiTestItem(item: EmojiTestDataItem, componentsData: EmojiTestDataComponentsMap): CombinedEmojiTestDataItem;
/**
* Combine similar items in one iteratable item
*/
declare function combineSimilarEmojiTestData(data: EmojiTestData, componentsData?: EmojiTestDataComponentsMap): SimilarEmojiTestData;
export { CombinedEmojiTestDataItem, SimilarEmojiTestData, combineSimilarEmojiTestData, findComponentsInEmojiTestItem };
+36
View File
@@ -0,0 +1,36 @@
import { vs16Emoji } from "../data.js";
import { emojiSequenceWithComponentsToString, mapEmojiTestDataComponents } from "./components.js";
import { splitEmojiNameVariations } from "./name.js";
/**
* Find components in item, generate CombinedEmojiTestDataItem
*/
function findComponentsInEmojiTestItem(item, componentsData) {
const name = splitEmojiNameVariations(item.name, item.sequence, componentsData);
const sequence = [...item.sequence];
name.variations?.forEach((item) => {
if (typeof item !== "string") sequence[item.index] = item.type;
});
const sequenceKey = emojiSequenceWithComponentsToString(sequence.filter((code) => code !== vs16Emoji));
return {
...item,
name,
sequenceKey,
sequence
};
}
/**
* Combine similar items in one iteratable item
*/
function combineSimilarEmojiTestData(data, componentsData) {
const results = Object.create(null);
componentsData = componentsData || mapEmojiTestDataComponents(data);
for (const key in data) {
const sourceItem = data[key];
if (sourceItem.status !== "component") {
const item = findComponentsInEmojiTestItem(sourceItem, componentsData);
results[item.sequenceKey] = item;
}
}
return results;
}
export { combineSimilarEmojiTestData, findComponentsInEmojiTestItem };
+26
View File
@@ -0,0 +1,26 @@
import { EmojiComponentType } from "../data.js";
import { CombinedEmojiTestDataItem, SimilarEmojiTestData } from "./similar.js";
/**
* List of components
*/
type ComponentsCount = Required<Record<EmojiComponentType, number>>;
/**
* Extended tree item
*/
interface TreeSplitEmojiTestDataItem extends CombinedEmojiTestDataItem {
components: ComponentsCount;
componentsKey: string;
}
/**
* Tree item
*/
interface EmojiComponentsTreeItem {
item: TreeSplitEmojiTestDataItem;
children?: Record<EmojiComponentType, EmojiComponentsTreeItem>;
}
type EmojiComponentsTree = Record<string, EmojiComponentsTreeItem>;
/**
* Convert test data to dependencies tree, based on components
*/
declare function getEmojiTestDataTree(data: SimilarEmojiTestData): EmojiComponentsTree;
export { EmojiComponentsTree, EmojiComponentsTreeItem, getEmojiTestDataTree };
+92
View File
@@ -0,0 +1,92 @@
import { emojiComponents } from "../data.js";
/**
* Merge types for unique key
*/
function mergeComponentTypes(value) {
return "[" + value.join(",") + "]";
}
/**
* Merge count for unique key
*/
function mergeComponentsCount(value) {
const keys = [];
for (const key in emojiComponents) {
const type = key;
for (let i = 0; i < value[type]; i++) keys.push(type);
}
return keys.length ? mergeComponentTypes(keys) : "";
}
/**
* Get item from group
*/
function getGroupItem(items, components) {
const item = items[mergeComponentsCount(components)];
if (item) {
item.parsed = true;
return item.item;
}
}
/**
* Convert test data to dependencies tree, based on components
*/
function getEmojiTestDataTree(data) {
const groups = Object.create(null);
for (const key in data) {
const item = data[key];
const text = item.name.key;
const parent = groups[text] || (groups[text] = {});
const components = {
"hair-style": 0,
"skin-tone": 0
};
item.sequence.forEach((value) => {
if (typeof value !== "number") components[value]++;
});
const componentsKey = mergeComponentsCount(components);
if (parent[componentsKey]) throw new Error(`Duplicate components tree item for "${text}"`);
parent[componentsKey] = { item: {
...item,
components,
componentsKey
} };
}
const results = Object.create(null);
for (const key in groups) {
const items = groups[key];
const check = (parent, parentComponents, type) => {
const item = parse(parentComponents, [type]);
if (item) {
const children = parent.children || (parent.children = {});
children[type] = item;
return true;
}
};
const parse = (parentComponents, newComponents) => {
const components = {
"hair-style": 0,
"skin-tone": 0
};
const componentsList = parentComponents.concat(newComponents);
componentsList.forEach((type) => {
components[type]++;
});
let item = getGroupItem(items, components);
if (!item && newComponents.length === 1 && newComponents[0] === "skin-tone") {
const doubleComponents = { ...components };
doubleComponents["skin-tone"]++;
item = getGroupItem(items, doubleComponents);
}
if (item) {
const result = { item };
for (const key in emojiComponents) check(result, componentsList, key);
return result;
}
};
const root = parse([], []);
if (!root) throw new Error(`Cannot find parent item for "${key}"`);
for (const itemsKey in items) if (!items[itemsKey].parsed) throw new Error(`Error generating tree for "${key}"`);
if (root.children) results[key] = root;
}
return results;
}
export { getEmojiTestDataTree };
+28
View File
@@ -0,0 +1,28 @@
/**
* Get qualified sequence, adding optional `FE0F` wherever it might exist
*
* This might result in sequence that is not actually valid, but considering
* that `FE0F` is always treated as optional, full sequence used in regex will
* catch both qualified and unqualified emojis, so proper sequence will get
* caught anyway. This function just makes sure that in case if sequence does
* have `FE0F`, it will be caught by regex too.
*/
declare function guessQualifiedEmojiSequence(sequence: number[]): number[];
/**
* Base type to extend
*/
interface BaseSequenceItem {
sequence: number[];
sequenceKey?: string;
}
/**
* Get qualified variations for emojis
*
* Also converts list to UTF-32 as needed and removes duplicate items
*/
declare function getQualifiedEmojiVariation<T extends BaseSequenceItem>(item: T): T;
/**
* Get qualified emoji variations for set of emojis, ignoring duplicate entries
*/
declare function getQualifiedEmojiVariations<T extends BaseSequenceItem>(items: T[]): T[];
export { getQualifiedEmojiVariation, getQualifiedEmojiVariations, guessQualifiedEmojiSequence };
+62
View File
@@ -0,0 +1,62 @@
import { emojiComponents, vs16Emoji } from "../data.js";
import { convertEmojiSequenceToUTF32 } from "../convert.js";
import { getUnqualifiedEmojiSequence, joinEmojiSequences, splitEmojiSequences } from "../cleanup.js";
import { getEmojiSequenceKeyword } from "../format.js";
/**
* Get qualified sequence, adding optional `FE0F` wherever it might exist
*
* This might result in sequence that is not actually valid, but considering
* that `FE0F` is always treated as optional, full sequence used in regex will
* catch both qualified and unqualified emojis, so proper sequence will get
* caught anyway. This function just makes sure that in case if sequence does
* have `FE0F`, it will be caught by regex too.
*/
function guessQualifiedEmojiSequence(sequence) {
return joinEmojiSequences(splitEmojiSequences(sequence).map((part) => {
if (part.indexOf(65039) !== -1) return part;
if (part.length === 2) {
const lastNum = part[1];
if (lastNum === 8419) return [
part[0],
vs16Emoji,
lastNum
];
for (const key in emojiComponents) {
const range = emojiComponents[key];
if (lastNum >= range[0] && lastNum < range[1]) return [
part[0],
vs16Emoji,
lastNum
];
}
}
return part.length === 1 ? [part[0], vs16Emoji] : part;
}));
}
/**
* Get qualified variations for emojis
*
* Also converts list to UTF-32 as needed and removes duplicate items
*/
function getQualifiedEmojiVariation(item) {
const unqualifiedSequence = getUnqualifiedEmojiSequence(convertEmojiSequenceToUTF32(item.sequence));
const result = {
...item,
sequence: guessQualifiedEmojiSequence(unqualifiedSequence)
};
if (result.sequenceKey) result.sequenceKey = getEmojiSequenceKeyword(unqualifiedSequence);
return result;
}
/**
* Get qualified emoji variations for set of emojis, ignoring duplicate entries
*/
function getQualifiedEmojiVariations(items) {
const results = Object.create(null);
for (let i = 0; i < items.length; i++) {
const result = getQualifiedEmojiVariation(items[i]);
const key = getEmojiSequenceKeyword(getUnqualifiedEmojiSequence(result.sequence));
if (!results[key] || results[key].sequence.length < result.sequence.length) results[key] = result;
}
return Object.values(results);
}
export { getQualifiedEmojiVariation, getQualifiedEmojiVariations, guessQualifiedEmojiSequence };
+26
View File
@@ -0,0 +1,26 @@
import { IconifyInfo } from "@iconify/types";
/**
* Item provided by API or loaded from collections.json, slightly different from IconifyInfo
*/
interface LegacyIconifyInfo {
name: string;
total?: number;
version?: string;
author?: string;
url?: string;
license?: string;
licenseURL?: string;
licenseSPDX?: string;
samples?: string[];
height?: number | number[];
displayHeight?: number;
samplesHeight?: number;
category?: string;
palette?: 'Colorless' | 'Colorful';
hidden?: boolean;
}
/**
* Convert data to valid CollectionInfo
*/
declare function convertIconSetInfo(data: unknown, expectedPrefix?: string): IconifyInfo | null;
export { LegacyIconifyInfo, convertIconSetInfo };
+125
View File
@@ -0,0 +1,125 @@
const minDisplayHeight = 16;
const maxDisplayHeight = 24;
/**
* Check if displayHeight value is valid, returns 0 on failure
*/
function validateDisplayHeight(value) {
while (value < minDisplayHeight) value *= 2;
while (value > maxDisplayHeight) value /= 2;
return value === Math.round(value) && value >= minDisplayHeight && value <= maxDisplayHeight ? value : 0;
}
/**
* Convert data to valid CollectionInfo
*/
function convertIconSetInfo(data, expectedPrefix = "") {
if (typeof data !== "object" || data === null) return null;
const source = data;
const getSourceNestedString = (field, key, defaultValue = "") => {
if (typeof source[field] !== "object") return defaultValue;
const obj = source[field];
return typeof obj[key] === "string" ? obj[key] : defaultValue;
};
let name;
if (typeof source.name === "string") name = source.name;
else if (typeof source.title === "string") name = source.title;
else return null;
if (expectedPrefix !== "" && typeof source.prefix === "string" && source.prefix !== expectedPrefix) return null;
const info = { name };
switch (typeof source.total) {
case "number":
info.total = source.total;
break;
case "string": {
const num = parseInt(source.total);
if (num > 0) info.total = num;
break;
}
}
if (typeof source.version === "string") info.version = source.version;
info.author = { name: getSourceNestedString("author", "name", typeof source.author === "string" ? source.author : "") };
if (typeof source.author === "object") {
const sourceAuthor = source.author;
if (typeof sourceAuthor.url === "string") info.author.url = sourceAuthor.url;
}
info.license = { title: getSourceNestedString("license", "title", typeof source.license === "string" ? source.license : "") };
if (typeof source.license === "object") {
const sourceLicense = source.license;
if (typeof sourceLicense.spdx === "string") info.license.spdx = sourceLicense.spdx;
if (typeof sourceLicense.url === "string") info.license.url = sourceLicense.url;
}
if (source.samples instanceof Array) {
const samples = [];
source.samples.forEach((item) => {
if (typeof item === "string" && !samples.includes(item)) samples.push(item);
});
if (samples.length) info.samples = samples;
}
if (typeof source.height === "number" || typeof source.height === "string") {
const num = parseInt(source.height);
if (num > 0) info.height = num;
}
if (source.height instanceof Array) {
source.height.forEach((item) => {
const num = parseInt(item);
if (num > 0) {
if (!(info.height instanceof Array)) info.height = [];
info.height.push(num);
}
});
switch (info.height.length) {
case 0:
delete info.height;
break;
case 1: info.height = info.height[0];
}
}
if (typeof info.height === "number") {
const displayHeight = validateDisplayHeight(info.height);
if (displayHeight && displayHeight !== info.height) info.displayHeight = displayHeight;
}
["samplesHeight", "displayHeight"].forEach((prop) => {
const value = source[prop];
if (typeof value === "number" || typeof value === "string") {
const displayHeight = validateDisplayHeight(parseInt(value));
if (displayHeight) info.displayHeight = displayHeight;
}
});
if (typeof source.category === "string") info.category = source.category;
if (source.tags instanceof Array) info.tags = source.tags;
switch (typeof source.palette) {
case "boolean":
info.palette = source.palette;
break;
case "string":
switch (source.palette.toLowerCase()) {
case "colorless":
case "false":
info.palette = false;
break;
case "colorful":
case "true": info.palette = true;
}
break;
}
if (source.hidden === true) info.hidden = true;
Object.keys(source).forEach((key) => {
const value = source[key];
if (typeof value !== "string") return;
switch (key) {
case "url":
case "uri":
info.author.url = value;
break;
case "licenseURL":
case "licenseURI":
info.license.url = value;
break;
case "licenseID":
case "licenseSPDX":
info.license.spdx = value;
break;
}
});
return info;
}
export { convertIconSetInfo };
+8
View File
@@ -0,0 +1,8 @@
import { IconifyJSON } from "@iconify/types";
/**
* Expand minified icon set
*
* Opposite of minifyIconSet() from ./minify.ts
*/
declare function expandIconSet(data: IconifyJSON): void;
export { expandIconSet };
+19
View File
@@ -0,0 +1,19 @@
import { defaultIconDimensions } from "../icon/defaults.js";
/**
* Expand minified icon set
*
* Opposite of minifyIconSet() from ./minify.ts
*/
function expandIconSet(data) {
const icons = Object.keys(data.icons);
Object.keys(defaultIconDimensions).forEach((prop) => {
if (typeof data[prop] !== typeof defaultIconDimensions[prop]) return;
const value = data[prop];
icons.forEach((name) => {
const item = data.icons[name];
if (!(prop in item)) item[prop] = value;
});
delete data[prop];
});
}
export { expandIconSet };
+10
View File
@@ -0,0 +1,10 @@
import { ExtendedIconifyIcon, IconifyJSON } from "@iconify/types";
/**
* Get icon data, using prepared aliases tree
*/
declare function internalGetIconData(data: IconifyJSON, name: string, tree: string[]): ExtendedIconifyIcon;
/**
* Get data for icon
*/
declare function getIconData(data: IconifyJSON, name: string): ExtendedIconifyIcon | null;
export { getIconData, internalGetIconData };
+25
View File
@@ -0,0 +1,25 @@
import { mergeIconData } from "../icon/merge.js";
import { getIconsTree } from "./tree.js";
/**
* Get icon data, using prepared aliases tree
*/
function internalGetIconData(data, name, tree) {
const icons = data.icons;
const aliases = data.aliases || Object.create(null);
let currentProps = {};
function parse(name) {
currentProps = mergeIconData(icons[name] || aliases[name], currentProps);
}
parse(name);
tree.forEach(parse);
return mergeIconData(data, currentProps);
}
/**
* Get data for icon
*/
function getIconData(data, name) {
if (data.icons[name]) return internalGetIconData(data, name, []);
const tree = getIconsTree(data, [name])[name];
return tree ? internalGetIconData(data, name, tree) : null;
}
export { getIconData, internalGetIconData };
+10
View File
@@ -0,0 +1,10 @@
import { IconifyJSON } from "@iconify/types";
/**
* Optional properties that must be copied when copying icon set
*/
declare const propsToCopy: (keyof IconifyJSON)[];
/**
* Extract icons from icon set
*/
declare function getIcons(data: IconifyJSON, names: string[], not_found?: boolean): IconifyJSON | null;
export { getIcons, propsToCopy };
+36
View File
@@ -0,0 +1,36 @@
import { defaultIconDimensions } from "../icon/defaults.js";
import { getIconsTree } from "./tree.js";
/**
* Optional properties that must be copied when copying icon set
*/
const propsToCopy = Object.keys(defaultIconDimensions).concat(["provider"]);
/**
* Extract icons from icon set
*/
function getIcons(data, names, not_found) {
const icons = Object.create(null);
const aliases = Object.create(null);
const result = {
prefix: data.prefix,
icons
};
const sourceIcons = data.icons;
const sourceAliases = data.aliases || Object.create(null);
if (data.lastModified) result.lastModified = data.lastModified;
const tree = getIconsTree(data, names);
let empty = true;
for (const name in tree) if (!tree[name]) {
if (not_found && names.indexOf(name) !== -1) (result.not_found || (result.not_found = [])).push(name);
} else if (sourceIcons[name]) {
icons[name] = { ...sourceIcons[name] };
empty = false;
} else {
aliases[name] = { ...sourceAliases[name] };
result.aliases = aliases;
}
propsToCopy.forEach((attr) => {
if (attr in data) result[attr] = data[attr];
});
return empty && not_found !== true ? null : result;
}
export { getIcons, propsToCopy };
+43
View File
@@ -0,0 +1,43 @@
import { IconifyJSON } from "@iconify/types";
/**
* Minify icon set
*
* Function finds common values for few numeric properties, such as 'width' and 'height' (see defaultIconDimensions keys for list of properties),
* removes entries from icons and sets default entry in root of icon set object.
*
* For example, this:
* {
* icons: {
* foo: {
* body: '<g />',
* width: 24
* },
* bar: {
* body: '<g />',
* width: 24
* },
* baz: {
* body: '<g />',
* width: 16
* }
* }
* }
* is changed to this:
* {
* icons: {
* foo: {
* body: '<g />'
* },
* bar: {
* body: '<g />'
* },
* baz: {
* body: '<g />',
* width: 16
* }
* },
* width: 24
* }
*/
declare function minifyIconSet(data: IconifyJSON): void;
export { minifyIconSet };
+91
View File
@@ -0,0 +1,91 @@
import { defaultIconDimensions } from "../icon/defaults.js";
/**
* Minify icon set
*
* Function finds common values for few numeric properties, such as 'width' and 'height' (see defaultIconDimensions keys for list of properties),
* removes entries from icons and sets default entry in root of icon set object.
*
* For example, this:
* {
* icons: {
* foo: {
* body: '<g />',
* width: 24
* },
* bar: {
* body: '<g />',
* width: 24
* },
* baz: {
* body: '<g />',
* width: 16
* }
* }
* }
* is changed to this:
* {
* icons: {
* foo: {
* body: '<g />'
* },
* bar: {
* body: '<g />'
* },
* baz: {
* body: '<g />',
* width: 16
* }
* },
* width: 24
* }
*/
function minifyIconSet(data) {
const icons = Object.keys(data.icons);
Object.keys(defaultIconDimensions).forEach((prop) => {
if (data[prop] === defaultIconDimensions[prop]) delete data[prop];
const defaultValue = defaultIconDimensions[prop];
const propType = typeof defaultValue;
const hasMinifiedDefault = typeof data[prop] === propType && data[prop] !== defaultValue;
let maxCount = 0;
let maxValue = null;
const counters = /* @__PURE__ */ new Map();
for (let i = 0; i < icons.length; i++) {
const item = data.icons[icons[i]];
let value;
if (typeof item[prop] === propType) value = item[prop];
else if (hasMinifiedDefault) value = data[prop];
else value = defaultIconDimensions[prop];
if (i === 0) {
maxCount = 1;
maxValue = value;
counters.set(value, 1);
continue;
}
if (!counters.has(value)) {
counters.set(value, 1);
continue;
}
const count = counters.get(value) + 1;
counters.set(value, count);
if (count > maxCount) {
maxCount = count;
maxValue = value;
}
}
const canMinify = maxValue !== null && maxCount > 1;
const oldDefault = hasMinifiedDefault ? data[prop] : null;
const newDefault = canMinify ? maxValue : oldDefault;
if (newDefault === defaultValue) delete data[prop];
else if (canMinify) data[prop] = newDefault;
icons.forEach((key) => {
const item = data.icons[key];
const value = prop in item ? item[prop] : hasMinifiedDefault ? oldDefault : defaultValue;
if (value === newDefault || newDefault === null && value === defaultValue) {
delete item[prop];
return;
}
if (canMinify && !(prop in item)) item[prop] = value;
});
});
}
export { minifyIconSet };
+19
View File
@@ -0,0 +1,19 @@
import { ExtendedIconifyIcon, IconifyJSON } from "@iconify/types";
/**
* Callback to call for each icon.
*
* If data === null, icon is missing.
*/
type SplitIconSetCallback = (name: string, data: ExtendedIconifyIcon | null) => unknown;
type SplitIconSetAsyncCallback = (name: string, data: ExtendedIconifyIcon | null) => Promise<unknown>;
/**
* Extract icons from an icon set
*
* Returns list of icons that were found in icon set
*/
declare function parseIconSet(data: IconifyJSON, callback: SplitIconSetCallback): string[];
/**
* Async version of parseIconSet()
*/
declare function parseIconSetAsync(data: IconifyJSON, callback: SplitIconSetAsyncCallback): Promise<string[]>;
export { SplitIconSetAsyncCallback, SplitIconSetCallback, parseIconSet, parseIconSetAsync };
+46
View File
@@ -0,0 +1,46 @@
import { getIconsTree } from "./tree.js";
import { internalGetIconData } from "./get-icon.js";
/**
* Extract icons from an icon set
*
* Returns list of icons that were found in icon set
*/
function parseIconSet(data, callback) {
const names = [];
if (typeof data !== "object" || typeof data.icons !== "object") return names;
if (data.not_found instanceof Array) data.not_found.forEach((name) => {
callback(name, null);
names.push(name);
});
const tree = getIconsTree(data);
for (const name in tree) {
const item = tree[name];
if (item) {
callback(name, internalGetIconData(data, name, item));
names.push(name);
}
}
return names;
}
/**
* Async version of parseIconSet()
*/
async function parseIconSetAsync(data, callback) {
const names = [];
if (typeof data !== "object" || typeof data.icons !== "object") return names;
if (data.not_found instanceof Array) for (let i = 0; i < data.not_found.length; i++) {
const name = data.not_found[i];
await callback(name, null);
names.push(name);
}
const tree = getIconsTree(data);
for (const name in tree) {
const item = tree[name];
if (item) {
await callback(name, internalGetIconData(data, name, item));
names.push(name);
}
}
return names;
}
export { parseIconSet, parseIconSetAsync };
+12
View File
@@ -0,0 +1,12 @@
import { IconifyJSON } from "@iconify/types";
/** Parent icons, first is direct parent, then parent of parent and so on. Does not include self */
type ParentIconsList = string[];
/** Result. Key is icon, value is list of parent icons */
type ParentIconsTree = Record<string, ParentIconsList | null>;
/**
* Resolve icon set icons
*
* Returns parent icon for each icon
*/
declare function getIconsTree(data: IconifyJSON, names?: string[]): ParentIconsTree;
export { ParentIconsList, ParentIconsTree, getIconsTree };
+23
View File
@@ -0,0 +1,23 @@
/**
* Resolve icon set icons
*
* Returns parent icon for each icon
*/
function getIconsTree(data, names) {
const icons = data.icons;
const aliases = data.aliases || Object.create(null);
const resolved = Object.create(null);
function resolve(name) {
if (icons[name]) return resolved[name] = [];
if (!(name in resolved)) {
resolved[name] = null;
const parent = aliases[name] && aliases[name].parent;
const value = parent && resolve(parent);
if (value) resolved[name] = [parent].concat(value);
}
return resolved[name];
}
(names || Object.keys(icons).concat(Object.keys(aliases))).forEach(resolve);
return resolved;
}
export { getIconsTree };
+9
View File
@@ -0,0 +1,9 @@
import { IconifyJSON } from "@iconify/types";
/**
* Validate icon set, return it as IconifyJSON on success, null on failure
*
* Unlike validateIconSet(), this function is very basic.
* It does not throw exceptions, it does not check metadata, it does not fix stuff.
*/
declare function quicklyValidateIconSet(obj: unknown): IconifyJSON | null;
export { quicklyValidateIconSet };
+42
View File
@@ -0,0 +1,42 @@
import { defaultExtendedIconProps, defaultIconDimensions } from "../icon/defaults.js";
/**
* Optional properties
*/
const optionalPropertyDefaults = {
provider: "",
aliases: {},
not_found: {},
...defaultIconDimensions
};
/**
* Check props
*/
function checkOptionalProps(item, defaults) {
for (const prop in defaults) if (prop in item && typeof item[prop] !== typeof defaults[prop]) return false;
return true;
}
/**
* Validate icon set, return it as IconifyJSON on success, null on failure
*
* Unlike validateIconSet(), this function is very basic.
* It does not throw exceptions, it does not check metadata, it does not fix stuff.
*/
function quicklyValidateIconSet(obj) {
if (typeof obj !== "object" || obj === null) return null;
const data = obj;
if (typeof data.prefix !== "string" || !obj.icons || typeof obj.icons !== "object") return null;
if (!checkOptionalProps(obj, optionalPropertyDefaults)) return null;
const icons = data.icons;
for (const name in icons) {
const icon = icons[name];
if (!name || typeof icon.body !== "string" || !checkOptionalProps(icon, defaultExtendedIconProps)) return null;
}
const aliases = data.aliases || Object.create(null);
for (const name in aliases) {
const icon = aliases[name];
const parent = icon.parent;
if (!name || typeof parent !== "string" || !icons[parent] && !aliases[parent] || !checkOptionalProps(icon, defaultExtendedIconProps)) return null;
}
return data;
}
export { quicklyValidateIconSet };
+18
View File
@@ -0,0 +1,18 @@
import { IconifyJSON } from "@iconify/types";
/**
* Match character
*/
declare const matchChar: RegExp;
interface IconSetValidationOptions {
/** Whether validation function will attempt to fix icon set instead of throwing errors. */
fix?: boolean;
/** Values for provider and prefix. If missing, validation should add them. */
prefix?: string;
provider?: string;
}
/**
* Validate icon set
* @returns param obj as IconifyJSON type on success, throw error on failure
*/
declare function validateIconSet(obj: unknown, options?: IconSetValidationOptions): IconifyJSON;
export { IconSetValidationOptions, matchChar, validateIconSet };
+123
View File
@@ -0,0 +1,123 @@
import { defaultExtendedIconProps } from "../icon/defaults.js";
import { getIconsTree } from "./tree.js";
/**
* Match character
*/
const matchChar = /^[a-f0-9]+(-[a-f0-9]+)*$/;
/**
* Validate icon
*
* Returns name of property that failed validation or null on success
*/
function validateIconProps(item, fix, checkOtherProps) {
for (const key in item) {
const attr = key;
const type = typeof item[attr];
if (type === "undefined") {
delete item[attr];
continue;
}
const expectedType = typeof defaultExtendedIconProps[attr];
if (expectedType !== "undefined") {
if (type !== expectedType) {
if (fix) {
delete item[attr];
continue;
}
return attr;
}
continue;
}
if (checkOtherProps && type === "object") if (fix) delete item[attr];
else return key;
}
return null;
}
/**
* Validate icon set
* @returns param obj as IconifyJSON type on success, throw error on failure
*/
function validateIconSet(obj, options) {
const fix = !!(options && options.fix);
if (typeof obj !== "object" || obj === null || typeof obj.icons !== "object" || !obj.icons) throw new Error("Bad icon set");
const data = obj;
if (options && typeof options.prefix === "string") data.prefix = options.prefix;
else if (typeof data.prefix !== "string" || !data.prefix) throw new Error("Invalid prefix");
if (options && typeof options.provider === "string") data.provider = options.provider;
else if (data.provider !== void 0) {
if (typeof data.provider !== "string") if (fix) delete data.provider;
else throw new Error("Invalid provider");
}
if (data.aliases !== void 0) {
if (typeof data.aliases !== "object" || data.aliases === null) if (fix) delete data.aliases;
else throw new Error("Invalid aliases list");
}
const tree = getIconsTree(data);
const icons = data.icons;
const aliases = data.aliases || Object.create(null);
for (const name in tree) {
const treeItem = tree[name];
const isAlias = !icons[name];
const parentObj = isAlias ? aliases : icons;
if (!treeItem) {
if (fix) {
delete parentObj[name];
continue;
}
throw new Error(`Invalid alias: ${name}`);
}
if (!name) {
if (fix) {
delete parentObj[name];
continue;
}
throw new Error(`Invalid icon name: "${name}"`);
}
const item = parentObj[name];
if (!isAlias) {
if (typeof item.body !== "string") {
if (fix) {
delete parentObj[name];
continue;
}
throw new Error(`Invalid icon: "${name}"`);
}
}
const requiredProp = isAlias ? "parent" : "body";
const key = typeof item[requiredProp] !== "string" ? requiredProp : validateIconProps(item, fix, true);
if (key !== null) throw new Error(`Invalid property "${key}" in "${name}"`);
}
if (data.not_found !== void 0 && !(data.not_found instanceof Array)) if (fix) delete data.not_found;
else throw new Error("Invalid not_found list");
if (!Object.keys(data.icons).length && !(data.not_found && data.not_found.length)) throw new Error("Icon set is empty");
if (fix && !Object.keys(aliases).length) delete data.aliases;
const failedOptionalProp = validateIconProps(data, false, false);
if (failedOptionalProp) throw new Error(`Invalid value type for "${failedOptionalProp}"`);
if (data.chars !== void 0) {
if (typeof data.chars !== "object" || data.chars === null) if (fix) delete data.chars;
else throw new Error("Invalid characters map");
}
if (typeof data.chars === "object") {
const chars = data.chars;
Object.keys(chars).forEach((char) => {
if (!matchChar.exec(char) || typeof chars[char] !== "string") {
if (fix) {
delete chars[char];
return;
}
throw new Error(`Invalid character "${char}"`);
}
const target = chars[char];
if (!data.icons[target] && (!data.aliases || !data.aliases[target])) {
if (fix) {
delete chars[char];
return;
}
throw new Error(`Character "${char}" points to missing icon "${target}"`);
}
});
if (fix && !Object.keys(data.chars).length) delete data.chars;
}
return data;
}
export { matchChar, validateIconSet };
+15
View File
@@ -0,0 +1,15 @@
import { ExtendedIconifyIcon, IconifyDimenisons, IconifyIcon, IconifyOptional, IconifyTransformations } from "@iconify/types";
type FullIconifyIcon = Required<IconifyIcon>;
/** Partial and full extended icon */
type PartialExtendedIconifyIcon = Partial<ExtendedIconifyIcon>;
type IconifyIconExtraProps = Omit<ExtendedIconifyIcon, keyof IconifyIcon>;
type FullExtendedIconifyIcon = FullIconifyIcon & IconifyIconExtraProps;
/** Default values for dimensions */
declare const defaultIconDimensions: Required<IconifyDimenisons>;
/** Default values for transformations */
declare const defaultIconTransformations: Required<IconifyTransformations>;
/** Default values for all optional IconifyIcon properties */
declare const defaultIconProps: Required<IconifyOptional>;
/** Default values for all properties used in ExtendedIconifyIcon */
declare const defaultExtendedIconProps: Required<FullExtendedIconifyIcon>;
export { FullExtendedIconifyIcon, FullIconifyIcon, IconifyIcon, PartialExtendedIconifyIcon, defaultExtendedIconProps, defaultIconDimensions, defaultIconProps, defaultIconTransformations };
+25
View File
@@ -0,0 +1,25 @@
/** Default values for dimensions */
const defaultIconDimensions = Object.freeze({
left: 0,
top: 0,
width: 16,
height: 16
});
/** Default values for transformations */
const defaultIconTransformations = Object.freeze({
rotate: 0,
vFlip: false,
hFlip: false
});
/** Default values for all optional IconifyIcon properties */
const defaultIconProps = Object.freeze({
...defaultIconDimensions,
...defaultIconTransformations
});
/** Default values for all properties used in ExtendedIconifyIcon */
const defaultExtendedIconProps = Object.freeze({
...defaultIconProps,
body: "",
hidden: false
});
export { defaultExtendedIconProps, defaultIconDimensions, defaultIconProps, defaultIconTransformations };
+8
View File
@@ -0,0 +1,8 @@
import { PartialExtendedIconifyIcon } from "./defaults.js";
/**
* Merge icon and alias
*
* Can also be used to merge default values and icon
*/
declare function mergeIconData<T extends PartialExtendedIconifyIcon>(parent: T, child: PartialExtendedIconifyIcon): T;
export { mergeIconData };
+16
View File
@@ -0,0 +1,16 @@
import { defaultExtendedIconProps, defaultIconTransformations } from "./defaults.js";
import { mergeIconTransformations } from "./transformations.js";
/**
* Merge icon and alias
*
* Can also be used to merge default values and icon
*/
function mergeIconData(parent, child) {
const result = mergeIconTransformations(parent, child);
for (const key in defaultExtendedIconProps) if (key in defaultIconTransformations) {
if (key in parent && !(key in result)) result[key] = defaultIconTransformations[key];
} else if (key in child) result[key] = child[key];
else if (key in parent) result[key] = parent[key];
return result;
}
export { mergeIconData };
+30
View File
@@ -0,0 +1,30 @@
/**
* Icon name
*/
interface IconifyIconName {
readonly provider: string;
readonly prefix: string;
readonly name: string;
}
/**
* Icon source: icon object without name
*/
type IconifyIconSource = Omit<IconifyIconName, 'name'>;
/**
* Expression to test part of icon name.
*
* Used when loading icons from Iconify API due to project naming convension.
* Ignored when using custom icon sets - convension does not apply.
*/
declare const matchIconName: RegExp;
/**
* Convert string icon name to IconifyIconName object.
*/
declare const stringToIcon: (value: string, validate?: boolean, allowSimpleName?: boolean, provider?: string) => IconifyIconName | null;
/**
* Check if icon is valid.
*
* This function is not part of stringToIcon because validation is not needed for most code.
*/
declare const validateIconName: (icon: IconifyIconName | null, allowSimpleName?: boolean) => boolean;
export { IconifyIconName, IconifyIconSource, matchIconName, stringToIcon, validateIconName };
+57
View File
@@ -0,0 +1,57 @@
/**
* Expression to test part of icon name.
*
* Used when loading icons from Iconify API due to project naming convension.
* Ignored when using custom icon sets - convension does not apply.
*/
const matchIconName = /^[a-z0-9]+(-[a-z0-9]+)*$/;
/**
* Convert string icon name to IconifyIconName object.
*/
const stringToIcon = (value, validate, allowSimpleName, provider = "") => {
const colonSeparated = value.split(":");
if (value.slice(0, 1) === "@") {
if (colonSeparated.length < 2 || colonSeparated.length > 3) return null;
provider = colonSeparated.shift().slice(1);
}
if (colonSeparated.length > 3 || !colonSeparated.length) return null;
if (colonSeparated.length > 1) {
const name = colonSeparated.pop();
const prefix = colonSeparated.pop();
const result = {
provider: colonSeparated.length > 0 ? colonSeparated[0] : provider,
prefix,
name
};
return validate && !validateIconName(result) ? null : result;
}
const name = colonSeparated[0];
const dashSeparated = name.split("-");
if (dashSeparated.length > 1) {
const result = {
provider,
prefix: dashSeparated.shift(),
name: dashSeparated.join("-")
};
return validate && !validateIconName(result) ? null : result;
}
if (allowSimpleName && provider === "") {
const result = {
provider,
prefix: "",
name
};
return validate && !validateIconName(result, allowSimpleName) ? null : result;
}
return null;
};
/**
* Check if icon is valid.
*
* This function is not part of stringToIcon because validation is not needed for most code.
*/
const validateIconName = (icon, allowSimpleName) => {
if (!icon) return false;
return !!((allowSimpleName && icon.prefix === "" || !!icon.prefix) && !!icon.name);
};
export { matchIconName, stringToIcon, validateIconName };
+11
View File
@@ -0,0 +1,11 @@
import { SVGViewBox } from "../svg/viewbox.js";
import { IconifyIcon } from "@iconify/types";
/**
* Make icon square
*/
declare function makeIconSquare(icon: Required<IconifyIcon>): Required<IconifyIcon>;
/**
* Make icon viewBox square
*/
declare function makeViewBoxSquare(viewBox: SVGViewBox): SVGViewBox;
export { makeIconSquare, makeViewBoxSquare };
+33
View File
@@ -0,0 +1,33 @@
/**
* Make icon square
*/
function makeIconSquare(icon) {
if (icon.width !== icon.height) {
const max = Math.max(icon.width, icon.height);
return {
...icon,
width: max,
height: max,
left: icon.left - (max - icon.width) / 2,
top: icon.top - (max - icon.height) / 2
};
}
return icon;
}
/**
* Make icon viewBox square
*/
function makeViewBoxSquare(viewBox) {
const [left, top, width, height] = viewBox;
if (width !== height) {
const max = Math.max(width, height);
return [
left - (max - width) / 2,
top - (max - height) / 2,
max,
max
];
}
return viewBox;
}
export { makeIconSquare, makeViewBoxSquare };
+6
View File
@@ -0,0 +1,6 @@
import { IconifyTransformations } from "@iconify/types";
/**
* Merge transformations
*/
declare function mergeIconTransformations<T extends IconifyTransformations>(obj1: T, obj2: IconifyTransformations): T;
export { mergeIconTransformations };
+12
View File
@@ -0,0 +1,12 @@
/**
* Merge transformations
*/
function mergeIconTransformations(obj1, obj2) {
const result = {};
if (!obj1.hFlip !== !obj2.hFlip) result.hFlip = true;
if (!obj1.vFlip !== !obj2.vFlip) result.vFlip = true;
const rotate = ((obj1.rotate || 0) + (obj2.rotate || 0)) % 4;
if (rotate) result.rotate = rotate;
return result;
}
export { mergeIconTransformations };
+53
View File
@@ -0,0 +1,53 @@
import { colorToString, compareColors, stringToColor } from "./colors/index.js";
import { colorKeywords } from "./colors/keywords.js";
import { getIconCSS, getIconContentCSS } from "./css/icon.js";
import { getIconsCSS, getIconsContentCSS } from "./css/icons.js";
import { toBoolean } from "./customisations/bool.js";
import { FullIconCustomisations, IconifyIconCustomisations, IconifyIconSize, IconifyIconSizeCustomisations, defaultIconCustomisations, defaultIconSizeCustomisations } from "./customisations/defaults.js";
import { flipFromString } from "./customisations/flip.js";
import { mergeCustomisations } from "./customisations/merge.js";
import { rotateFromString } from "./customisations/rotate.js";
import { getEmojiSequenceFromString, getUnqualifiedEmojiSequence } from "./emoji/cleanup.js";
import { convertEmojiSequenceToUTF16, convertEmojiSequenceToUTF32, getEmojiCodePoint, getEmojiUnicode, isUTF32SplitNumber, mergeUTF32Numbers, splitUTF32Number } from "./emoji/convert.js";
import { getEmojiSequenceKeyword, getEmojiSequenceString, getEmojiUnicodeString } from "./emoji/format.js";
import { prepareEmojiForIconSet, prepareEmojiForIconsList } from "./emoji/parse.js";
import { createOptimisedRegex, createOptimisedRegexForEmojiSequences } from "./emoji/regex/create.js";
import { findAndReplaceEmojisInText } from "./emoji/replace/replace.js";
import { parseEmojiTestFile } from "./emoji/test/parse.js";
import { findMissingEmojis } from "./emoji/test/missing.js";
import { getQualifiedEmojiVariations } from "./emoji/test/variations.js";
import { convertIconSetInfo } from "./icon-set/convert-info.js";
import { expandIconSet } from "./icon-set/expand.js";
import { getIconData } from "./icon-set/get-icon.js";
import { getIcons } from "./icon-set/get-icons.js";
import { minifyIconSet } from "./icon-set/minify.js";
import { parseIconSet, parseIconSetAsync } from "./icon-set/parse.js";
import { ParentIconsList, ParentIconsTree, getIconsTree } from "./icon-set/tree.js";
import { quicklyValidateIconSet } from "./icon-set/validate-basic.js";
import { validateIconSet } from "./icon-set/validate.js";
import { FullExtendedIconifyIcon, FullIconifyIcon, IconifyIcon, PartialExtendedIconifyIcon, defaultExtendedIconProps, defaultIconDimensions, defaultIconProps, defaultIconTransformations } from "./icon/defaults.js";
import { mergeIconData } from "./icon/merge.js";
import { IconifyIconName, IconifyIconSource, matchIconName, stringToIcon, validateIconName } from "./icon/name.js";
import { SVGViewBox, getSVGViewBox } from "./svg/viewbox.js";
import { makeIconSquare } from "./icon/square.js";
import { mergeIconTransformations } from "./icon/transformations.js";
import { IconifyIconBuildResult, iconToSVG } from "./svg/build.js";
import { mergeDefsAndContent, splitSVGDefs, wrapSVGContent } from "./svg/defs.js";
import { clearIDCache, replaceIDs } from "./svg/id.js";
import { calculateSize } from "./svg/size.js";
import { encodeSvgForCss } from "./svg/encode-svg-for-css.js";
import { trimSVG } from "./svg/trim.js";
import { prettifySVG } from "./svg/pretty.js";
import { iconToHTML } from "./svg/html.js";
import { svgToData, svgToURL } from "./svg/url.js";
import { cleanUpInnerHTML } from "./svg/inner-html.js";
import { ParsedSVGContent, buildParsedSVG, convertParsedSVG, parseSVGContent } from "./svg/parse.js";
import { CustomCollections, CustomIconLoader, ExternalPkgName, IconCustomizations, IconCustomizer, IconifyLoaderOptions, InlineCollection, UniversalIconLoader } from "./loader/types.js";
import { mergeIconProps } from "./loader/utils.js";
import { getCustomIcon } from "./loader/custom.js";
import { searchForIcon } from "./loader/modern.js";
import { loadIcon } from "./loader/loader.js";
import { camelToKebab, camelize, pascalize, snakelize } from "./misc/strings.js";
import { commonObjectProps, compareObjects, unmergeObjects } from "./misc/objects.js";
import { sanitiseTitleAttribute } from "./misc/title.js";
export { type CustomCollections, type CustomIconLoader, type ExternalPkgName, type FullExtendedIconifyIcon, type FullIconCustomisations, type FullIconifyIcon, type IconCustomizations, type IconCustomizer, type IconifyIcon, type IconifyIconBuildResult, type IconifyIconCustomisations, type IconifyIconName, type IconifyIconSize, type IconifyIconSizeCustomisations, type IconifyIconSource, type IconifyLoaderOptions, type InlineCollection, type ParentIconsList, type ParentIconsTree, type ParsedSVGContent, type PartialExtendedIconifyIcon, type SVGViewBox, type UniversalIconLoader, buildParsedSVG, calculateSize, camelToKebab, camelize, cleanUpInnerHTML, clearIDCache, colorKeywords, colorToString, commonObjectProps, compareColors, compareObjects, convertEmojiSequenceToUTF16, convertEmojiSequenceToUTF32, convertIconSetInfo, convertParsedSVG, createOptimisedRegex, createOptimisedRegexForEmojiSequences, defaultExtendedIconProps, defaultIconCustomisations, defaultIconDimensions, defaultIconProps, defaultIconSizeCustomisations, defaultIconTransformations, encodeSvgForCss, expandIconSet, findAndReplaceEmojisInText, findMissingEmojis, flipFromString, getCustomIcon, getEmojiCodePoint, getEmojiSequenceFromString, getEmojiSequenceKeyword, getEmojiSequenceString, getEmojiUnicode, getEmojiUnicodeString, getIconCSS, getIconContentCSS, getIconData, getIcons, getIconsCSS, getIconsContentCSS, getIconsTree, getQualifiedEmojiVariations, getSVGViewBox, getUnqualifiedEmojiSequence, iconToHTML, iconToSVG, isUTF32SplitNumber, loadIcon, makeIconSquare, matchIconName, mergeCustomisations, mergeDefsAndContent, mergeIconData, mergeIconProps, mergeIconTransformations, mergeUTF32Numbers, minifyIconSet, parseEmojiTestFile, parseIconSet, parseIconSetAsync, parseSVGContent, pascalize, prepareEmojiForIconSet, prepareEmojiForIconsList, prettifySVG, quicklyValidateIconSet, replaceIDs, rotateFromString, sanitiseTitleAttribute, searchForIcon, snakelize, splitSVGDefs, splitUTF32Number, stringToColor, stringToIcon, svgToData, svgToURL, toBoolean, trimSVG, unmergeObjects, validateIconName, validateIconSet, wrapSVGContent };
+52
View File
@@ -0,0 +1,52 @@
import { defaultExtendedIconProps, defaultIconDimensions, defaultIconProps, defaultIconTransformations } from "./icon/defaults.js";
import { defaultIconCustomisations, defaultIconSizeCustomisations } from "./customisations/defaults.js";
import { mergeCustomisations } from "./customisations/merge.js";
import { toBoolean } from "./customisations/bool.js";
import { flipFromString } from "./customisations/flip.js";
import { rotateFromString } from "./customisations/rotate.js";
import { matchIconName, stringToIcon, validateIconName } from "./icon/name.js";
import { mergeIconTransformations } from "./icon/transformations.js";
import { mergeIconData } from "./icon/merge.js";
import { makeIconSquare } from "./icon/square.js";
import { getIconsTree } from "./icon-set/tree.js";
import { getIconData } from "./icon-set/get-icon.js";
import { parseIconSet, parseIconSetAsync } from "./icon-set/parse.js";
import { validateIconSet } from "./icon-set/validate.js";
import { quicklyValidateIconSet } from "./icon-set/validate-basic.js";
import { expandIconSet } from "./icon-set/expand.js";
import { minifyIconSet } from "./icon-set/minify.js";
import { getIcons } from "./icon-set/get-icons.js";
import { convertIconSetInfo } from "./icon-set/convert-info.js";
import { calculateSize } from "./svg/size.js";
import { mergeDefsAndContent, splitSVGDefs, wrapSVGContent } from "./svg/defs.js";
import { iconToSVG } from "./svg/build.js";
import { clearIDCache, replaceIDs } from "./svg/id.js";
import { svgToData, svgToURL } from "./svg/url.js";
import { encodeSvgForCss } from "./svg/encode-svg-for-css.js";
import { trimSVG } from "./svg/trim.js";
import { prettifySVG } from "./svg/pretty.js";
import { iconToHTML } from "./svg/html.js";
import { cleanUpInnerHTML } from "./svg/inner-html.js";
import { getSVGViewBox } from "./svg/viewbox.js";
import { buildParsedSVG, convertParsedSVG, parseSVGContent } from "./svg/parse.js";
import { colorKeywords } from "./colors/keywords.js";
import { colorToString, compareColors, stringToColor } from "./colors/index.js";
import { getIconCSS, getIconContentCSS } from "./css/icon.js";
import { getIconsCSS, getIconsContentCSS } from "./css/icons.js";
import { mergeIconProps } from "./loader/utils.js";
import { getCustomIcon } from "./loader/custom.js";
import { searchForIcon } from "./loader/modern.js";
import { loadIcon } from "./loader/loader.js";
import { convertEmojiSequenceToUTF16, convertEmojiSequenceToUTF32, getEmojiCodePoint, getEmojiUnicode, isUTF32SplitNumber, mergeUTF32Numbers, splitUTF32Number } from "./emoji/convert.js";
import { getEmojiSequenceFromString, getUnqualifiedEmojiSequence } from "./emoji/cleanup.js";
import { getEmojiSequenceKeyword, getEmojiSequenceString, getEmojiUnicodeString } from "./emoji/format.js";
import { parseEmojiTestFile } from "./emoji/test/parse.js";
import { getQualifiedEmojiVariations } from "./emoji/test/variations.js";
import { findMissingEmojis } from "./emoji/test/missing.js";
import { createOptimisedRegex, createOptimisedRegexForEmojiSequences } from "./emoji/regex/create.js";
import { prepareEmojiForIconSet, prepareEmojiForIconsList } from "./emoji/parse.js";
import { findAndReplaceEmojisInText } from "./emoji/replace/replace.js";
import { camelToKebab, camelize, pascalize, snakelize } from "./misc/strings.js";
import { commonObjectProps, compareObjects, unmergeObjects } from "./misc/objects.js";
import { sanitiseTitleAttribute } from "./misc/title.js";
export { buildParsedSVG, calculateSize, camelToKebab, camelize, cleanUpInnerHTML, clearIDCache, colorKeywords, colorToString, commonObjectProps, compareColors, compareObjects, convertEmojiSequenceToUTF16, convertEmojiSequenceToUTF32, convertIconSetInfo, convertParsedSVG, createOptimisedRegex, createOptimisedRegexForEmojiSequences, defaultExtendedIconProps, defaultIconCustomisations, defaultIconDimensions, defaultIconProps, defaultIconSizeCustomisations, defaultIconTransformations, encodeSvgForCss, expandIconSet, findAndReplaceEmojisInText, findMissingEmojis, flipFromString, getCustomIcon, getEmojiCodePoint, getEmojiSequenceFromString, getEmojiSequenceKeyword, getEmojiSequenceString, getEmojiUnicode, getEmojiUnicodeString, getIconCSS, getIconContentCSS, getIconData, getIcons, getIconsCSS, getIconsContentCSS, getIconsTree, getQualifiedEmojiVariations, getSVGViewBox, getUnqualifiedEmojiSequence, iconToHTML, iconToSVG, isUTF32SplitNumber, loadIcon, makeIconSquare, matchIconName, mergeCustomisations, mergeDefsAndContent, mergeIconData, mergeIconProps, mergeIconTransformations, mergeUTF32Numbers, minifyIconSet, parseEmojiTestFile, parseIconSet, parseIconSetAsync, parseSVGContent, pascalize, prepareEmojiForIconSet, prepareEmojiForIconsList, prettifySVG, quicklyValidateIconSet, replaceIDs, rotateFromString, sanitiseTitleAttribute, searchForIcon, snakelize, splitSVGDefs, splitUTF32Number, stringToColor, stringToIcon, svgToData, svgToURL, toBoolean, trimSVG, unmergeObjects, validateIconName, validateIconSet, wrapSVGContent };
+6
View File
@@ -0,0 +1,6 @@
import { CustomIconLoader, IconifyLoaderOptions, InlineCollection } from "./types.js";
/**
* Get custom icon from inline collection or using loader
*/
declare function getCustomIcon(custom: CustomIconLoader | InlineCollection, collection: string, icon: string, options?: IconifyLoaderOptions): Promise<string | undefined>;
export { getCustomIcon };
+30
View File
@@ -0,0 +1,30 @@
import { trimSVG } from "../svg/trim.js";
import { mergeIconProps } from "./utils.js";
/**
* Get custom icon from inline collection or using loader
*/
async function getCustomIcon(custom, collection, icon, options) {
let result;
try {
if (typeof custom === "function") result = await custom(icon);
else {
const inline = custom[icon];
result = typeof inline === "function" ? await inline() : inline;
}
} catch (err) {
console.warn(`Failed to load custom icon "${icon}" in "${collection}":`, err);
return;
}
if (result) {
const cleanupIdx = result.indexOf("<svg");
if (cleanupIdx > 0) result = result.slice(cleanupIdx);
const { transform } = options?.customizations ?? {};
result = typeof transform === "function" ? await transform(result, collection, icon) : result;
if (!result.startsWith("<svg")) {
console.warn(`Custom icon "${icon}" in "${collection}" is not a valid SVG`);
return result;
}
return await mergeIconProps(options?.customizations?.trimCustomSvg === true ? trimSVG(result) : result, collection, icon, options, void 0);
}
}
export { getCustomIcon };
+9
View File
@@ -0,0 +1,9 @@
import { AutoInstall, CustomIconLoader, ExternalPkgName } from "./types.js";
/**
* Creates a CustomIconLoader collection from an external package collection.
*
* @param packageName The package name.
* @param autoInstall {AutoInstall} [autoInstall=false] - whether to automatically install
*/
declare function createExternalPackageIconLoader(packageName: ExternalPkgName, autoInstall?: AutoInstall, cwd?: string): Record<string, CustomIconLoader>;
export { createExternalPackageIconLoader };
+43
View File
@@ -0,0 +1,43 @@
import { getPossibleIconNames } from "./utils.js";
import { searchForIcon } from "./modern.js";
import { warnOnce } from "./warn.js";
import { loadCollectionFromFS } from "./fs.js";
/**
* Creates a CustomIconLoader collection from an external package collection.
*
* @param packageName The package name.
* @param autoInstall {AutoInstall} [autoInstall=false] - whether to automatically install
*/
function createExternalPackageIconLoader(packageName, autoInstall = false, cwd) {
let scope;
let collection;
const collections = {};
if (typeof packageName === "string") {
if (packageName.length === 0) {
warnOnce(`invalid package name, it is empty`);
return collections;
}
if (packageName[0] === "@") {
if (packageName.indexOf("/") === -1) {
warnOnce(`invalid scoped package name "${packageName}"`);
return collections;
}
[scope, collection] = packageName.split("/");
} else {
scope = "";
collection = packageName;
}
} else [scope, collection] = packageName;
collections[collection] = createCustomIconLoader(scope, collection, autoInstall, cwd);
return collections;
}
function createCustomIconLoader(scope, collection, autoInstall, cwd) {
const iconSetPromise = loadCollectionFromFS(collection, autoInstall, scope, cwd);
return (async (icon) => {
const iconSet = await iconSetPromise;
let result;
if (iconSet) result = await searchForIcon(iconSet, collection, getPossibleIconNames(icon));
return result;
});
}
export { createExternalPackageIconLoader };
+13
View File
@@ -0,0 +1,13 @@
import { AutoInstall } from "./types.js";
import { IconifyJSON } from "@iconify/types";
/**
* Asynchronously loads a collection from the file system.
*
* @param name {string} the name of the collection, e.g. 'mdi'
* @param autoInstall {AutoInstall} [autoInstall=false] - whether to automatically install
* @param scope {string} [scope='@iconify-json'] - the scope of the collection, e.g. '@my-company-json'
* @param cwd {string} [cwd=process.cwd()] - current working directory for caching
* @return {Promise<IconifyJSON | undefined>} the loaded IconifyJSON or undefined
*/
declare function loadCollectionFromFS(name: string, autoInstall?: AutoInstall, scope?: string, cwd?: string): Promise<IconifyJSON | undefined>;
export { loadCollectionFromFS };
+53
View File
@@ -0,0 +1,53 @@
import { tryInstallPkg } from "./install-pkg.js";
import { resolvePathAsync } from "./resolve.js";
import { promises } from "fs";
import { pathToFileURL } from "node:url";
const _collections = Object.create(null);
/** Check if full package exists, per cwd value */
const isLegacyExists = Object.create(null);
/**
* Asynchronously loads a collection from the file system.
*
* @param name {string} the name of the collection, e.g. 'mdi'
* @param autoInstall {AutoInstall} [autoInstall=false] - whether to automatically install
* @param scope {string} [scope='@iconify-json'] - the scope of the collection, e.g. '@my-company-json'
* @param cwd {string} [cwd=process.cwd()] - current working directory for caching
* @return {Promise<IconifyJSON | undefined>} the loaded IconifyJSON or undefined
*/
async function loadCollectionFromFS(name, autoInstall = false, scope = "@iconify-json", cwd = process.cwd()) {
const cache = _collections[cwd] || (_collections[cwd] = Object.create(null));
if (!await cache[name]) cache[name] = task();
return cache[name];
async function task() {
const packageName = scope.length === 0 ? name : `${scope}/${name}`;
let jsonPath = await resolvePathAsync(`${packageName}/icons.json`, cwd);
if (scope === "@iconify-json") {
if (isLegacyExists[cwd] === void 0) isLegacyExists[cwd] = !!await resolvePathAsync(`@iconify/json/collections.json`, cwd);
const checkLegacy = isLegacyExists[cwd];
if (!jsonPath && checkLegacy) jsonPath = await resolvePathAsync(`@iconify/json/json/${name}.json`, cwd);
if (!jsonPath && !checkLegacy && autoInstall) {
await tryInstallPkg(packageName, autoInstall);
jsonPath = await resolvePathAsync(`${packageName}/icons.json`, cwd);
}
} else if (!jsonPath && autoInstall) {
await tryInstallPkg(packageName, autoInstall);
jsonPath = await resolvePathAsync(`${packageName}/icons.json`, cwd);
}
if (!jsonPath) {
const packagePath = await resolvePathAsync(packageName, cwd);
if (packagePath) {
const { icons } = await import(pathToFileURL(packagePath).href);
if (icons) return icons;
}
}
let stat;
try {
stat = jsonPath ? await promises.lstat(jsonPath) : void 0;
} catch (err) {
return;
}
if (stat?.isFile()) return JSON.parse(await promises.readFile(jsonPath, "utf8"));
else return;
}
}
export { loadCollectionFromFS };

Some files were not shown because too many files have changed in this diff Show More