mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-15 11:01:21 +08:00

Add parsers for the background css shorthand property, make ViewBase unit testable in node environment Add background parser and linear-gradient parser Use sticky regexes Simplify some types, introduce generic Parsed<T> instead of & TokenRange Apply each parser to return a { start, end, value } object Move the css selector parser to the css/parser and unify types Add the first steps toward building homegrown css parser Add somewhat standards compliant tokenizer, add baseline, rework and shady css parsers Enable all tests again, skip flaky perf test Improve css parser tokenizer by converting some char token types to simple string Implement 'parse a stylesheet' Add gonzales css-parser Add parseLib and css-tree perf Add a thin parser layer that will convert CSS3 tokens to values, for now output is compatible with rework Make root tsc green Return the requires of tns-core-modules to use relative paths for webpack to work Implement support for '@import 'url-string'; Fix function parser, function-token is no-longer neglected Make the style-scope be able to load from "css" and "css-ast" modules Add a loadAppCss event so theme can be added to snapshot separately from loaded
1544 lines
51 KiB
TypeScript
1544 lines
51 KiB
TypeScript
export type Parsed<V> = { start: number, end: number, value: V };
|
|
|
|
import * as reworkcss from "./reworkcss";
|
|
|
|
// Values
|
|
export type ARGB = number;
|
|
export type URL = string;
|
|
export type Angle = number;
|
|
export interface Unit<T> {
|
|
value: number;
|
|
unit: string;
|
|
}
|
|
export type Length = Unit<"px" | "dip">;
|
|
export type Percentage = Unit<"%">;
|
|
export type LengthPercentage = Length | Percentage;
|
|
export type Keyword = string;
|
|
export interface ColorStop {
|
|
argb: ARGB;
|
|
offset?: LengthPercentage;
|
|
}
|
|
export interface LinearGradient {
|
|
angle: number;
|
|
colors: ColorStop[];
|
|
}
|
|
export interface Background {
|
|
readonly color?: number;
|
|
readonly image?: URL | LinearGradient;
|
|
readonly repeat?: BackgroundRepeat;
|
|
readonly position?: BackgroundPosition;
|
|
readonly size?: BackgroundSize;
|
|
}
|
|
export type BackgroundRepeat = "repeat" | "repeat-x" | "repeat-y" | "no-repeat";
|
|
export type BackgroundSize = "auto" | "cover" | "contain" | {
|
|
x: LengthPercentage,
|
|
y: "auto" | LengthPercentage
|
|
}
|
|
export type HorizontalAlign = "left" | "center" | "right";
|
|
export type VerticalAlign = "top" | "center" | "bottom";
|
|
export interface HorizontalAlignWithOffset {
|
|
readonly align: "left" | "right";
|
|
readonly offset: LengthPercentage;
|
|
}
|
|
export interface VerticalAlignWithOffset {
|
|
readonly align: "top" | "bottom";
|
|
readonly offset: LengthPercentage
|
|
}
|
|
export interface BackgroundPosition {
|
|
readonly x: HorizontalAlign | HorizontalAlignWithOffset;
|
|
readonly y: VerticalAlign | VerticalAlignWithOffset;
|
|
}
|
|
|
|
const urlRegEx = /\s*url\((?:('|")([^\1]*)\1|([^\)]*))\)\s*/gy;
|
|
export function parseURL(text: string, start: number = 0): Parsed<URL> {
|
|
urlRegEx.lastIndex = start;
|
|
const result = urlRegEx.exec(text);
|
|
if (!result) {
|
|
return null;
|
|
}
|
|
const end = urlRegEx.lastIndex;
|
|
const value: URL = result[2] || result[3];
|
|
return { start, end, value };
|
|
}
|
|
|
|
const hexColorRegEx = /\s*#((?:[0-9A-F]{8})|(?:[0-9A-F]{6})|(?:[0-9A-F]{3}))\s*/giy;
|
|
export function parseHexColor(text: string, start: number = 0): Parsed<ARGB> {
|
|
hexColorRegEx.lastIndex = start;
|
|
const result = hexColorRegEx.exec(text);
|
|
if (!result) {
|
|
return null;
|
|
}
|
|
const end = hexColorRegEx.lastIndex;
|
|
let hex = result[1];
|
|
let argb;
|
|
if (hex.length === 8) {
|
|
argb = parseInt("0x" + hex);
|
|
} else if (hex.length === 6) {
|
|
argb = parseInt("0xFF" + hex);
|
|
} else if (hex.length === 3) {
|
|
argb = parseInt("0xFF" + hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]);
|
|
}
|
|
return { start, end, value: argb };
|
|
}
|
|
|
|
function rgbaToArgbNumber(r: number, g: number, b: number, a: number = 1): number | undefined {
|
|
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255 && a >= 0 && a <= 1) {
|
|
return (Math.round(a * 0xFF) * 0x01000000) + (r * 0x010000) + (g * 0x000100) + (b * 0x000001);
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const rgbColorRegEx = /\s*(rgb\(\s*(\d*)\s*,\s*(\d*)\s*,\s*(\d*)\s*\))/gy;
|
|
export function parseRGBColor(text: string, start: number = 0): Parsed<ARGB> {
|
|
rgbColorRegEx.lastIndex = start;
|
|
const result = rgbColorRegEx.exec(text);
|
|
if (!result) {
|
|
return null;
|
|
}
|
|
const end = rgbColorRegEx.lastIndex;
|
|
const value = result[1] && rgbaToArgbNumber(parseInt(result[2]), parseInt(result[3]), parseInt(result[4]));
|
|
return { start, end, value };
|
|
}
|
|
|
|
const rgbaColorRegEx = /\s*(rgba\(\s*(\d*)\s*,\s*(\d*)\s*,\s*(\d*)\s*,\s*([01]?\.?\d*)\s*\))/gy;
|
|
export function parseRGBAColor(text: string, start: number = 0): Parsed<ARGB> {
|
|
rgbaColorRegEx.lastIndex = start;
|
|
const result = rgbaColorRegEx.exec(text);
|
|
if (!result) {
|
|
return null;
|
|
}
|
|
const end = rgbaColorRegEx.lastIndex;
|
|
const value = rgbaToArgbNumber(parseInt(result[2]), parseInt(result[3]), parseInt(result[4]), parseFloat(result[5]));
|
|
return { start, end, value };
|
|
}
|
|
|
|
export enum colors {
|
|
transparent = 0x00000000,
|
|
aliceblue = 0xFFF0F8FF,
|
|
antiquewhite = 0xFFFAEBD7,
|
|
aqua = 0xFF00FFFF,
|
|
aquamarine = 0xFF7FFFD4,
|
|
azure = 0xFFF0FFFF,
|
|
beige = 0xFFF5F5DC,
|
|
bisque = 0xFFFFE4C4,
|
|
black = 0xFF000000,
|
|
blanchedalmond = 0xFFFFEBCD,
|
|
blue = 0xFF0000FF,
|
|
blueviolet = 0xFF8A2BE2,
|
|
brown = 0xFFA52A2A,
|
|
burlywood = 0xFFDEB887,
|
|
cadetblue = 0xFF5F9EA0,
|
|
chartreuse = 0xFF7FFF00,
|
|
chocolate = 0xFFD2691E,
|
|
coral = 0xFFFF7F50,
|
|
cornflowerblue = 0xFF6495ED,
|
|
cornsilk = 0xFFFFF8DC,
|
|
crimson = 0xFFDC143C,
|
|
cyan = 0xFF00FFFF,
|
|
darkblue = 0xFF00008B,
|
|
darkcyan = 0xFF008B8B,
|
|
darkgoldenrod = 0xFFB8860B,
|
|
darkgray = 0xFFA9A9A9,
|
|
darkgreen = 0xFF006400,
|
|
darkgrey = 0xFFA9A9A9,
|
|
darkkhaki = 0xFFBDB76B,
|
|
darkmagenta = 0xFF8B008B,
|
|
darkolivegreen = 0xFF556B2F,
|
|
darkorange = 0xFFFF8C00,
|
|
darkorchid = 0xFF9932CC,
|
|
darkred = 0xFF8B0000,
|
|
darksalmon = 0xFFE9967A,
|
|
darkseagreen = 0xFF8FBC8F,
|
|
darkslateblue = 0xFF483D8B,
|
|
darkslategray = 0xFF2F4F4F,
|
|
darkslategrey = 0xFF2F4F4F,
|
|
darkturquoise = 0xFF00CED1,
|
|
darkviolet = 0xFF9400D3,
|
|
deeppink = 0xFFFF1493,
|
|
deepskyblue = 0xFF00BFFF,
|
|
dimgray = 0xFF696969,
|
|
dimgrey = 0xFF696969,
|
|
dodgerblue = 0xFF1E90FF,
|
|
firebrick = 0xFFB22222,
|
|
floralwhite = 0xFFFFFAF0,
|
|
forestgreen = 0xFF228B22,
|
|
fuchsia = 0xFFFF00FF,
|
|
gainsboro = 0xFFDCDCDC,
|
|
ghostwhite = 0xFFF8F8FF,
|
|
gold = 0xFFFFD700,
|
|
goldenrod = 0xFFDAA520,
|
|
gray = 0xFF808080,
|
|
green = 0xFF008000,
|
|
greenyellow = 0xFFADFF2F,
|
|
grey = 0xFF808080,
|
|
honeydew = 0xFFF0FFF0,
|
|
hotpink = 0xFFFF69B4,
|
|
indianred = 0xFFCD5C5C,
|
|
indigo = 0xFF4B0082,
|
|
ivory = 0xFFFFFFF0,
|
|
khaki = 0xFFF0E68C,
|
|
lavender = 0xFFE6E6FA,
|
|
lavenderblush = 0xFFFFF0F5,
|
|
lawngreen = 0xFF7CFC00,
|
|
lemonchiffon = 0xFFFFFACD,
|
|
lightblue = 0xFFADD8E6,
|
|
lightcoral = 0xFFF08080,
|
|
lightcyan = 0xFFE0FFFF,
|
|
lightgoldenrodyellow = 0xFFFAFAD2,
|
|
lightgray = 0xFFD3D3D3,
|
|
lightgreen = 0xFF90EE90,
|
|
lightgrey = 0xFFD3D3D3,
|
|
lightpink = 0xFFFFB6C1,
|
|
lightsalmon = 0xFFFFA07A,
|
|
lightseagreen = 0xFF20B2AA,
|
|
lightskyblue = 0xFF87CEFA,
|
|
lightslategray = 0xFF778899,
|
|
lightslategrey = 0xFF778899,
|
|
lightsteelblue = 0xFFB0C4DE,
|
|
lightyellow = 0xFFFFFFE0,
|
|
lime = 0xFF00FF00,
|
|
limegreen = 0xFF32CD32,
|
|
linen = 0xFFFAF0E6,
|
|
magenta = 0xFFFF00FF,
|
|
maroon = 0xFF800000,
|
|
mediumaquamarine = 0xFF66CDAA,
|
|
mediumblue = 0xFF0000CD,
|
|
mediumorchid = 0xFFBA55D3,
|
|
mediumpurple = 0xFF9370DB,
|
|
mediumseagreen = 0xFF3CB371,
|
|
mediumslateblue = 0xFF7B68EE,
|
|
mediumspringgreen = 0xFF00FA9A,
|
|
mediumturquoise = 0xFF48D1CC,
|
|
mediumvioletred = 0xFFC71585,
|
|
midnightblue = 0xFF191970,
|
|
mintcream = 0xFFF5FFFA,
|
|
mistyrose = 0xFFFFE4E1,
|
|
moccasin = 0xFFFFE4B5,
|
|
navajowhite = 0xFFFFDEAD,
|
|
navy = 0xFF000080,
|
|
oldlace = 0xFFFDF5E6,
|
|
olive = 0xFF808000,
|
|
olivedrab = 0xFF6B8E23,
|
|
orange = 0xFFFFA500,
|
|
orangered = 0xFFFF4500,
|
|
orchid = 0xFFDA70D6,
|
|
palegoldenrod = 0xFFEEE8AA,
|
|
palegreen = 0xFF98FB98,
|
|
paleturquoise = 0xFFAFEEEE,
|
|
palevioletred = 0xFFDB7093,
|
|
papayawhip = 0xFFFFEFD5,
|
|
peachpuff = 0xFFFFDAB9,
|
|
peru = 0xFFCD853F,
|
|
pink = 0xFFFFC0CB,
|
|
plum = 0xFFDDA0DD,
|
|
powderblue = 0xFFB0E0E6,
|
|
purple = 0xFF800080,
|
|
red = 0xFFFF0000,
|
|
rosybrown = 0xFFBC8F8F,
|
|
royalblue = 0xFF4169E1,
|
|
saddlebrown = 0xFF8B4513,
|
|
salmon = 0xFFFA8072,
|
|
sandybrown = 0xFFF4A460,
|
|
seagreen = 0xFF2E8B57,
|
|
seashell = 0xFFFFF5EE,
|
|
sienna = 0xFFA0522D,
|
|
silver = 0xFFC0C0C0,
|
|
skyblue = 0xFF87CEEB,
|
|
slateblue = 0xFF6A5ACD,
|
|
slategray = 0xFF708090,
|
|
slategrey = 0xFF708090,
|
|
snow = 0xFFFFFAFA,
|
|
springgreen = 0xFF00FF7F,
|
|
steelblue = 0xFF4682B4,
|
|
tan = 0xFFD2B48C,
|
|
teal = 0xFF008080,
|
|
thistle = 0xFFD8BFD8,
|
|
tomato = 0xFFFF6347,
|
|
turquoise = 0xFF40E0D0,
|
|
violet = 0xFFEE82EE,
|
|
wheat = 0xFFF5DEB3,
|
|
white = 0xFFFFFFFF,
|
|
whitesmoke = 0xFFF5F5F5,
|
|
yellow = 0xFFFFFF00,
|
|
yellowgreen = 0xFF9ACD32
|
|
};
|
|
|
|
export function parseColorKeyword(value, start: number, keyword = parseKeyword(value, start)): Parsed<ARGB> {
|
|
if (keyword && keyword.value in colors) {
|
|
const end = keyword.end;
|
|
const value = colors[keyword.value];
|
|
return { start, end, value };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function parseColor(value: string, start: number = 0, keyword = parseKeyword(value, start)): Parsed<ARGB> {
|
|
return parseHexColor(value, start) || parseColorKeyword(value, start, keyword) || parseRGBColor(value, start) || parseRGBAColor(value, start);
|
|
}
|
|
|
|
const keywordRegEx = /\s*([a-z][\w\-]*)\s*/giy;
|
|
function parseKeyword(text: string, start: number = 0): Parsed<Keyword> {
|
|
keywordRegEx.lastIndex = start;
|
|
const result = keywordRegEx.exec(text);
|
|
if (!result) {
|
|
return null;
|
|
}
|
|
const end = keywordRegEx.lastIndex;
|
|
const value = result[1];
|
|
return { start, end, value }
|
|
}
|
|
|
|
const backgroundRepeatKeywords = new Set([ "repeat", "repeat-x", "repeat-y", "no-repeat" ]);
|
|
export function parseRepeat(value: string, start: number = 0, keyword = parseKeyword(value, start)): Parsed<BackgroundRepeat> {
|
|
if (keyword && backgroundRepeatKeywords.has(keyword.value)) {
|
|
const end = keyword.end;
|
|
const value = <BackgroundRepeat>keyword.value;
|
|
return { start, end, value };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const unitRegEx = /\s*([\+\-]?(?:\d+\.\d+|\d+|\.\d+)(?:[eE][\+\-]?\d+)?)([a-zA-Z]+|%)?\s*/gy;
|
|
export function parseUnit(text: string, start: number = 0): Parsed<Unit<string>> {
|
|
unitRegEx.lastIndex = start;
|
|
const result = unitRegEx.exec(text);
|
|
if (!result) {
|
|
return null;
|
|
}
|
|
const end = unitRegEx.lastIndex;
|
|
const value = parseFloat(result[1]);
|
|
const unit = <any>result[2] || "dip";
|
|
return { start, end, value: { value, unit }};
|
|
}
|
|
|
|
export function parsePercentageOrLength(text: string, start: number = 0): Parsed<LengthPercentage> {
|
|
const unitResult = parseUnit(text, start);
|
|
if (unitResult) {
|
|
const { start, end } = unitResult;
|
|
const value = <LengthPercentage>unitResult.value;
|
|
if (value.unit === "%") {
|
|
value.value /= 100;
|
|
} else if (!value.unit) {
|
|
value.unit = "dip";
|
|
} else if (value.unit === "px" || value.unit === "dip") {
|
|
// same
|
|
} else {
|
|
return null;
|
|
}
|
|
return { start, end, value };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const angleUnitsToRadMap: { [unit: string]: (start: number, end: number, value: number) => Parsed<Angle> } = {
|
|
"deg": (start: number, end: number, deg: number) => ({ start, end, value: deg / 180 * Math.PI }),
|
|
"rad": (start: number, end: number, rad: number) => ({ start, end, value: rad }),
|
|
"grad": (start: number, end: number, grad: number) => ({ start, end, value: grad / 200 * Math.PI }),
|
|
"turn": (start: number, end: number, turn: number) => ({ start, end, value: turn * Math.PI * 2 })
|
|
}
|
|
export function parseAngle(value: string, start: number = 0): Parsed<Angle> {
|
|
const angleResult = parseUnit(value, start);
|
|
if (angleResult) {
|
|
const { start, end, value } = angleResult;
|
|
return (angleUnitsToRadMap[value.unit] || (() => null))(start, end, value.value);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const backgroundSizeKeywords = new Set(["auto", "contain", "cover"]);
|
|
export function parseBackgroundSize(value: string, start: number = 0, keyword = parseKeyword(value, start)): Parsed<BackgroundSize> {
|
|
let end = start;
|
|
if (keyword && backgroundSizeKeywords.has(keyword.value)) {
|
|
end = keyword.end;
|
|
const value = <"auto" | "cover" | "contain">keyword.value;
|
|
return { start, end, value };
|
|
}
|
|
|
|
// Parse one or two lengths... the other will be "auto"
|
|
const firstLength = parsePercentageOrLength(value, end);
|
|
if (firstLength) {
|
|
end = firstLength.end;
|
|
const secondLength = parsePercentageOrLength(value, firstLength.end);
|
|
if (secondLength) {
|
|
end = secondLength.end;
|
|
return { start, end, value: { x: firstLength.value, y: secondLength.value }};
|
|
} else {
|
|
return { start, end, value: { x: firstLength.value, y: "auto" }};
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const backgroundPositionKeywords = Object.freeze(new Set([ "left", "right", "top", "bottom", "center" ]));
|
|
const backgroundPositionKeywordsDirection: {[align: string]: "x" | "center" | "y" } = {
|
|
"left": "x",
|
|
"right": "x",
|
|
"center": "center",
|
|
"top": "y",
|
|
"bottom": "y"
|
|
}
|
|
export function parseBackgroundPosition(text: string, start: number = 0, keyword = parseKeyword(text, start)): Parsed<BackgroundPosition> {
|
|
function formatH(align: Parsed<HorizontalAlign>, offset: Parsed<LengthPercentage>) {
|
|
if (align.value === "center") {
|
|
return "center";
|
|
}
|
|
if (offset && offset.value.value !== 0) {
|
|
return { align: align.value, offset: offset.value };
|
|
}
|
|
return align.value;
|
|
}
|
|
function formatV(align: Parsed<VerticalAlign>, offset: Parsed<LengthPercentage>) {
|
|
if (align.value === "center") {
|
|
return "center";
|
|
}
|
|
if (offset && offset.value.value !== 0) {
|
|
return { align: align.value, offset: offset.value };
|
|
}
|
|
return align.value;
|
|
}
|
|
let end = start;
|
|
if (keyword && backgroundPositionKeywords.has(keyword.value)) {
|
|
end = keyword.end;
|
|
let firstDirection = backgroundPositionKeywordsDirection[keyword.value];
|
|
|
|
const firstLength = firstDirection !== "center" && parsePercentageOrLength(text, end);
|
|
if (firstLength) {
|
|
end = firstLength.end;
|
|
}
|
|
|
|
const secondKeyword = parseKeyword(text, end);
|
|
if (secondKeyword && backgroundPositionKeywords.has(secondKeyword.value)) {
|
|
end = secondKeyword.end;
|
|
let secondDirection = backgroundPositionKeywordsDirection[secondKeyword.end];
|
|
|
|
if (firstDirection === secondDirection && firstDirection !== "center") {
|
|
return null; // Reject pair of both horizontal or both vertical alignments.
|
|
}
|
|
|
|
const secondLength = secondDirection !== "center" && parsePercentageOrLength(text, end);
|
|
if (secondLength) {
|
|
end = secondLength.end;
|
|
}
|
|
|
|
if ((firstDirection === secondDirection && secondDirection === "center") || (firstDirection === "x" || secondDirection === "y")) {
|
|
return { start, end, value: {
|
|
x: formatH(<Parsed<HorizontalAlign>>keyword, firstLength),
|
|
y: formatV(<Parsed<VerticalAlign>>secondKeyword, secondLength)
|
|
}};
|
|
} else {
|
|
return { start, end, value: {
|
|
x: formatH(<Parsed<HorizontalAlign>>secondKeyword, secondLength),
|
|
y: formatV(<Parsed<VerticalAlign>>keyword, firstLength),
|
|
}};
|
|
}
|
|
} else {
|
|
if (firstDirection === "center") {
|
|
return { start, end, value: { x: "center", y: "center" }};
|
|
} else if (firstDirection === "x") {
|
|
return { start, end, value: { x: formatH(<Parsed<HorizontalAlign>>keyword, firstLength), y: "center" }};
|
|
} else {
|
|
return { start, end, value: { x: "center", y: formatV(<Parsed<VerticalAlign>>keyword, firstLength) }};
|
|
}
|
|
}
|
|
} else {
|
|
const firstLength = parsePercentageOrLength(text, end);
|
|
if (firstLength) {
|
|
end = firstLength.end;
|
|
const secondLength = parsePercentageOrLength(text, end);
|
|
if (secondLength) {
|
|
end = secondLength.end;
|
|
return { start, end, value: { x: { align: "left", offset: firstLength.value }, y: { align: "top", offset: secondLength.value }}};
|
|
} else {
|
|
return { start, end, value: { x: { align: "left", offset: firstLength.value }, y: "center" }};
|
|
}
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
const directionRegEx = /\s*to\s*(left|right|top|bottom)\s*(left|right|top|bottom)?\s*/gy;
|
|
const sideDirections = {
|
|
top: Math.PI * 0/2,
|
|
right: Math.PI * 1/2,
|
|
bottom: Math.PI * 2/2,
|
|
left: Math.PI * 3/2
|
|
}
|
|
const cornerDirections = {
|
|
top: {
|
|
right: Math.PI * 1/4,
|
|
left: Math.PI * 7/4
|
|
},
|
|
right: {
|
|
top: Math.PI * 1/4,
|
|
bottom: Math.PI * 3/4
|
|
},
|
|
bottom: {
|
|
right: Math.PI * 3/4,
|
|
left: Math.PI * 5/4
|
|
},
|
|
left: {
|
|
top: Math.PI * 7/4,
|
|
bottom: Math.PI * 5/4
|
|
}
|
|
}
|
|
function parseDirection(text: string, start: number = 0): Parsed<Angle> {
|
|
directionRegEx.lastIndex = start;
|
|
const result = directionRegEx.exec(text);
|
|
if (!result) {
|
|
return null;
|
|
}
|
|
const end = directionRegEx.lastIndex;
|
|
const firstDirection = result[1];
|
|
if (result[2]) {
|
|
const secondDirection = result[2];
|
|
const value = cornerDirections[firstDirection][secondDirection];
|
|
return value === undefined ? null : { start, end, value };
|
|
} else {
|
|
return { start, end, value: sideDirections[firstDirection] }
|
|
}
|
|
}
|
|
|
|
const openingBracketRegEx = /\s*\(\s*/gy;
|
|
const closingBracketRegEx = /\s*\)\s*/gy;
|
|
const closingBracketOrCommaRegEx = /\s*(\)|,)\s*/gy;
|
|
function parseArgumentsList<T>(text: string, start: number, argument: (value: string, lastIndex: number, index: number) => Parsed<T>): Parsed<Parsed<T>[]> {
|
|
openingBracketRegEx.lastIndex = start;
|
|
const openingBracket = openingBracketRegEx.exec(text);
|
|
if (!openingBracket) {
|
|
return null;
|
|
}
|
|
let end = openingBracketRegEx.lastIndex;
|
|
const value: Parsed<T>[] = [];
|
|
|
|
closingBracketRegEx.lastIndex = end;
|
|
const closingBracket = closingBracketRegEx.exec(text);
|
|
if (closingBracket) {
|
|
return { start, end, value };
|
|
}
|
|
|
|
for(var index = 0; true; index++) {
|
|
const arg = argument(text, end, index);
|
|
if (!arg) {
|
|
return null;
|
|
}
|
|
end = arg.end;
|
|
value.push(arg);
|
|
|
|
closingBracketOrCommaRegEx.lastIndex = end;
|
|
const closingBracketOrComma = closingBracketOrCommaRegEx.exec(text);
|
|
if (closingBracketOrComma) {
|
|
end = closingBracketOrCommaRegEx.lastIndex;
|
|
if (closingBracketOrComma[1] === ",") {
|
|
continue;
|
|
} else if (closingBracketOrComma[1] === ")") {
|
|
return { start, end, value };
|
|
}
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function parseColorStop(text: string, start: number = 0): Parsed<ColorStop> {
|
|
const color = parseColor(text, start);
|
|
if (!color) {
|
|
return null;
|
|
}
|
|
let end = color.end;
|
|
const offset = parsePercentageOrLength(text, end);
|
|
if (offset) {
|
|
end = offset.end;
|
|
return { start, end, value: { argb: color.value, offset: offset.value }};
|
|
}
|
|
return { start, end, value: { argb: color.value }};
|
|
}
|
|
|
|
const linearGradientStartRegEx = /\s*linear-gradient\s*/gy;
|
|
export function parseLinearGradient(text: string, start: number = 0): Parsed<LinearGradient> {
|
|
linearGradientStartRegEx.lastIndex = start;
|
|
const lgs = linearGradientStartRegEx.exec(text);
|
|
if (!lgs) {
|
|
return null;
|
|
}
|
|
let end = linearGradientStartRegEx.lastIndex;
|
|
|
|
let angle = Math.PI;
|
|
const colors: ColorStop[] = [];
|
|
|
|
const parsedArgs = parseArgumentsList<Angle | ColorStop>(text, end, (text, start, index) => {
|
|
if (index === 0) {
|
|
// First arg can be gradient direction
|
|
const angleArg = parseAngle(text, start) || parseDirection(text, start);
|
|
if (angleArg) {
|
|
angle = angleArg.value;
|
|
return angleArg;
|
|
}
|
|
}
|
|
|
|
const colorStop = parseColorStop(text, start);
|
|
if (colorStop) {
|
|
colors.push(colorStop.value);
|
|
return colorStop;
|
|
}
|
|
|
|
return null;
|
|
});
|
|
if (!parsedArgs) {
|
|
return null;
|
|
}
|
|
end = parsedArgs.end;
|
|
|
|
return { start, end, value: { angle, colors }};
|
|
}
|
|
|
|
const slashRegEx = /\s*(\/)\s*/gy;
|
|
function parseSlash(text: string, start: number): Parsed<"/"> {
|
|
slashRegEx.lastIndex = start;
|
|
const slash = slashRegEx.exec(text);
|
|
if (!slash) {
|
|
return null;
|
|
}
|
|
const end = slashRegEx.lastIndex;
|
|
return { start, end, value: "/" };
|
|
}
|
|
|
|
export function parseBackground(text: string, start: number = 0): Parsed<Background> {
|
|
const value: any = {};
|
|
let end = start;
|
|
while(end < text.length) {
|
|
const keyword = parseKeyword(text, end);
|
|
const color = parseColor(text, end, keyword);
|
|
if (color) {
|
|
value.color = color.value;
|
|
end = color.end;
|
|
continue;
|
|
}
|
|
const repeat = parseRepeat(text, end, keyword);
|
|
if (repeat) {
|
|
value.repeat = repeat.value;
|
|
end = repeat.end;
|
|
continue;
|
|
}
|
|
const position = parseBackgroundPosition(text, end, keyword);
|
|
if (position) {
|
|
value.position = position.value;
|
|
end = position.end;
|
|
|
|
const slash = parseSlash(text, end);
|
|
if (slash) {
|
|
end = slash.end;
|
|
const size = parseBackgroundSize(text, end);
|
|
if (!size) {
|
|
// Found / but no proper size following
|
|
return null;
|
|
}
|
|
value.size = size.value;
|
|
end = size.end;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const url = parseURL(text, end);
|
|
if (url) {
|
|
value.image = url.value;
|
|
end = url.end;
|
|
continue;
|
|
}
|
|
const gradient = parseLinearGradient(text, end);
|
|
if (gradient) {
|
|
value.image = gradient.value;
|
|
end = gradient.end;
|
|
continue;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
return { start, end, value };
|
|
}
|
|
|
|
// Selectors
|
|
|
|
export type Combinator = "+" | "~" | ">" | " ";
|
|
|
|
export interface UniversalSelector {
|
|
type: "*";
|
|
}
|
|
export interface TypeSelector {
|
|
type: "";
|
|
identifier: string;
|
|
}
|
|
export interface ClassSelector {
|
|
type: ".";
|
|
identifier: string;
|
|
}
|
|
export interface IdSelector {
|
|
type: "#";
|
|
identifier: string;
|
|
}
|
|
export interface PseudoClassSelector {
|
|
type: ":";
|
|
identifier: string;
|
|
}
|
|
export type AttributeSelectorTest = "=" | "^=" | "$=" | "*=" | "=" | "~=" | "|=";
|
|
export interface AttributeSelector {
|
|
type: "[]";
|
|
property: string;
|
|
test?: AttributeSelectorTest;
|
|
value?: string;
|
|
}
|
|
|
|
export type SimpleSelector = UniversalSelector | TypeSelector | ClassSelector | IdSelector | PseudoClassSelector | AttributeSelector;
|
|
export type SimpleSelectorSequence = SimpleSelector[];
|
|
export type Selector = [SimpleSelectorSequence, Combinator];
|
|
|
|
const universalSelectorRegEx = /\*/gy;
|
|
export function parseUniversalSelector(text: string, start: number = 0): Parsed<UniversalSelector> {
|
|
universalSelectorRegEx.lastIndex = start;
|
|
const result = universalSelectorRegEx.exec(text);
|
|
if (!result) {
|
|
return null;
|
|
}
|
|
const end = universalSelectorRegEx.lastIndex;
|
|
return { start, end, value: { type: "*" }};
|
|
}
|
|
|
|
const simpleIdentifierSelectorRegEx = /(#|\.|:|\b)([_-\w][_-\w\d]*)/gy;
|
|
export function parseSimpleIdentifierSelector(text: string, start: number = 0): Parsed<TypeSelector | ClassSelector | IdSelector | PseudoClassSelector> {
|
|
simpleIdentifierSelectorRegEx.lastIndex = start;
|
|
const result = simpleIdentifierSelectorRegEx.exec(text);
|
|
if (!result) {
|
|
return null;
|
|
}
|
|
const end = simpleIdentifierSelectorRegEx.lastIndex;
|
|
const type = <"#" | "." | ":" | "">result[1];
|
|
const identifier: string = result[2];
|
|
const value = <TypeSelector | ClassSelector | IdSelector | PseudoClassSelector>{ type, identifier };
|
|
return { start, end, value };
|
|
}
|
|
|
|
const attributeSelectorRegEx = /\[\s*([_-\w][_-\w\d]*)\s*(?:(=|\^=|\$=|\*=|\~=|\|=)\s*(?:([_-\w][_-\w\d]*)|"((?:[^\\"]|\\(?:"|n|r|f|\\|0-9a-f))*)"|'((?:[^\\']|\\(?:'|n|r|f|\\|0-9a-f))*)')\s*)?\]/gy;
|
|
export function parseAttributeSelector(text: string, start: number): Parsed<AttributeSelector> {
|
|
attributeSelectorRegEx.lastIndex = start;
|
|
const result = attributeSelectorRegEx.exec(text);
|
|
if (!result) {
|
|
return null;
|
|
}
|
|
const end = attributeSelectorRegEx.lastIndex;
|
|
const property = result[1];
|
|
if (result[2]) {
|
|
const test = <AttributeSelectorTest>result[2];
|
|
const value = result[3] || result[4] || result[5];
|
|
return { start, end, value: { type: "[]", property, test, value }};
|
|
}
|
|
return { start, end, value: { type: "[]", property }};
|
|
}
|
|
|
|
export function parseSimpleSelector(text: string, start: number = 0): Parsed<SimpleSelector> {
|
|
return parseUniversalSelector(text, start) ||
|
|
parseSimpleIdentifierSelector(text, start) ||
|
|
parseAttributeSelector(text, start);
|
|
}
|
|
|
|
export function parseSimpleSelectorSequence(text: string, start: number): Parsed<SimpleSelector[]> {
|
|
let simpleSelector = parseSimpleSelector(text, start);
|
|
if (!simpleSelector) {
|
|
return null;
|
|
}
|
|
let end = simpleSelector.end;
|
|
let value = <SimpleSelectorSequence>[];
|
|
while(simpleSelector) {
|
|
value.push(simpleSelector.value);
|
|
end = simpleSelector.end;
|
|
simpleSelector = parseSimpleSelector(text, end);
|
|
}
|
|
return { start, end, value }
|
|
}
|
|
|
|
const combinatorRegEx = /\s*(\+|~|>)?\s*/gy;
|
|
export function parseCombinator(text: string, start: number = 0): Parsed<Combinator> {
|
|
combinatorRegEx.lastIndex = start;
|
|
const result = combinatorRegEx.exec(text);
|
|
if (!result) {
|
|
return null;
|
|
}
|
|
const end = combinatorRegEx.lastIndex;
|
|
const value = <Combinator>result[1] || " ";
|
|
return { start, end, value }
|
|
}
|
|
|
|
const whiteSpaceRegEx = /\s*/gy;
|
|
export function parseSelector(text: string, start: number = 0): Parsed<Selector> {
|
|
let end = start;
|
|
whiteSpaceRegEx.lastIndex = end;
|
|
const leadingWhiteSpace = whiteSpaceRegEx.exec(text);
|
|
if (leadingWhiteSpace) {
|
|
end = whiteSpaceRegEx.lastIndex;
|
|
}
|
|
let value = <Selector>[];
|
|
let combinator: Parsed<Combinator>;
|
|
let expectSimpleSelector = true; // Must have at least one
|
|
do {
|
|
const simpleSelectorSequence = parseSimpleSelectorSequence(text, end);
|
|
if (!simpleSelectorSequence) {
|
|
if (expectSimpleSelector) {
|
|
return null;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
end = simpleSelectorSequence.end;
|
|
if (combinator) {
|
|
value.push(combinator.value);
|
|
}
|
|
value.push(simpleSelectorSequence.value);
|
|
combinator = parseCombinator(text, end);
|
|
if (combinator) {
|
|
end = combinator.end;
|
|
}
|
|
expectSimpleSelector = combinator && combinator.value !== " "; // Simple selector must follow non trailing white space combinator
|
|
} while(combinator);
|
|
value.push(undefined);
|
|
return { start, end, value };
|
|
}
|
|
|
|
export interface Stylesheet {
|
|
rules: Rule[];
|
|
}
|
|
export type Rule = QualifiedRule | AtRule;
|
|
|
|
export interface AtRule {
|
|
type: "at-rule";
|
|
name: string;
|
|
prelude: InputToken[];
|
|
block: SimpleBlock;
|
|
}
|
|
export interface QualifiedRule {
|
|
type: "qualified-rule";
|
|
prelude: InputToken[];
|
|
block: SimpleBlock;
|
|
}
|
|
|
|
const whitespaceRegEx = /[\s\t\n\r\f]*/gym;
|
|
|
|
const singleQuoteStringRegEx = /'((?:[^\n\r\f\']|\\(?:\$|\n|[0-9a-fA-F]{1,6}\s?))*)(:?'|$)/gym; // Besides $n, parse escape
|
|
const doubleQuoteStringRegEx = /"((?:[^\n\r\f\"]|\\(?:\$|\n|[0-9a-fA-F]{1,6}\s?))*)(:?"|$)/gym; // Besides $n, parse escape
|
|
|
|
const commentRegEx = /(\/\*(?:[^\*]|\*[^\/])*\*\/)/gym;
|
|
const numberRegEx = /[\+\-]?(?:\d+\.\d+|\d+|\.\d+)(?:[eE][\+\-]?\d+)?/gym;
|
|
const nameRegEx = /-?(?:(?:[a-zA-Z_]|[^\x00-\x7F]|\\(?:\$|\n|[0-9a-fA-F]{1,6}\s?))(?:[a-zA-Z_0-9\-]*|\\(?:\$|\n|[0-9a-fA-F]{1,6}\s?))*)/gym;
|
|
const nonQuoteURLRegEx = /(:?[^\)\s\t\n\r\f\'\"\(]|\\(?:\$|\n|[0-9a-fA-F]{1,6}\s?))*/gym; // TODO: non-printable code points omitted
|
|
|
|
type InputToken = "(" | ")" | "{" | "}" | "[" | "]" | ":" | ";" | "," | " " | "^=" | "|=" | "$=" | "*=" | "~=" | "<!--" | "-->" | undefined /* <EOF-token> */ | InputTokenObject | FunctionInputToken | FunctionToken | SimpleBlock | AtKeywordToken;
|
|
|
|
export const enum TokenObjectType {
|
|
/**
|
|
* <string-token>
|
|
*/
|
|
string = 1,
|
|
/**
|
|
* <delim-token>
|
|
*/
|
|
delim = 2,
|
|
/**
|
|
* <number-token>
|
|
*/
|
|
number = 3,
|
|
/**
|
|
* <percentage-token>
|
|
*/
|
|
percentage = 4,
|
|
/**
|
|
* <dimension-token>
|
|
*/
|
|
dimension = 5,
|
|
/**
|
|
* <ident-token>
|
|
*/
|
|
ident = 6,
|
|
/**
|
|
* <url-token>
|
|
*/
|
|
url = 7,
|
|
/**
|
|
* <function-token>
|
|
* This is a token indicating a function's leading: <ident-token>(
|
|
*/
|
|
functionToken = 8,
|
|
/**
|
|
* <simple-block>
|
|
*/
|
|
simpleBlock = 9,
|
|
/**
|
|
* <comment-token>
|
|
*/
|
|
comment = 10,
|
|
/**
|
|
* <at-keyword-token>
|
|
*/
|
|
atKeyword = 11,
|
|
/**
|
|
* <hash-token>
|
|
*/
|
|
hash = 12,
|
|
/**
|
|
* <function>
|
|
* This is a complete consumed function: <function-token>([<component-value> [, <component-value>]*])")"
|
|
*/
|
|
function = 14,
|
|
}
|
|
|
|
interface InputTokenObject {
|
|
type: TokenObjectType;
|
|
text: string;
|
|
}
|
|
|
|
/**
|
|
* This is a "<ident>(" token.
|
|
*/
|
|
interface FunctionInputToken extends InputTokenObject {
|
|
name: string;
|
|
}
|
|
|
|
/**
|
|
* This is a completely parsed function like "<ident>([component [, component]*])".
|
|
*/
|
|
interface FunctionToken extends FunctionInputToken {
|
|
components: any[];
|
|
}
|
|
|
|
interface SimpleBlock extends InputTokenObject {
|
|
associatedToken: InputToken;
|
|
values: InputToken[];
|
|
}
|
|
|
|
interface AtKeywordToken extends InputTokenObject {}
|
|
|
|
/**
|
|
* CSS parser following relatively close:
|
|
* CSS Syntax Module Level 3
|
|
* https://www.w3.org/TR/css-syntax-3/
|
|
*/
|
|
export class CSS3Parser {
|
|
private nextInputCodePointIndex = 0;
|
|
private reconsumedInputToken: InputToken;
|
|
private topLevelFlag: boolean;
|
|
|
|
constructor(private text: string) {}
|
|
|
|
/**
|
|
* For testing purposes.
|
|
* This method allows us to run and assert the proper working of the tokenizer.
|
|
*/
|
|
tokenize(): InputToken[] {
|
|
let tokens: InputToken[] = [];
|
|
let inputToken: InputToken;
|
|
do {
|
|
inputToken = this.consumeAToken();
|
|
tokens.push(inputToken);
|
|
} while(inputToken);
|
|
return tokens;
|
|
}
|
|
|
|
/**
|
|
* 4.3.1. Consume a token
|
|
* https://www.w3.org/TR/css-syntax-3/#consume-a-token
|
|
*/
|
|
private consumeAToken(): InputToken {
|
|
if (this.reconsumedInputToken) {
|
|
let result = this.reconsumedInputToken;
|
|
this.reconsumedInputToken = null;
|
|
return result;
|
|
}
|
|
const char = this.text[this.nextInputCodePointIndex];
|
|
switch(char) {
|
|
case "\"": return this.consumeAStringToken();
|
|
case "'": return this.consumeAStringToken();
|
|
case "(":
|
|
case ")":
|
|
case ",":
|
|
case ":":
|
|
case ";":
|
|
case "[":
|
|
case "]":
|
|
case "{":
|
|
case "}":
|
|
this.nextInputCodePointIndex++;
|
|
return <any>char;
|
|
case "#": return this.consumeAHashToken() || this.consumeADelimToken();
|
|
case " ":
|
|
case "\t":
|
|
case "\n":
|
|
case "\r":
|
|
case "\f":
|
|
return this.consumeAWhitespace();
|
|
case "@": return this.consumeAtKeyword() || this.consumeADelimToken();
|
|
// TODO: Only if this is valid escape, otherwise it is a parse error
|
|
case "\\": return this.consumeAnIdentLikeToken() || this.consumeADelimToken();
|
|
case "0":
|
|
case "1":
|
|
case "2":
|
|
case "3":
|
|
case "4":
|
|
case "5":
|
|
case "6":
|
|
case "7":
|
|
case "8":
|
|
case "9":
|
|
return this.consumeANumericToken();
|
|
case "u":
|
|
case "U":
|
|
if (this.text[this.nextInputCodePointIndex + 1] === "+") {
|
|
const thirdChar = this.text[this.nextInputCodePointIndex + 2];
|
|
if (thirdChar >= '0' && thirdChar <= '9' || thirdChar === "?") {
|
|
// TODO: Handle unicode stuff such as U+002B
|
|
throw new Error("Unicode tokens not supported!");
|
|
}
|
|
}
|
|
return this.consumeAnIdentLikeToken() || this.consumeADelimToken();
|
|
case "$":
|
|
case "*":
|
|
case "^":
|
|
case "|":
|
|
case "~":
|
|
return this.consumeAMatchToken() || this.consumeADelimToken();
|
|
case "-": return this.consumeANumericToken() || this.consumeAnIdentLikeToken() || this.consumeCDC() || this.consumeADelimToken();
|
|
case "+":
|
|
case ".":
|
|
return this.consumeANumericToken() || this.consumeADelimToken();
|
|
case "/": return this.consumeAComment() || this.consumeADelimToken();
|
|
case "<": return this.consumeCDO() || this.consumeADelimToken();
|
|
case undefined: return undefined;
|
|
default: return this.consumeAnIdentLikeToken() || this.consumeADelimToken();
|
|
}
|
|
}
|
|
|
|
private consumeADelimToken(): InputToken {
|
|
return { type: TokenObjectType.delim, text: this.text[this.nextInputCodePointIndex++] };
|
|
}
|
|
|
|
private consumeAWhitespace(): InputToken {
|
|
whitespaceRegEx.lastIndex = this.nextInputCodePointIndex;
|
|
const result = whitespaceRegEx.exec(this.text);
|
|
this.nextInputCodePointIndex = whitespaceRegEx.lastIndex;
|
|
return " ";
|
|
}
|
|
|
|
private consumeAHashToken(): InputTokenObject {
|
|
this.nextInputCodePointIndex++;
|
|
let hashName = this.consumeAName();
|
|
if (hashName) {
|
|
return { type: TokenObjectType.hash, text: "#" + hashName.text };
|
|
}
|
|
this.nextInputCodePointIndex--;
|
|
return null;
|
|
}
|
|
|
|
private consumeCDO(): "<!--" | null {
|
|
if (this.text.substr(this.nextInputCodePointIndex, 4) === "<!--") {
|
|
this.nextInputCodePointIndex += 4;
|
|
return "<!--";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private consumeCDC(): "-->" | null {
|
|
if (this.text.substr(this.nextInputCodePointIndex, 3) === "-->") {
|
|
this.nextInputCodePointIndex += 3;
|
|
return "-->";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private consumeAMatchToken(): "*=" | "$=" | "|=" | "~=" | "^=" | null {
|
|
if (this.text[this.nextInputCodePointIndex + 1] === "=") {
|
|
const token = this.text.substr(this.nextInputCodePointIndex, 2);
|
|
this.nextInputCodePointIndex += 2
|
|
return <"*=" | "$=" | "|=" | "~=" | "^=">token;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 4.3.2. Consume a numeric token
|
|
* https://www.w3.org/TR/css-syntax-3/#consume-a-numeric-token
|
|
*/
|
|
private consumeANumericToken(): InputToken {
|
|
numberRegEx.lastIndex = this.nextInputCodePointIndex;
|
|
const result = numberRegEx.exec(this.text);
|
|
if (!result) {
|
|
return null;
|
|
}
|
|
this.nextInputCodePointIndex = numberRegEx.lastIndex;
|
|
if (this.text[this.nextInputCodePointIndex] === "%") {
|
|
return { type: TokenObjectType.percentage, text: result[0] }; // TODO: Push the actual number and unit here...
|
|
}
|
|
|
|
const name = this.consumeAName();
|
|
if (name) {
|
|
return { type: TokenObjectType.dimension, text: result[0] + name.text };
|
|
}
|
|
|
|
return { type: TokenObjectType.number, text: result[0] };
|
|
}
|
|
|
|
/**
|
|
* 4.3.3. Consume an ident-like token
|
|
* https://www.w3.org/TR/css-syntax-3/#consume-an-ident-like-token
|
|
*/
|
|
private consumeAnIdentLikeToken(): InputToken {
|
|
const name = this.consumeAName();
|
|
if (!name) {
|
|
return null;
|
|
}
|
|
if (this.text[this.nextInputCodePointIndex] === "(") {
|
|
this.nextInputCodePointIndex++;
|
|
if (name.text.toLowerCase() === "url") {
|
|
return this.consumeAURLToken();
|
|
}
|
|
return <FunctionInputToken>{ type: TokenObjectType.functionToken, name: name.text, text: name.text + "(" };
|
|
}
|
|
return name;
|
|
}
|
|
|
|
/**
|
|
* 4.3.4. Consume a string token
|
|
* https://www.w3.org/TR/css-syntax-3/#consume-a-string-token
|
|
*/
|
|
private consumeAStringToken(): InputTokenObject {
|
|
const char = this.text[this.nextInputCodePointIndex];
|
|
let result: RegExpExecArray;
|
|
if (char === "'") {
|
|
singleQuoteStringRegEx.lastIndex = this.nextInputCodePointIndex;
|
|
result = singleQuoteStringRegEx.exec(this.text);
|
|
if (!result) {
|
|
return null;
|
|
}
|
|
this.nextInputCodePointIndex = singleQuoteStringRegEx.lastIndex;
|
|
} else if (char === "\"") {
|
|
doubleQuoteStringRegEx.lastIndex = this.nextInputCodePointIndex;
|
|
result = doubleQuoteStringRegEx.exec(this.text);
|
|
if (!result) {
|
|
return null;
|
|
}
|
|
this.nextInputCodePointIndex = doubleQuoteStringRegEx.lastIndex;
|
|
}
|
|
|
|
// TODO: Handle bad-string.
|
|
// TODO: Perform string escaping.
|
|
return { type: TokenObjectType.string, text: result[0] };
|
|
}
|
|
|
|
/**
|
|
* 4.3.5. Consume a url token
|
|
* https://www.w3.org/TR/css-syntax-3/#consume-a-url-token
|
|
*/
|
|
private consumeAURLToken(): InputToken {
|
|
const start = this.nextInputCodePointIndex - 3 /* url */ - 1 /* ( */;
|
|
const urlToken: InputToken = { type: TokenObjectType.url, text: undefined };
|
|
this.consumeAWhitespace();
|
|
if (this.nextInputCodePointIndex >= this.text.length) {
|
|
return urlToken;
|
|
}
|
|
const nextInputCodePoint = this.text[this.nextInputCodePointIndex];
|
|
if (nextInputCodePoint === "\"" || nextInputCodePoint === "'") {
|
|
const stringToken = this.consumeAStringToken();
|
|
// TODO: Handle bad-string.
|
|
// TODO: Set value instead.
|
|
urlToken.text = stringToken.text;
|
|
this.consumeAWhitespace();
|
|
if (this.text[this.nextInputCodePointIndex] === ")" || this.nextInputCodePointIndex >= this.text.length) {
|
|
this.nextInputCodePointIndex++;
|
|
const end = this.nextInputCodePointIndex;
|
|
urlToken.text = this.text.substring(start, end);
|
|
return urlToken;
|
|
} else {
|
|
// TODO: Handle bad-url.
|
|
return null;
|
|
}
|
|
}
|
|
|
|
while(this.nextInputCodePointIndex < this.text.length) {
|
|
const char = this.text[this.nextInputCodePointIndex++];
|
|
switch(char) {
|
|
case ")": return urlToken;
|
|
case " ":
|
|
case "\t":
|
|
case "\n":
|
|
case "\r":
|
|
case "\f":
|
|
this.consumeAWhitespace();
|
|
if (this.text[this.nextInputCodePointIndex] === ")") {
|
|
this.nextInputCodePointIndex++;
|
|
return urlToken;
|
|
} else {
|
|
// TODO: Bar url! Consume remnants.
|
|
return null;
|
|
}
|
|
case "\"":
|
|
case "\'":
|
|
// TODO: Parse error! Bar url! Consume remnants.
|
|
return null;
|
|
case "\\":
|
|
// TODO: Escape!
|
|
throw new Error("Escaping not yet supported!");
|
|
default:
|
|
// TODO: Non-printable chars - error.
|
|
urlToken.text += char;
|
|
}
|
|
}
|
|
return urlToken;
|
|
}
|
|
|
|
/**
|
|
* 4.3.11. Consume a name
|
|
* https://www.w3.org/TR/css-syntax-3/#consume-a-name
|
|
*/
|
|
private consumeAName(): InputTokenObject {
|
|
nameRegEx.lastIndex = this.nextInputCodePointIndex;
|
|
const result = nameRegEx.exec(this.text);
|
|
if (!result) {
|
|
return null;
|
|
}
|
|
this.nextInputCodePointIndex = nameRegEx.lastIndex;
|
|
// TODO: Perform string escaping.
|
|
return { type: TokenObjectType.ident, text: result[0] };
|
|
}
|
|
|
|
private consumeAtKeyword(): InputTokenObject {
|
|
this.nextInputCodePointIndex++;
|
|
let name = this.consumeAName();
|
|
if (name) {
|
|
return { type: TokenObjectType.atKeyword, text: name.text };
|
|
}
|
|
this.nextInputCodePointIndex--;
|
|
return null;
|
|
}
|
|
|
|
private consumeAComment(): InputToken {
|
|
if (this.text[this.nextInputCodePointIndex + 1] === "*") {
|
|
commentRegEx.lastIndex = this.nextInputCodePointIndex;
|
|
const result = commentRegEx.exec(this.text);
|
|
if (!result) {
|
|
return null; // TODO: Handle <bad-comment>
|
|
}
|
|
this.nextInputCodePointIndex = commentRegEx.lastIndex;
|
|
// The CSS spec tokenizer does not emmit comment tokens
|
|
return this.consumeAToken();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private reconsumeTheCurrentInputToken(currentInputToken: InputToken) {
|
|
this.reconsumedInputToken = currentInputToken;
|
|
}
|
|
|
|
/**
|
|
* 5.3.1. Parse a stylesheet
|
|
* https://www.w3.org/TR/css-syntax-3/#parse-a-stylesheet
|
|
*/
|
|
public parseAStylesheet(): Stylesheet {
|
|
this.topLevelFlag = true;
|
|
const stylesheet: Stylesheet = {
|
|
rules: this.consumeAListOfRules()
|
|
};
|
|
return stylesheet;
|
|
}
|
|
|
|
/**
|
|
* 5.4.1. Consume a list of rules
|
|
* https://www.w3.org/TR/css-syntax-3/#consume-a-list-of-rules
|
|
*/
|
|
public consumeAListOfRules(): Rule[] {
|
|
const rules: Rule[] = [];
|
|
let inputToken: InputToken;
|
|
while(inputToken = this.consumeAToken()) {
|
|
switch(inputToken) {
|
|
case " ": continue;
|
|
case "<!--":
|
|
case "-->":
|
|
if (this.topLevelFlag) {
|
|
continue;
|
|
}
|
|
this.reconsumeTheCurrentInputToken(inputToken);
|
|
const atRule = this.consumeAnAtRule();
|
|
if (atRule) {
|
|
rules.push(atRule);
|
|
}
|
|
continue;
|
|
}
|
|
if ((<InputTokenObject>inputToken).type === TokenObjectType.atKeyword) {
|
|
this.reconsumeTheCurrentInputToken(inputToken);
|
|
const atRule = this.consumeAnAtRule();
|
|
if (atRule) {
|
|
rules.push(atRule);
|
|
}
|
|
continue;
|
|
}
|
|
this.reconsumeTheCurrentInputToken(inputToken);
|
|
const qualifiedRule = this.consumeAQualifiedRule();
|
|
if (qualifiedRule) {
|
|
rules.push(qualifiedRule);
|
|
}
|
|
}
|
|
return rules;
|
|
}
|
|
|
|
/**
|
|
* 5.4.2. Consume an at-rule
|
|
* https://www.w3.org/TR/css-syntax-3/#consume-an-at-rule
|
|
*/
|
|
public consumeAnAtRule(): AtRule {
|
|
let inputToken = this.consumeAToken();
|
|
const atRule: AtRule = {
|
|
type: "at-rule",
|
|
name: (<AtKeywordToken>inputToken).text,
|
|
prelude: [],
|
|
block: undefined
|
|
}
|
|
while(inputToken = this.consumeAToken()) {
|
|
if (inputToken === ";") {
|
|
return atRule;
|
|
} else if (inputToken === "{") {
|
|
atRule.block = this.consumeASimpleBlock(inputToken);
|
|
return atRule;
|
|
} else if ((<InputTokenObject>inputToken).type === TokenObjectType.simpleBlock && (<SimpleBlock>inputToken).associatedToken === "{") {
|
|
atRule.block = <SimpleBlock>inputToken;
|
|
return atRule;
|
|
}
|
|
this.reconsumeTheCurrentInputToken(inputToken);
|
|
const component = this.consumeAComponentValue();
|
|
if (component) {
|
|
atRule.prelude.push(component);
|
|
}
|
|
}
|
|
return atRule;
|
|
}
|
|
|
|
/**
|
|
* 5.4.3. Consume a qualified rule
|
|
* https://www.w3.org/TR/css-syntax-3/#consume-a-qualified-rule
|
|
*/
|
|
public consumeAQualifiedRule(): QualifiedRule {
|
|
const qualifiedRule: QualifiedRule = {
|
|
type: "qualified-rule",
|
|
prelude: [],
|
|
block: undefined
|
|
};
|
|
let inputToken: InputToken;
|
|
while(inputToken = this.consumeAToken()) {
|
|
if (inputToken === "{") {
|
|
let block = this.consumeASimpleBlock(inputToken);
|
|
qualifiedRule.block = block;
|
|
return qualifiedRule;
|
|
} else if ((<InputTokenObject>inputToken).type === TokenObjectType.simpleBlock) {
|
|
const simpleBlock: SimpleBlock = <SimpleBlock>inputToken;
|
|
if (simpleBlock.associatedToken === "{") {
|
|
qualifiedRule.block = simpleBlock;
|
|
return qualifiedRule;
|
|
}
|
|
}
|
|
this.reconsumeTheCurrentInputToken(inputToken);
|
|
const componentValue = this.consumeAComponentValue();
|
|
if (componentValue) {
|
|
qualifiedRule.prelude.push(componentValue);
|
|
}
|
|
}
|
|
// TODO: This is a parse error, log parse errors!
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 5.4.6. Consume a component value
|
|
* https://www.w3.org/TR/css-syntax-3/#consume-a-component-value
|
|
*/
|
|
private consumeAComponentValue(): InputToken {
|
|
// const inputToken = this.consumeAToken();
|
|
const inputToken = this.consumeAToken();
|
|
switch(inputToken) {
|
|
case "{":
|
|
case "[":
|
|
case "(":
|
|
this.nextInputCodePointIndex++;
|
|
return this.consumeASimpleBlock(inputToken);
|
|
}
|
|
if (typeof inputToken === "object" && inputToken.type === TokenObjectType.functionToken) {
|
|
return this.consumeAFunction((<FunctionInputToken>inputToken).name);
|
|
}
|
|
return inputToken;
|
|
}
|
|
|
|
/**
|
|
* 5.4.7. Consume a simple block
|
|
* https://www.w3.org/TR/css-syntax-3/#consume-a-simple-block
|
|
*/
|
|
private consumeASimpleBlock(associatedToken: InputToken): SimpleBlock {
|
|
const endianToken: "]" | "}" | ")" = {
|
|
"[": "]",
|
|
"{": "}",
|
|
"(": ")"
|
|
}[<any>associatedToken];
|
|
const start = this.nextInputCodePointIndex - 1;
|
|
const block: SimpleBlock = {
|
|
type: TokenObjectType.simpleBlock,
|
|
text: undefined,
|
|
associatedToken,
|
|
values: []
|
|
};
|
|
let nextInputToken;
|
|
while(nextInputToken = this.text[this.nextInputCodePointIndex]) {
|
|
if (nextInputToken === endianToken) {
|
|
this.nextInputCodePointIndex++;
|
|
const end = this.nextInputCodePointIndex;
|
|
block.text = this.text.substring(start, end);
|
|
return block;
|
|
}
|
|
const value = this.consumeAComponentValue();
|
|
if (value) {
|
|
block.values.push(value);
|
|
}
|
|
}
|
|
block.text = this.text.substring(start);
|
|
return block;
|
|
}
|
|
|
|
/**
|
|
* 5.4.8. Consume a function
|
|
* https://www.w3.org/TR/css-syntax-3/#consume-a-function
|
|
*/
|
|
private consumeAFunction(name: string): InputToken {
|
|
const start = this.nextInputCodePointIndex;
|
|
const funcToken: FunctionToken = { type: TokenObjectType.function, name, text: undefined, components: [] };
|
|
do {
|
|
if (this.nextInputCodePointIndex >= this.text.length) {
|
|
funcToken.text = name + "(" + this.text.substring(start);
|
|
return funcToken;
|
|
}
|
|
const nextInputToken = this.text[this.nextInputCodePointIndex];
|
|
switch(nextInputToken) {
|
|
case ")":
|
|
this.nextInputCodePointIndex++;
|
|
const end = this.nextInputCodePointIndex;
|
|
funcToken.text = name + "(" + this.text.substring(start, end);
|
|
return funcToken;
|
|
default:
|
|
const component = this.consumeAComponentValue();
|
|
if (component) {
|
|
funcToken.components.push(component);
|
|
}
|
|
// TODO: Else we won't advance
|
|
}
|
|
} while(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Consume a CSS3 parsed stylesheet and convert the rules and selectors to the
|
|
* NativeScript internal JSON representation.
|
|
*/
|
|
export class CSSNativeScript {
|
|
public parseStylesheet(stylesheet: Stylesheet): any {
|
|
return {
|
|
type: "stylesheet",
|
|
stylesheet: {
|
|
rules: this.parseRules(stylesheet.rules)
|
|
}
|
|
}
|
|
}
|
|
|
|
private parseRules(rules: Rule[]): any {
|
|
return rules.map(rule => this.parseRule(rule));
|
|
}
|
|
|
|
private parseRule(rule: Rule): any {
|
|
if (rule.type === "at-rule") {
|
|
return this.parseAtRule(rule);
|
|
} else if (rule.type === "qualified-rule") {
|
|
return this.parseQualifiedRule(rule);
|
|
}
|
|
}
|
|
|
|
private parseAtRule(rule: AtRule): any {
|
|
if (rule.name === "import") {
|
|
// TODO: We have used an "@improt { url('path somewhere'); }" at few places.
|
|
return {
|
|
import: rule.prelude.map(m => typeof m === "string" ? m : m.text).join("").trim(),
|
|
type: "import"
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
private parseQualifiedRule(rule: QualifiedRule): any {
|
|
return {
|
|
type: "rule",
|
|
selectors: this.preludeToSelectorsStringArray(rule.prelude),
|
|
declarations: this.ruleBlockToDeclarations(rule.block.values)
|
|
}
|
|
}
|
|
|
|
private ruleBlockToDeclarations(declarationsInputTokens: InputToken[]): { type: "declaration", property: string, value: string }[] {
|
|
// return <any>declarationsInputTokens;
|
|
const declarations: { type: "declaration", property: string, value: string }[] = [];
|
|
|
|
let property = "";
|
|
let value = "";
|
|
let reading: "property" | "value" = "property";
|
|
|
|
for (var i = 0; i < declarationsInputTokens.length; i++) {
|
|
let inputToken = declarationsInputTokens[i];
|
|
if (reading === "property") {
|
|
if (inputToken === ":") {
|
|
reading = "value";
|
|
} else if (typeof inputToken === "string") {
|
|
property += inputToken;
|
|
} else {
|
|
property += inputToken.text;
|
|
}
|
|
} else {
|
|
if (inputToken === ";") {
|
|
property = property.trim();
|
|
value = value.trim();
|
|
declarations.push({ type: "declaration", property, value });
|
|
property = "";
|
|
value = "";
|
|
reading = "property";
|
|
} else if (typeof inputToken === "string") {
|
|
value += inputToken;
|
|
} else {
|
|
value += inputToken.text;
|
|
}
|
|
}
|
|
}
|
|
property = property.trim();
|
|
value = value.trim();
|
|
if (property || value) {
|
|
declarations.push({ type: "declaration", property, value });
|
|
}
|
|
return declarations;
|
|
}
|
|
|
|
private preludeToSelectorsStringArray(prelude: InputToken[]): string[] {
|
|
let selectors = [];
|
|
let selector = "";
|
|
prelude.forEach(inputToken => {
|
|
if (typeof inputToken === "string") {
|
|
if (inputToken === ",") {
|
|
if (selector) {
|
|
selectors.push(selector.trim());
|
|
}
|
|
selector = "";
|
|
} else {
|
|
selector += inputToken;
|
|
}
|
|
} else if (typeof inputToken === "object") {
|
|
selector += inputToken.text;
|
|
}
|
|
});
|
|
if (selector) {
|
|
selectors.push(selector.trim());
|
|
}
|
|
return selectors;
|
|
}
|
|
} |