352 lines
13 KiB
JavaScript
352 lines
13 KiB
JavaScript
/*
|
|
* Copyright (C) 2023 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.FontStyles = class FontStyles
|
|
{
|
|
constructor(nodeStyles)
|
|
{
|
|
this._nodeStyles = nodeStyles;
|
|
|
|
this._featuresMap = new Map;
|
|
this._variationsMap = new Map;
|
|
this._propertiesMap = new Map;
|
|
|
|
this._authoredFontVariationSettingsMap = new Map;
|
|
this._effectiveWritablePropertyForNameMap = new Map;
|
|
|
|
// A change in the number of axes or their tags is considered a significant change.
|
|
// A change to the value of a known axis is not considered a significant change.
|
|
this._significantChangeSinceLastRefresh = true;
|
|
|
|
this._variationAxesTags = [];
|
|
this._registeredAxesTags = [];
|
|
|
|
const forceSignificantChange = true;
|
|
this.refresh(forceSignificantChange);
|
|
}
|
|
|
|
// Public
|
|
|
|
get featuresMap() { return this._featuresMap; }
|
|
|
|
get variationsMap() { return this._variationsMap; }
|
|
|
|
get propertiesMap() { return this._propertiesMap; }
|
|
|
|
get significantChangeSinceLastRefresh() { return this._significantChangeSinceLastRefresh; }
|
|
|
|
// Static
|
|
|
|
static fontPropertyForAxisTag(tag) {
|
|
const tagToPropertyMap = {
|
|
"wght": "font-weight",
|
|
"wdth": "font-stretch",
|
|
"slnt": "font-style",
|
|
"ital": "font-style",
|
|
}
|
|
|
|
return tagToPropertyMap[tag];
|
|
}
|
|
|
|
static axisValueToFontPropertyValue(tag, value)
|
|
{
|
|
switch (tag) {
|
|
case "wdth":
|
|
return `${value}%`;
|
|
case "slnt":
|
|
return `oblique ${value}deg`;
|
|
case "ital":
|
|
return value >= 1 ? "italic" : "normal";
|
|
default:
|
|
return value;
|
|
}
|
|
}
|
|
|
|
static fontPropertyValueToAxisValue(tag, value)
|
|
{
|
|
switch (tag) {
|
|
case "wdth":
|
|
return parseFloat(value);
|
|
case "ital":
|
|
case "slnt":
|
|
// See: https://w3c.github.io/csswg-drafts/css-fonts/#valdef-font-style-oblique-angle--90deg-90deg
|
|
const obliqueAngleDefaultValue = 14;
|
|
|
|
if (value === "normal")
|
|
return 0;
|
|
|
|
if (tag === "ital" && (value === "oblique" || value === "italic"))
|
|
return 1;
|
|
|
|
if (tag === "slnt" && (value === "oblique" || value === "italic"))
|
|
return obliqueAngleDefaultValue;
|
|
|
|
let degrees = value.match(/oblique (?<degrees>-?\d+(\.\d+)?)deg/)?.groups?.degrees;
|
|
if (degrees && tag === "ital")
|
|
return parseFloat(degrees) >= obliqueAngleDefaultValue ? 1 : 0; // The `ital` variation axis acts as an on/off toggle (0 = off, 1 = on).
|
|
|
|
if (degrees && tag === "slnt")
|
|
return parseFloat(degrees);
|
|
|
|
console.assert(false, `Unexpected font property value associated with variation axis ${tag}`, value);
|
|
break;
|
|
default:
|
|
return parseFloat(value);
|
|
}
|
|
}
|
|
|
|
// Public
|
|
|
|
writeFontVariation(tag, value)
|
|
{
|
|
let targetPropertyName = WI.FontStyles.fontPropertyForAxisTag(tag);
|
|
let targetPropertyValue;
|
|
if (targetPropertyName && !this._authoredFontVariationSettingsMap.has(tag))
|
|
targetPropertyValue = WI.FontStyles.axisValueToFontPropertyValue(tag, value);
|
|
else {
|
|
this._authoredFontVariationSettingsMap.set(tag, value);
|
|
let axes = [];
|
|
for (let [tag, value] of this._authoredFontVariationSettingsMap) {
|
|
axes.push(`"${tag}" ${value}`);
|
|
}
|
|
|
|
targetPropertyName = "font-variation-settings";
|
|
targetPropertyValue = axes.join(", ");
|
|
}
|
|
|
|
const createIfMissing = true;
|
|
let cssProperty = this._effectiveWritablePropertyForName(targetPropertyName, createIfMissing);
|
|
cssProperty.rawValue = targetPropertyValue;
|
|
}
|
|
|
|
refresh(forceSignificantChange)
|
|
{
|
|
this._effectiveWritablePropertyForNameMap.clear();
|
|
|
|
let prevVariationAxisTags = this._variationAxesTags.slice();
|
|
let prevRegisteredAxisTags = this._registeredAxesTags.slice();
|
|
this._variationAxesTags = [];
|
|
this._registeredAxesTags = [];
|
|
|
|
this._calculateFontProperties();
|
|
|
|
if (forceSignificantChange)
|
|
this._significantChangeSinceLastRefresh = true;
|
|
else
|
|
this._significantChangeSinceLastRefresh = !Array.shallowEqual(prevRegisteredAxisTags, this._registeredAxesTags) || !Array.shallowEqual(prevVariationAxisTags, this._variationAxesTags);
|
|
}
|
|
|
|
// Private
|
|
|
|
_calculateFontProperties()
|
|
{
|
|
this._featuresMap = this._calculateFontFeatureAxes(this._nodeStyles);
|
|
this._variationsMap = this._calculateFontVariationAxes(this._nodeStyles);
|
|
this._propertiesMap = this._calculateProperties({domNodeStyle: this._nodeStyles, featuresMap: this._featuresMap, variationsMap: this._variationsMap});
|
|
}
|
|
|
|
_calculateProperties(style)
|
|
{
|
|
let resultProperties = new Map;
|
|
|
|
this._populateProperty("font-size", style, resultProperties, {
|
|
keywordComputedReplacements: ["larger", "smaller", "xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"],
|
|
});
|
|
this._populateProperty("font-style", style, resultProperties, {
|
|
variations: ["ital", "slnt"],
|
|
keywordReplacements: new Map([
|
|
["oblique", "oblique 14deg"],
|
|
]),
|
|
});
|
|
this._populateProperty("font-weight", style, resultProperties, {
|
|
variations: ["wght"],
|
|
keywordComputedReplacements: ["bolder", "lighter"],
|
|
keywordReplacements: new Map([
|
|
["normal", "400"],
|
|
["bold", "700"],
|
|
]),
|
|
});
|
|
this._populateProperty("font-stretch", style, resultProperties, {
|
|
variations: ["wdth"],
|
|
keywordReplacements: new Map([
|
|
["ultra-condensed", "50%"],
|
|
["extra-condensed", "62.5%"],
|
|
["condensed", "75%"],
|
|
["semi-condensed", "87.5%"],
|
|
["normal", "100%"],
|
|
["semi-expanded", "112.5%"],
|
|
["expanded", "125%"],
|
|
["extra-expanded", "150%"],
|
|
["ultra-expanded", "200%"],
|
|
]),
|
|
});
|
|
|
|
this._populateProperty("font-variant-ligatures", style, resultProperties, {features: ["liga", "clig", "dlig", "hlig", "calt"]});
|
|
this._populateProperty("font-variant-position", style, resultProperties, {features: ["subs", "sups"]});
|
|
this._populateProperty("font-variant-caps", style, resultProperties, {features: ["smcp", "c2sc", "pcap", "c2pc", "unic", "titl"]});
|
|
this._populateProperty("font-variant-numeric", style, resultProperties, {features: ["lnum", "onum", "pnum", "tnum", "frac", "afrc", "ordn", "zero"]});
|
|
this._populateProperty("font-variant-alternates", style, resultProperties, {features: ["hist"] });
|
|
this._populateProperty("font-variant-east-asian", style, resultProperties, {features: ["jp78", "jp83", "jp90", "jp04", "smpl", "trad", "fwid", "pwid", "ruby"]});
|
|
|
|
return resultProperties;
|
|
}
|
|
|
|
_calculateFontFeatureAxes(domNodeStyle)
|
|
{
|
|
return this._parseFontFeatureOrVariationSettings(domNodeStyle, "font-feature-settings");
|
|
}
|
|
|
|
_calculateFontVariationAxes(domNodeStyle)
|
|
{
|
|
this._authoredFontVariationSettingsMap = this._parseFontFeatureOrVariationSettings(domNodeStyle, "font-variation-settings");
|
|
let resultAxes = new Map;
|
|
|
|
if (!this._nodeStyles.computedPrimaryFont)
|
|
return resultAxes;
|
|
|
|
for (let axis of this._nodeStyles.computedPrimaryFont.variationAxes) {
|
|
// `value` can be undefined.
|
|
resultAxes.set(axis.tag, {
|
|
tag: axis.tag,
|
|
name: axis.name,
|
|
minimumValue: axis.minimumValue,
|
|
maximumValue: axis.maximumValue,
|
|
defaultValue: axis.defaultValue,
|
|
value: this._authoredFontVariationSettingsMap.get(axis.tag),
|
|
});
|
|
|
|
this._variationAxesTags.push(axis.tag);
|
|
}
|
|
|
|
return resultAxes;
|
|
}
|
|
|
|
_parseFontFeatureOrVariationSettings(domNodeStyle, property)
|
|
{
|
|
let cssSettings = new Map;
|
|
let cssSettingsRawValue = this._computedPropertyValueForName(domNodeStyle, property);
|
|
|
|
if (cssSettingsRawValue !== "normal") {
|
|
for (let axis of cssSettingsRawValue.split(",")) {
|
|
// Tags can contains upper and lowercase latin letters, numbers, and spaces (only ending with space(s)). Values will be numbers, `on`, or `off`.
|
|
let [tag, value] = axis.match(WI.FontStyles.SettingPattern);
|
|
tag = tag.replaceAll(/["']/g, "");
|
|
if (!value || value === "on")
|
|
value = 1;
|
|
else if (value === "off")
|
|
value = 0;
|
|
cssSettings.set(tag, parseFloat(value));
|
|
}
|
|
}
|
|
|
|
return cssSettings;
|
|
}
|
|
|
|
_populateProperty(name, style, resultProperties, {variations, features, keywordComputedReplacements, keywordReplacements})
|
|
{
|
|
resultProperties.set(name, this._computeProperty(name, style, {variations, features, keywordComputedReplacements, keywordReplacements}));
|
|
}
|
|
|
|
_computeProperty(name, style, {variations, features, keywordComputedReplacements, keywordReplacements})
|
|
{
|
|
variations ??= [];
|
|
features ??= [];
|
|
keywordComputedReplacements ??= [];
|
|
keywordReplacements ??= new Map;
|
|
|
|
let resultProperties = {};
|
|
|
|
let value = this._effectivePropertyValueForName(style.domNodeStyle, name);
|
|
|
|
if (!value || value === "inherit" || keywordComputedReplacements.includes(value))
|
|
value = this._computedPropertyValueForName(style.domNodeStyle, name);
|
|
|
|
if (keywordReplacements.has(value))
|
|
value = keywordReplacements.get(value);
|
|
|
|
resultProperties.value = value;
|
|
|
|
for (let fontVariationTag of variations) {
|
|
let fontVariationAxis = style.variationsMap.get(fontVariationTag);
|
|
if (fontVariationAxis) {
|
|
resultProperties.variations ??= new Map;
|
|
resultProperties.variations.set(fontVariationTag, fontVariationAxis);
|
|
|
|
// Remove the tag so it is not presented twice.
|
|
style.variationsMap.delete(fontVariationTag);
|
|
|
|
this._registeredAxesTags.push(fontVariationTag);
|
|
}
|
|
}
|
|
|
|
for (let fontFeatureSetting of features) {
|
|
let featureSettingValue = style.featuresMap.get(fontFeatureSetting);
|
|
if (featureSettingValue || featureSettingValue === 0) {
|
|
resultProperties.features ??= new Map;
|
|
resultProperties.features.set(fontFeatureSetting, featureSettingValue);
|
|
|
|
// Remove the tag so it is not presented twice.
|
|
style.featuresMap.delete(fontFeatureSetting);
|
|
}
|
|
}
|
|
|
|
return resultProperties;
|
|
}
|
|
|
|
_effectivePropertyValueForName(domNodeStyle, name)
|
|
{
|
|
return domNodeStyle.effectivePropertyForName(name)?.value || "";
|
|
}
|
|
|
|
_effectiveWritablePropertyForName(name, createIfMissing)
|
|
{
|
|
let cssProperty = this._effectiveWritablePropertyForNameMap.get(name);
|
|
if (cssProperty)
|
|
return cssProperty;
|
|
|
|
// FIXME: <webkit.org/b/250127> Value for edited variation axis should be written to ideal CSS rule in cascade
|
|
let inlineCSSStyleDeclaration = this._nodeStyles.inlineStyle;
|
|
let properties = inlineCSSStyleDeclaration.visibleProperties;
|
|
|
|
cssProperty = properties.find(property => property.name === name);
|
|
if (!cssProperty && createIfMissing) {
|
|
cssProperty = inlineCSSStyleDeclaration.newBlankProperty(properties.length);
|
|
cssProperty.name = name;
|
|
}
|
|
|
|
if (cssProperty)
|
|
this._effectiveWritablePropertyForNameMap.set(name, cssProperty);
|
|
|
|
return cssProperty;
|
|
}
|
|
|
|
_computedPropertyValueForName(domNodeStyle, name)
|
|
{
|
|
return domNodeStyle.computedStyle?.propertyForName(name)?.value || "";
|
|
}
|
|
};
|
|
|
|
WI.FontStyles.SettingPattern = /[^\s"']+|["']([^"']*)["']/g;
|