diff --git a/README.md b/README.md
index fa47947c7..1f3855ccd 100644
--- a/README.md
+++ b/README.md
@@ -53,156 +53,137 @@ Monospace Iosevka contains various stylistic sets to change the shape of certain
-ss01 |
-Andale Mono Style |
+ss01 — Andale Mono Style |
- |
- |
+ |
+ |
-ss02 |
-Anonymous Pro Style |
+ss02 — Anonymous Pro Style |
- |
- |
+ |
+ |
-ss03 |
-Consolas Style |
+ss03 — Consolas Style |
- |
- |
+ |
+ |
-ss04 |
-Menlo Style |
+ss04 — Menlo Style |
- |
- |
+ |
+ |
-ss05 |
-Fira Mono Style |
+ss05 — Fira Mono Style |
- |
- |
+ |
+ |
-ss06 |
-Liberation Mono Style |
+ss06 — Liberation Mono Style |
- |
- |
+ |
+ |
-ss07 |
-Monaco Style |
+ss07 — Monaco Style |
- |
- |
+ |
+ |
-ss08 |
-Pragmata Pro Style |
+ss08 — Pragmata Pro Style |
- |
- |
+ |
+ |
-ss09 |
-Source Code Pro Style |
+ss09 — Source Code Pro Style |
- |
- |
+ |
+ |
-ss10 |
-Envy Code R Style |
+ss10 — Envy Code R Style |
- |
- |
+ |
+ |
-ss11 |
-X Window Style |
+ss11 — X Window Style |
- |
- |
+ |
+ |
-ss12 |
-Ubuntu Mono Style |
+ss12 — Ubuntu Mono Style |
- |
- |
+ |
+ |
-ss13 |
-Lucida Style |
+ss13 — Lucida Style |
- |
- |
+ |
+ |
-ss14 |
-JetBrains Mono Style |
+ss14 — JetBrains Mono Style |
- |
- |
+ |
+ |
-ss15 |
-IBM Plex Mono Style |
+ss15 — IBM Plex Mono Style |
- |
- |
+ |
+ |
-ss16 |
-PT Mono Style |
+ss16 — PT Mono Style |
- |
- |
+ |
+ |
-ss17 |
-Recursive Mono Style |
+ss17 — Recursive Mono Style |
- |
- |
+ |
+ |
-ss18 |
-Input Mono Style |
+ss18 — Input Mono Style |
- |
- |
+ |
+ |
-ss20 |
-Curly Style |
+ss20 — Curly Style |
- |
- |
+ |
+ |
@@ -2971,16 +2952,23 @@ Subsection `variants` is used to configure character variants in the font. Prope
Subsection `weights` is used to change the weight grades that the custom family needs. It is a dictionary of sub-objects with properties:
-* `shape`: Integer, configures the weight grade of the glyphs' shapes.
+* `shape`: Number, configures the weight grade of the glyphs' shapes.
* `menu`: Integer, configures the weight grade used when naming fonts.
* `css`: Integer, configures the weight grade used in web font CSS.
-Subsection `widths` is used to change the weight grades that the custom family needs. It is a dictionary of sub-objects with properties:
+Subsection `widths` is used to change the width grades that the custom family needs. It is a dictionary of sub-objects with properties:
-* `shape`: Integer, configures the width of the glyphs' shapes, measured in 1/1000 em.
+* `shape`: Number, configures the width of the glyphs' shapes, measured in 1/1000 em.
* `menu`: Integer, configures the width grade used when naming fonts. The valid values are `1` to `9`, inclusive.
* `css`: String, configures the [font-stretch](https://developer.mozilla.org/en-US/docs/Web/CSS/font-stretch) value used in web font CSS.
+Subsection `slopes` is used to change the slope angles and grades that the custom family needs. It is a dictionary of sub-objects with properties:
+
+* `angle`: Number, configures the slope angle in degrees. The valid vales are `0` to `15`, inclusive.
+* `shape`: String from `upright`, `italic` or `oblique`. Configures the slope used for variant selection.
+* `menu`: String from `upright`, `italic` or `oblique`. Configures the slope grade used when naming fonts.
+* `css`: String from `normal`, `italic` or `oblique`. Configures the [CSS font-style](https://developer.mozilla.org/zh-CN/docs/Web/CSS/font-style) value.
+
Subsection `slopes` is a simple string-to-string dictionary maps slopes (`upright`, `italic` or `oblique`) to [CSS font-style](https://developer.mozilla.org/zh-CN/docs/Web/CSS/font-style) values, represented in string.
#### Compatibility Ligatures
diff --git a/build-plans.toml b/build-plans.toml
index 42b0cb1fe..c6fdb63e2 100644
--- a/build-plans.toml
+++ b/build-plans.toml
@@ -990,11 +990,6 @@ from = ["iosevka-aile"]
release = true
from = ["iosevka-etoile"]
-[collectConfig]
-distinguishWeights = true
-distinguishWidths = false
-distinguishSlope = false
-
###################################################################################################
# Weight mappings (style => shape weight, menu weight, CSS weight)
# Shape weight : affects the shape of the glyphs
@@ -1048,13 +1043,24 @@ shape = 900
menu = 900
css = 900
-# slope mappings (style => CSS)
-# NOTE: This mapping does NOT affect the font's metadata, only affects
-# the webfont CSS. Change `params/parameters.toml` instead.
-[slopes]
-upright = "normal"
-oblique = "oblique"
-italic = "italic"
+# slope mappings (style => slope angle, shape slope grade, menu slope, CSS slope)
+[slopes.upright]
+angle = 0
+shape = "upright"
+menu = "upright"
+css = "normal"
+
+[slopes.oblique]
+angle = 9.4
+shape = "oblique"
+menu = "oblique"
+css = "oblique"
+
+[slopes.italic]
+angle = 9.4
+shape = "italic"
+menu = "italic"
+css = "italic"
# Width mappings (style => shape width, menu width, CSS stretch)
# Shape width : affects the shape of the glyphs
diff --git a/changes/8.0.0.md b/changes/8.0.0.md
new file mode 100644
index 000000000..e38f22dc6
--- /dev/null
+++ b/changes/8.0.0.md
@@ -0,0 +1,22 @@
+ * \[**Breaking**\] Add support for slope customization (#599, #1165).
+ - Slope customization format has a major change, giving ability to customize slope grade used for variant selection, as well as slope angle.
+ - The format will look like this:
+ ``` toml
+ [buildPlans.iosevka-custom.slopes.upright]
+ angle = 0 # Angle in degrees. Valid range [0, 15]
+ shape = "upright" # Slope grade used for shape selection. `upright` | `oblique` | `italic`
+ menu = "upright" # Slope grade used for naming. `upright` | `oblique` | `italic`
+ css = "normal" # Slope grade used for webfont CSS. `normal` | `oblique` | `italic`
+
+ [buildPlans.iosevka-custom.slopes.oblique]
+ angle = 9.4
+ shape = "oblique"
+ menu = "oblique"
+ css = "oblique"
+
+ [buildPlans.iosevka-custom.slopes.italic]
+ angle = 9.4
+ shape = "italic"
+ menu = "italic"
+ css = "italic"
+ ```
\ No newline at end of file
diff --git a/font-src/index.js b/font-src/index.js
index 628515591..dacc60bf6 100644
--- a/font-src/index.js
+++ b/font-src/index.js
@@ -26,6 +26,7 @@ async function getParameters() {
const PARAMETERS_TOML = path.resolve(__dirname, "../params/parameters.toml");
const WEIGHTS_TOML = path.resolve(__dirname, "../params/shape-weight.toml");
const WIDTHS_TOML = path.resolve(__dirname, "../params/shape-width.toml");
+ const SLOPES_TOML = path.resolve(__dirname, "../params/shape-slope.toml");
const PRIVATE_TOML = path.resolve(__dirname, "../params/private-parameters.toml");
const VARIANTS_TOML = path.resolve(__dirname, "../params/variants.toml");
const LIGATIONS_TOML = path.resolve(__dirname, "../params/ligation-set.toml");
@@ -35,6 +36,7 @@ async function getParameters() {
await tryParseToml(PARAMETERS_TOML),
await tryParseToml(WEIGHTS_TOML),
await tryParseToml(WIDTHS_TOML),
+ await tryParseToml(SLOPES_TOML),
fs.existsSync(PRIVATE_TOML) ? await tryParseToml(PRIVATE_TOML) : {}
);
const rawVariantsData = await tryParseToml(VARIANTS_TOML);
diff --git a/font-src/support/parameters.js b/font-src/support/parameters.js
index dcaa64000..56231d55e 100644
--- a/font-src/support/parameters.js
+++ b/font-src/support/parameters.js
@@ -11,6 +11,7 @@ function initPara(data, argv) {
applyBlendingParam(argv, para, data, "shapeWeight", "weight");
applyBlendingParam(argv, para, data, "shapeWidth", "width");
+ applyBlendingParam(argv, para, data, "shapeSlopeAngle", "slopeAngle");
applyAlternatesParam(argv, para, data, "slope", "slope");
if (argv.featureControl.noCvSs) para.enableCvSs = false;
diff --git a/package.json b/package.json
index c960e33fa..25c8eca23 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "iosevka",
- "version": "7.3.3",
+ "version": "8.0.0",
"main": "./font-src/index.js",
"scripts": {
"build": "node utility/ensure-verda-exists && verda -f verdafile.js",
diff --git a/params/parameters.toml b/params/parameters.toml
index 477d8ffb9..e7e6385de 100644
--- a/params/parameters.toml
+++ b/params/parameters.toml
@@ -127,8 +127,6 @@ diversityII = 0.50
###### Slopes
[slope-italic]
isItalic = true
-slopeAngle = 9.4
[slope-oblique]
isOblique = true
-slopeAngle = 9.4
diff --git a/params/shape-slope.toml b/params/shape-slope.toml
new file mode 100644
index 000000000..38bad1ab8
--- /dev/null
+++ b/params/shape-slope.toml
@@ -0,0 +1,8 @@
+[shapeSlopeAngle.blend.0]
+slopeAngle = 0
+
+[shapeSlopeAngle.blend.-15]
+slopeAngle = -15
+
+[shapeSlopeAngle.blend.15]
+slopeAngle = 15
diff --git a/private-build-plans.sample.toml b/private-build-plans.sample.toml
index 1a4fcae66..2dbb8c905 100644
--- a/private-build-plans.sample.toml
+++ b/private-build-plans.sample.toml
@@ -67,14 +67,25 @@ css = 700
###################################################################################################
# Override default building slope sets
-# Format: = <"normal"|"italic"|"oblique">
# When this section is absent, all slopes would be built.
-[buildPlans.iosevka-custom.slopes]
-upright = "normal"
-italic = "italic"
-oblique = "oblique"
+[buildPlans.iosevka-custom.slopes.upright]
+angle = 0 # Angle in degrees. Valid range [0, 15]
+shape = "upright" # Slope grade used for shape selection. `upright` | `oblique` | `italic`
+menu = "upright" # Slope grade used for naming. `upright` | `oblique` | `italic`
+css = "normal" # Slope grade used for webfont CSS. `normal` | `oblique` | `italic`
+[buildPlans.iosevka-custom.slopes.oblique]
+angle = 9.4
+shape = "oblique"
+menu = "oblique"
+css = "oblique"
+
+[buildPlans.iosevka-custom.slopes.italic]
+angle = 9.4
+shape = "italic"
+menu = "italic"
+css = "italic"
# End slope section
###################################################################################################
diff --git a/utility/amend-readme/index.js b/utility/amend-readme/index.js
index 137ed1ddb..7802d9bcb 100644
--- a/utility/amend-readme/index.js
+++ b/utility/amend-readme/index.js
@@ -39,18 +39,13 @@ async function processSsOt() {
if (!ss.rank) continue;
{
md.log(``);
- md.log(`${ss.tag} | `);
- md.log(`${ss.description} | `);
+ md.log(`${ss.tag} — ${ss.description} | `);
md.log(`
`);
}
{
md.log(``);
- md.log(
- ` | `
- );
- md.log(
- ` | `
- );
+ md.log(` | `);
+ md.log(` | `);
md.log(`
`);
}
}
diff --git a/verdafile.js b/verdafile.js
index a1a519412..4d7504a1a 100644
--- a/verdafile.js
+++ b/verdafile.js
@@ -36,7 +36,6 @@ const webfontFormatsPages = [["woff2", "woff2"]];
const WIDTH_NORMAL = "normal";
const WEIGHT_NORMAL = "regular";
const SLOPE_NORMAL = "upright";
-const SLOPE_OBLIQUE = "oblique";
const DEFAULT_SUBFAMILY = "regular";
const BUILD_PLANS = "build-plans.toml";
@@ -189,8 +188,9 @@ const FontInfoOf = computed.group("metadata:font-info-of", async (target, fileNa
serifs: bp.serifs || null,
spacing: bp.spacing || null,
weight: sfi.shapeWeight,
- slope: sfi.slope,
- width: sfi.shapeWidth
+ width: sfi.shapeWidth,
+ slope: sfi.shapeSlope,
+ slopeAngle: sfi.shapeSlopeAngle
},
// Menu
menu: {
@@ -228,23 +228,28 @@ function getSuffixMapping(weights, slopes, widths) {
return mapping;
}
function getSuffixMappingItem(weights, w, slopes, s, widths, wd) {
+ const weightDef = wwsDefValidate("Weight definition of " + s, weights[w]);
+ const widthDef = wwsDefValidate("Width definition of " + s, widths[wd]);
+ const slopeDef = wwsDefValidate("Slope definition of " + s, slopes[s]);
return {
// Weights
weight: w,
- shapeWeight: nValidate("Shape weight of " + w, weights[w].shape, VlShapeWeight),
- cssWeight: nValidate("CSS weight of " + w, weights[w].css, VlCssWeight),
- menuWeight: nValidate("Menu weight of " + w, weights[w].menu, VlMenuWeight),
+ shapeWeight: nValidate("Shape weight of " + w, weightDef.shape, VlShapeWeight),
+ cssWeight: nValidate("CSS weight of " + w, weightDef.css, VlCssWeight),
+ menuWeight: nValidate("Menu weight of " + w, weightDef.menu, VlMenuWeight),
// Widths
width: wd,
- shapeWidth: nValidate("Shape width of " + wd, widths[wd].shape, VlShapeWidth),
- cssStretch: widths[wd].css || wd,
- menuWidth: nValidate("Menu width of " + wd, widths[wd].menu, VlMenuWidth),
+ shapeWidth: nValidate("Shape width of " + wd, widthDef.shape, VlShapeWidth),
+ cssStretch: sValidate("CSS stretch of " + wd, widthDef.css, VlCssFontStretch),
+ menuWidth: nValidate("Menu width of " + wd, widthDef.menu, VlMenuWidth),
// Slopes
slope: s,
- cssStyle: slopes[s] || s,
- menuSlope: slopes[s] || s
+ shapeSlope: sValidate("Shape slope of " + s, slopeDef.shape, VlShapeSlope),
+ shapeSlopeAngle: nValidate("Angle of " + s, slopeDef.angle, VlSlopeAngle),
+ cssStyle: sValidate("CSS style of " + s, slopeDef.css, VlCssStyle),
+ menuSlope: sValidate("Menu slope of " + s, slopeDef.menu, VlShapeSlope)
};
}
@@ -366,18 +371,11 @@ const DistWoff2 = file.make(
const CollectPlans = computed(`metadata:collect-plans`, async target => {
const [rawPlans] = await target.need(RawPlans);
- return await getCollectPlans(
- target,
- rawPlans.collectPlans,
- rawPlans.collectConfig,
- fnStandardTtc
- );
+ return await getCollectPlans(target, rawPlans.collectPlans);
});
-async function getCollectPlans(target, rawCollectPlans, config, fnFileName) {
- const glyfTtcComposition = {},
- ttcComposition = {},
- plans = {};
+async function getCollectPlans(target, rawCollectPlans) {
+ const plans = {};
let allCollectableGroups = new Set();
for (const collectPrefix in rawCollectPlans) {
@@ -392,65 +390,60 @@ async function getCollectPlans(target, rawCollectPlans, config, fnFileName) {
}
for (const collectPrefix in amendedRawCollectPlans) {
- const groupFileList = new Set();
+ const glyfTtcComposition = {};
+ const ttcComposition = {};
const collect = amendedRawCollectPlans[collectPrefix];
if (!collect || !collect.from || !collect.from.length) continue;
for (const prefix of collect.from) {
const [gri] = await target.need(BuildPlanOf(prefix));
const ttfFileNameSet = new Set(gri.targets);
- const suffixMapping = getSuffixMapping(gri.weights, gri.slopes, gri.widths);
- for (const suffix in suffixMapping) {
- const sfi = suffixMapping[suffix];
- const ttcFileName = fnFileName(
- config,
- collectPrefix,
- sfi.weight,
- sfi.width,
- sfi.slope
- );
- const glyfTtcFileName = fnFileName(
- { ...config, distinguishWidths: true, distinguishWhetherUpright: true },
- collectPrefix,
- sfi.weight,
- sfi.width,
- sfi.slope
- );
+ const suffixMap = getSuffixMapping(gri.weights, gri.slopes, gri.widths);
+ for (const suffix in suffixMap) {
+ const sfi = suffixMap[suffix];
const ttfTargetName = makeFileName(prefix, suffix);
if (!ttfFileNameSet.has(ttfTargetName)) continue;
+ const glyfTtcFileName = fnStandardTtc(true, collectPrefix, suffixMap, sfi);
if (!glyfTtcComposition[glyfTtcFileName]) glyfTtcComposition[glyfTtcFileName] = [];
glyfTtcComposition[glyfTtcFileName].push({ dir: prefix, file: ttfTargetName });
+
+ const ttcFileName = fnStandardTtc(false, collectPrefix, suffixMap, sfi);
if (!ttcComposition[ttcFileName]) ttcComposition[ttcFileName] = [];
ttcComposition[ttcFileName].push(glyfTtcFileName);
-
- groupFileList.add(ttcFileName);
}
}
plans[collectPrefix] = {
- ttcContents: [...groupFileList],
+ glyfTtcComposition,
+ ttcComposition,
groupDecomposition: [...collect.from],
inRelease: !!collect.release,
isAmended: !!collect.isAmended
};
}
- return { glyfTtcComposition, ttcComposition, plans };
+ return plans;
}
-function fnStandardTtc(collectConfig, prefix, w, wd, s) {
- const ttcSuffix = makeSuffix(
- collectConfig.distinguishWeights ? w : WEIGHT_NORMAL,
- collectConfig.distinguishWidths ? wd : WIDTH_NORMAL,
- collectConfig.distinguishSlope
- ? s
- : collectConfig.distinguishWhetherUpright
- ? s === SLOPE_NORMAL
- ? SLOPE_NORMAL
- : SLOPE_OBLIQUE
- : SLOPE_NORMAL,
- DEFAULT_SUBFAMILY
- );
- return `${prefix}-${ttcSuffix}`;
+
+function fnStandardTtc(fIsGlyfTtc, prefix, suffixMapping, sfi) {
+ let optimalSfi = null,
+ maxScore = 0;
+ for (const ttcSuffix in suffixMapping) {
+ const sfiT = suffixMapping[ttcSuffix];
+ if (sfi.shapeWeight !== sfiT.shapeWeight) continue;
+ if (sfi.shapeWidth !== sfiT.shapeWidth) continue;
+ if (fIsGlyfTtc && sfi.shapeSlopeAngle !== sfiT.shapeSlopeAngle) continue;
+ const score =
+ (sfiT.weight === WEIGHT_NORMAL ? 1 : 0) +
+ (sfiT.width === WIDTH_NORMAL ? 1 : 0) +
+ (sfiT.slope === SLOPE_NORMAL ? 1 : 0);
+ if (!optimalSfi || score > maxScore) {
+ maxScore = score;
+ optimalSfi = sfiT;
+ }
+ }
+ if (!optimalSfi) throw new Error("Unreachable: TTC name decision");
+ return `${prefix}-${makeSuffix(optimalSfi.weight, optimalSfi.width, optimalSfi.slope)}`;
}
///////////////////////////////////////////////////////////
@@ -464,25 +457,25 @@ const CollectedSuperTtcFile = file.make(
cgr => `${DIST_SUPER_TTC}/${cgr}.ttc`,
async (target, out, cgr) => {
const [cp] = await target.need(CollectPlans, de(out.dir));
- const parts = Array.from(new Set(cp.plans[cgr].ttcContents));
- const [inputs] = await target.need(parts.map(pt => CollectedTtcFile(cgr, pt)));
+ const parts = Array.from(Object.keys(cp[cgr].glyfTtcComposition));
+ const [inputs] = await target.need(parts.map(pt => GlyfTtc(cgr, pt)));
await buildCompositeTtc(out, inputs);
}
);
const CollectedTtcFile = file.make(
- (cgr, f) => `${BUILD}/ttc-collect/${cgr}/ttc/${f}.ttc`,
- async (target, out, gr, f) => {
+ (cgr, f) => `${BUILD}/ttc-collect/${cgr}/${f}.ttc`,
+ async (target, out, cgr, f) => {
const [cp] = await target.need(CollectPlans, de`${out.dir}`);
- const parts = Array.from(new Set(cp.ttcComposition[f]));
- const [inputs] = await target.need(parts.map(pt => GlyfTtc(gr, pt)));
+ const parts = Array.from(new Set(cp[cgr].ttcComposition[f]));
+ const [inputs] = await target.need(parts.map(pt => GlyfTtc(cgr, pt)));
await buildCompositeTtc(out, inputs);
}
);
const GlyfTtc = file.make(
(cgr, f) => `${BUILD}/glyf-ttc/${cgr}/${f}.ttc`,
- async (target, out, gr, f) => {
+ async (target, out, cgr, f) => {
const [cp] = await target.need(CollectPlans);
- const parts = cp.glyfTtcComposition[f];
+ const parts = cp[cgr].glyfTtcComposition[f];
await buildGlyphSharingTtc(target, parts, out);
}
);
@@ -508,13 +501,13 @@ async function buildGlyphSharingTtc(target, parts, out) {
const TtcArchiveFile = file.make(
(cgr, version) => `${ARCHIVE_DIR}/ttc-${cgr}-${version}.zip`,
async (target, out, cgr) => {
- const [collectPlans] = await target.need(CollectPlans, de`${out.dir}`);
- const ttcFiles = Array.from(new Set(collectPlans.plans[cgr].ttcContents));
+ const [cp] = await target.need(CollectPlans, de`${out.dir}`);
+ const ttcFiles = Array.from(Object.keys(cp[cgr].ttcComposition));
await target.need(ttcFiles.map(pt => CollectedTtcFile(cgr, pt)));
// Packaging
await rm(out.full);
- await cd(`${BUILD}/ttc-collect/${cgr}/ttc`).run(
+ await cd(`${BUILD}/ttc-collect/${cgr}`).run(
["7z", "a"],
["-tzip", "-r", "-mx=9"],
`../../../../${out.full}`,
@@ -795,7 +788,7 @@ phony(`clean`, async () => {
phony(`release`, async target => {
const [collectPlans] = await target.need(CollectPlans);
let goals = [];
- for (const [cgr, plan] of Object.entries(collectPlans.plans)) {
+ for (const [cgr, plan] of Object.entries(collectPlans)) {
if (!plan.inRelease) continue;
goals.push(ReleaseGroup(cgr));
}
@@ -804,7 +797,7 @@ phony(`release`, async target => {
});
const ReleaseGroup = phony.group("release-group", async (target, cgr) => {
const [version, collectPlans] = await target.need(Version, CollectPlans);
- const subGroups = collectPlans.plans[cgr].groupDecomposition;
+ const subGroups = collectPlans[cgr].groupDecomposition;
let goals = [TtcArchiveFile(cgr, version), SuperTtcArchiveFile(cgr, version)];
for (const gr of subGroups) {
@@ -887,6 +880,7 @@ const Parameters = task(`meta:parameters`, async target => {
sfu`params/parameters.toml`,
sfu`params/shape-weight.toml`,
sfu`params/shape-width.toml`,
+ sfu`params/shape-slope.toml`,
ofu`params/private-parameters.toml`,
sfu`params/variants.toml`,
sfu`params/ligation-set.toml`
@@ -937,6 +931,13 @@ function validateRecommendedWeight(w, value, label) {
}
// Value validation
+function wwsDefValidate(key, obj) {
+ if (!obj || typeof obj === "string") {
+ throw new TypeError(`${key} is invalid.`);
+ }
+ return obj;
+}
+
function nValidate(key, v, validator) {
if (validator.fixup) v = validator.fix(v);
if (typeof v !== "number" || !isFinite(v) || !validator.validate(v)) {
@@ -968,3 +969,26 @@ const VlShapeWidth = {
}
};
const VlMenuWidth = { validate: x => x >= 1 && x <= 9 && x % 1 === 0 };
+const VlSlopeAngle = { validate: x => x >= 0 && x <= 15 };
+
+function sValidate(key, v, validator) {
+ if (validator.fixup) v = validator.fix(v);
+ if (typeof v !== "string" || !validator.validate(v)) {
+ throw new TypeError(`${key} = ${v} is not a valid string.`);
+ }
+ return v;
+}
+const VlShapeSlope = { validate: x => x === "upright" || x === "oblique" || x === "italic" };
+const VlCssStyle = { validate: x => x === "normal" || x === "oblique" || x === "italic" };
+const VlCssFontStretch = {
+ validate: x =>
+ x == "ultra-condensed" ||
+ x == "extra-condensed" ||
+ x == "condensed" ||
+ x == "semi-condensed" ||
+ x == "normal" ||
+ x == "semi-expanded" ||
+ x == "expanded" ||
+ x == "extra-expanded" ||
+ x == "ultra-expanded"
+};