362 lines
9.9 KiB
362 lines
9.9 KiB
"use strict";
|
|
|
|
//
|
|
// Parsers for properties that take CSS-style strings as values
|
|
//
|
|
|
|
// -- Font & Variant --------------------------------------------------------------------
|
|
// https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant
|
|
// https://www.w3.org/TR/css-fonts-3/#font-size-prop
|
|
import splitBy from "string-split-by";
|
|
var m,
|
|
cache = { font: {}, variant: {} };
|
|
|
|
const styleRE = /^(normal|italic|oblique)$/,
|
|
smallcapsRE = /^(normal|small-caps)$/,
|
|
stretchRE = /^(normal|(semi-|extra-|ultra-)?(condensed|expanded))$/,
|
|
namedSizeRE = /(?:xx?-)?small|smaller|medium|larger|(?:xx?-)?large|normal/,
|
|
numSizeRE = /^([\d\.]+)(px|pt|pc|in|cm|mm|%|em|ex|ch|rem|q)/,
|
|
namedWeightRE = /^(normal|bold(er)?|lighter)$/,
|
|
numWeightRE = /^(1000|\d{1,3})$/,
|
|
parameterizedRE = /([\w\-]+)\((.*?)\)/,
|
|
unquote = s => s.replace(/^(['"])(.*?)\1$/, "$2"),
|
|
isSize = s => namedSizeRE.test(s) || numSizeRE.test(s),
|
|
isWeight = s => namedWeightRE.test(s) || numWeightRE.test(s);
|
|
|
|
function parseFont(str) {
|
|
if (cache.font[str] === undefined) {
|
|
try {
|
|
if (typeof str !== "string")
|
|
throw new Error("Font specification must be a string");
|
|
if (!str) throw new Error("Font specification cannot be an empty string");
|
|
|
|
let font = {
|
|
style: "normal",
|
|
variant: "normal",
|
|
weight: "normal",
|
|
stretch: "normal"
|
|
},
|
|
value = str.replace(/\s*\/\*s/, "/"),
|
|
tokens = splitBy(value, /\s+/),
|
|
token;
|
|
|
|
while ((token = tokens.shift())) {
|
|
let match = styleRE.test(token)
|
|
? "style"
|
|
: smallcapsRE.test(token)
|
|
? "variant"
|
|
: stretchRE.test(token)
|
|
? "stretch"
|
|
: isWeight(token)
|
|
? "weight"
|
|
: isSize(token)
|
|
? "size"
|
|
: null;
|
|
|
|
switch (match) {
|
|
case "style":
|
|
case "variant":
|
|
case "stretch":
|
|
case "weight":
|
|
font[match] = token;
|
|
break;
|
|
|
|
case "size":
|
|
// size is the pivot point between the style fields and the family name stack,
|
|
// so start processing what's been collected
|
|
let [emSize, leading] = splitBy(token, "/"),
|
|
size = parseSize(emSize),
|
|
lineHeight = parseSize(
|
|
(leading || "1.2").replace(/(\d)$/, "$1em"),
|
|
size
|
|
),
|
|
weight = parseWeight(font.weight),
|
|
family = splitBy(tokens.join(" "), /\s*,\s*/).map(unquote),
|
|
features =
|
|
font.variant == "small-caps" ? { on: ["smcp", "onum"] } : {},
|
|
{ style, stretch, variant } = font;
|
|
|
|
// make sure all the numeric fields have legitimate values
|
|
let invalid = !isFinite(size)
|
|
? `font size "${emSize}"`
|
|
: !isFinite(lineHeight)
|
|
? `line height "${leading}"`
|
|
: !isFinite(weight)
|
|
? `font weight "${font.weight}"`
|
|
: family.length == 0
|
|
? `font family "${tokens.join(", ")}"`
|
|
: false;
|
|
|
|
if (!invalid) {
|
|
// include a re-stringified version of the decoded/absified values
|
|
return (cache.font[str] = Object.assign(font, {
|
|
size,
|
|
lineHeight,
|
|
weight,
|
|
family,
|
|
features,
|
|
canonical: [
|
|
style,
|
|
variant !== style && variant,
|
|
[variant, style].indexOf(weight) == -1 && weight,
|
|
[variant, style, weight].indexOf(stretch) == -1 && stretch,
|
|
`${size}px/${lineHeight}px`,
|
|
family.map(nm => (nm.match(/\s/) ? `"${nm}"` : nm)).join(", ")
|
|
]
|
|
.filter(Boolean)
|
|
.join(" ")
|
|
}));
|
|
}
|
|
throw new Error(`Invalid ${invalid}`);
|
|
|
|
default:
|
|
throw new Error(`Unrecognized font attribute "${token}"`);
|
|
}
|
|
}
|
|
throw new Error("Could not find a font size value");
|
|
} catch (e) {
|
|
// console.warn(Object.assign(e, {name:"Warning"}))
|
|
cache.font[str] = null;
|
|
}
|
|
}
|
|
return cache.font[str];
|
|
}
|
|
|
|
function parseSize(str, emSize = 16) {
|
|
if ((m = numSizeRE.exec(str))) {
|
|
let [size, unit] = [parseFloat(m[1]), m[2]];
|
|
return (
|
|
size *
|
|
(unit == "px"
|
|
? 1
|
|
: unit == "pt"
|
|
? 1 / 0.75
|
|
: unit == "%"
|
|
? emSize / 100
|
|
: unit == "pc"
|
|
? 16
|
|
: unit == "in"
|
|
? 96
|
|
: unit == "cm"
|
|
? 96.0 / 2.54
|
|
: unit == "mm"
|
|
? 96.0 / 25.4
|
|
: unit == "q"
|
|
? 96 / 25.4 / 4
|
|
: unit.match("r?em")
|
|
? emSize
|
|
: NaN)
|
|
);
|
|
}
|
|
|
|
if ((m = namedSizeRE.exec(str))) {
|
|
return emSize * (sizeMap[m[0]] || 1.0);
|
|
}
|
|
|
|
return NaN;
|
|
}
|
|
|
|
function parseWeight(str) {
|
|
return (m = numWeightRE.exec(str))
|
|
? parseInt(m[0]) || NaN
|
|
: (m = namedWeightRE.exec(str))
|
|
? weightMap[m[0]]
|
|
: NaN;
|
|
}
|
|
|
|
function parseVariant(str) {
|
|
if (cache.variant[str] === undefined) {
|
|
let variants = [],
|
|
features = { on: [], off: [] };
|
|
|
|
for (let token of splitBy(str, /\s+/)) {
|
|
if (token == "normal") {
|
|
return { variants: [token], features: { on: [], off: [] } };
|
|
} else if (token in featureMap) {
|
|
featureMap[token].forEach(feat => {
|
|
if (feat[0] == "-") features.off.push(feat.slice(1));
|
|
else features.on.push(feat);
|
|
});
|
|
variants.push(token);
|
|
} else if ((m = parameterizedRE.exec(token))) {
|
|
let subPattern = alternatesMap[m[1]],
|
|
subValue = Math.max(0, Math.min(99, parseInt(m[2], 10))),
|
|
[feat, val] = subPattern
|
|
.replace(/##/, subValue < 10 ? "0" + subValue : subValue)
|
|
.replace(/#/, Math.min(9, subValue))
|
|
.split(" ");
|
|
if (typeof val == "undefined") features.on.push(feat);
|
|
else features[feat] = parseInt(val, 10);
|
|
variants.push(`${m[1]}(${subValue})`);
|
|
} else {
|
|
throw new Error(`Invalid font variant "${token}"`);
|
|
}
|
|
}
|
|
|
|
cache.variant[str] = { variant: variants.join(" "), features: features };
|
|
}
|
|
|
|
return cache.variant[str];
|
|
}
|
|
|
|
// -- Image Filters -----------------------------------------------------------------------
|
|
// https://developer.mozilla.org/en-US/docs/Web/CSS/filter
|
|
|
|
var plainFilterRE = /(blur|hue-rotate|brightness|contrast|grayscale|invert|opacity|saturate|sepia)\((.*?)\)/,
|
|
shadowFilterRE = /drop-shadow\((.*)\)/,
|
|
percentValueRE = /^(\+|-)?\d+%$/,
|
|
angleValueRE = /([\d\.]+)(deg|g?rad|turn)/;
|
|
|
|
function parseFilter(str) {
|
|
let filters = {};
|
|
let canonical = [];
|
|
|
|
for (var spec of splitBy(str, /\s+/) || []) {
|
|
if ((m = shadowFilterRE.exec(spec))) {
|
|
let kind = "drop-shadow",
|
|
args = m[1].trim().split(/\s+/),
|
|
lengths = args.slice(0, 3),
|
|
color = args.slice(3).join(" "),
|
|
dims = lengths.map(s => parseSize(s)).filter(isFinite);
|
|
if (dims.length == 3 && !!color) {
|
|
filters[kind] = [...dims, color];
|
|
canonical.push(
|
|
`${kind}(${lengths.join(" ")} ${color.replace(/ /g, "")})`
|
|
);
|
|
}
|
|
} else if ((m = plainFilterRE.exec(spec))) {
|
|
let [kind, arg] = m.slice(1);
|
|
let val =
|
|
kind == "blur"
|
|
? parseSize(arg)
|
|
: kind == "hue-rotate"
|
|
? parseAngle(arg)
|
|
: parsePercentage(arg);
|
|
if (isFinite(val)) {
|
|
filters[kind] = val;
|
|
canonical.push(`${kind}(${arg.trim()})`);
|
|
}
|
|
}
|
|
}
|
|
|
|
return str.trim() == "none"
|
|
? { canonical: "none", filters }
|
|
: canonical.length
|
|
? { canonical: canonical.join(" "), filters }
|
|
: null;
|
|
}
|
|
|
|
function parsePercentage(str) {
|
|
return percentValueRE.test(str.trim()) ? parseInt(str, 10) / 100 : NaN;
|
|
}
|
|
|
|
function parseAngle(str) {
|
|
if ((m = angleValueRE.exec(str.trim()))) {
|
|
let [amt, unit] = [parseFloat(m[1]), m[2]];
|
|
return unit == "deg"
|
|
? amt
|
|
: unit == "rad"
|
|
? (360 * amt) / (2 * Math.PI)
|
|
: unit == "grad"
|
|
? (360 * amt) / 400
|
|
: unit == "turn"
|
|
? 360 * amt
|
|
: NaN;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Font attribute keywords & corresponding values
|
|
//
|
|
|
|
const weightMap = {
|
|
lighter: 300,
|
|
normal: 400,
|
|
bold: 700,
|
|
bolder: 800
|
|
};
|
|
|
|
const sizeMap = {
|
|
"xx-small": 3 / 5,
|
|
"x-small": 3 / 4,
|
|
small: 8 / 9,
|
|
smaller: 8 / 9,
|
|
large: 6 / 5,
|
|
larger: 6 / 5,
|
|
"x-large": 3 / 2,
|
|
"xx-large": 2 / 1,
|
|
normal: 1.2 // special case for lineHeight
|
|
};
|
|
|
|
const featureMap = {
|
|
normal: [],
|
|
|
|
// font-variant-ligatures
|
|
"common-ligatures": ["liga", "clig"],
|
|
"no-common-ligatures": ["-liga", "-clig"],
|
|
"discretionary-ligatures": ["dlig"],
|
|
"no-discretionary-ligatures": ["-dlig"],
|
|
"historical-ligatures": ["hlig"],
|
|
"no-historical-ligatures": ["-hlig"],
|
|
contextual: ["calt"],
|
|
"no-contextual": ["-calt"],
|
|
|
|
// font-variant-position
|
|
super: ["sups"],
|
|
sub: ["subs"],
|
|
|
|
// font-variant-caps
|
|
"small-caps": ["smcp"],
|
|
"all-small-caps": ["c2sc", "smcp"],
|
|
"petite-caps": ["pcap"],
|
|
"all-petite-caps": ["c2pc", "pcap"],
|
|
unicase: ["unic"],
|
|
"titling-caps": ["titl"],
|
|
|
|
// font-variant-numeric
|
|
"lining-nums": ["lnum"],
|
|
"oldstyle-nums": ["onum"],
|
|
"proportional-nums": ["pnum"],
|
|
"tabular-nums": ["tnum"],
|
|
"diagonal-fractions": ["frac"],
|
|
"stacked-fractions": ["afrc"],
|
|
ordinal: ["ordn"],
|
|
"slashed-zero": ["zero"],
|
|
|
|
// font-variant-east-asian
|
|
jis78: ["jp78"],
|
|
jis83: ["jp83"],
|
|
jis90: ["jp90"],
|
|
jis04: ["jp04"],
|
|
simplified: ["smpl"],
|
|
traditional: ["trad"],
|
|
"full-width": ["fwid"],
|
|
"proportional-width": ["pwid"],
|
|
ruby: ["ruby"],
|
|
|
|
// font-variant-alternates (non-parameterized)
|
|
"historical-forms": ["hist"]
|
|
};
|
|
|
|
const alternatesMap = {
|
|
stylistic: "salt #",
|
|
styleset: "ss##",
|
|
"character-variant": "cv##",
|
|
swash: "swsh #",
|
|
ornaments: "ornm #",
|
|
annotation: "nalt #"
|
|
};
|
|
|
|
// module.exports = {
|
|
// font: parseFont,
|
|
// variant: parseVariant,
|
|
// size: parseSize,
|
|
// filter: parseFilter
|
|
// };
|
|
export default {
|
|
font: parseFont,
|
|
variant: parseVariant,
|
|
size: parseSize,
|
|
filter: parseFilter
|
|
};
|