"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 };