From 46280122131400e64dc2216a2772424f6f6cae77 Mon Sep 17 00:00:00 2001 From: "asamuzaK (Kazz)" Date: Thu, 20 Nov 2025 09:55:41 +0900 Subject: [PATCH] Add function to resolve relative length in pixels --- src/js/css-calc.ts | 18 +--- src/js/util.ts | 117 ++++++++++++++++++++++++++ test/css-calc.test.ts | 24 +++--- test/util.test.ts | 185 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 318 insertions(+), 26 deletions(-) diff --git a/src/js/css-calc.ts b/src/js/css-calc.ts index 0550113..f765f61 100644 --- a/src/js/css-calc.ts +++ b/src/js/css-calc.ts @@ -13,7 +13,7 @@ import { } from './cache'; import { isString, isStringOrNumber } from './common'; import { resolveVar } from './css-var'; -import { roundToPrecision } from './util'; +import { resolveLengthInPixels, roundToPrecision } from './util'; import { MatchedRegExp, Options } from './typedef'; /* constants */ @@ -786,22 +786,12 @@ export const resolveDimension = ( unit: string; value: number; }; - const { dimension = {} } = opt; if (unit === 'px') { return `${value}${unit}`; } - const relativeValue = Number(value); - if (unit && Number.isFinite(relativeValue)) { - let pixelValue; - if (Object.hasOwn(dimension, unit)) { - pixelValue = dimension[unit]; - } else if (typeof dimension.callback === 'function') { - pixelValue = dimension.callback(unit); - } - pixelValue = Number(pixelValue); - if (Number.isFinite(pixelValue)) { - return `${relativeValue * pixelValue}px`; - } + const pixelValue = resolveLengthInPixels(Number(value), unit, opt); + if (Number.isFinite(pixelValue)) { + return `${roundToPrecision(pixelValue, HEX)}px`; } return new NullObject(); }; diff --git a/src/js/util.ts b/src/js/util.ts index 31e6a55..02d7e7c 100644 --- a/src/js/util.ts +++ b/src/js/util.ts @@ -334,3 +334,120 @@ export const interpolateHue = ( } return [hueA, hueB]; }; + +/* absolute font size to pixel ratio */ +const absoluteFontSize = new Map([ + ['xx-small', 3 / 5], + ['x-small', 3 / 4], + ['small', 8 / 9], + ['medium', 1], + ['large', 6 / 5], + ['x-large', 3 / 2], + ['xx-large', 2], + ['xxx-large', 3] +]); + +/* relative font size to pixel ratio */ +const relativeFontSize = new Map([ + ['smaller', 1 / 1.2], + ['larger', 1.2] +]); + +/* absolute length to pixel ratio */ +const absoluteLength = new Map([ + ['cm', 96 / 2.54], + ['mm', 96 / 2.54 / 10], + ['q', 96 / 2.54 / 40], + ['in', 96], + ['pc', 96 / 6], + ['pt', 96 / 72], + ['px', 1] +]); + +/* root relative length to pixel ratio */ +const rootLength = new Map([ + ['rcap', 1], + ['rch', 0.5], + ['rem', 1], + ['rex', 0.5], + ['ric', 1], + ['rlh', 1.2] +]); + +/* relative length to pixel ratio */ +const relativeLength = new Map([ + ['cap', 1], + ['ch', 0.5], + ['em', 1], + ['ex', 0.5], + ['ic', 1], + ['lh', 1.2] +]); + +/** + * resolve length in pixels + * @param value - value + * @param unit - unit + * @param [opt] - options + * @returns pixelated value + */ +export const resolveLengthInPixels = ( + value: number | string, + unit: string | undefined, + opt: Options = {} +): number => { + const { dimension = {} } = opt; + const { callback, em, rem, vh, vw } = dimension as { + callback: (K: string) => number; + em: number; + rem: number; + vh: number; + vw: number; + }; + if (isString(value)) { + value = value.toLowerCase().trim(); + if (absoluteFontSize.has(value)) { + return Number(absoluteFontSize.get(value)) * rem; + } else if (relativeFontSize.has(value)) { + return Number(relativeFontSize.get(value)) * em; + } + return Number.NaN; + } else if (Number.isFinite(value) && unit) { + if (Object.hasOwn(dimension, unit)) { + return value * Number(dimension[unit]); + } else if (typeof callback === 'function') { + return value * callback(unit); + } else if (absoluteLength.has(unit)) { + return value * Number(absoluteLength.get(unit)); + } else if (rootLength.has(unit)) { + return value * Number(rootLength.get(unit)) * rem; + } else if (relativeLength.has(unit)) { + return value * Number(relativeLength.get(unit)) * em; + } else { + switch (unit) { + case 'vb': + case 'vi': { + return value * vw; + } + case 'vmax': { + if (vh > vw) { + return value * vh; + } + return value * vw; + } + case 'vmin': { + if (vh < vw) { + return value * vh; + } + return value * vw; + } + default: { + // unsupported or invalid unit + return Number.NaN; + } + } + } + } + // unsupported or invalid value + return Number.NaN; +}; diff --git a/test/css-calc.test.ts b/test/css-calc.test.ts index 8cdcfdd..7800c2e 100644 --- a/test/css-calc.test.ts +++ b/test/css-calc.test.ts @@ -1004,7 +1004,7 @@ describe('resolve dimension', () => { assert.strictEqual(res, '1200px', 'result'); }); - it('should get null object', () => { + it('should get value', () => { const token = [ 'dimension-token', '100ch', @@ -1024,10 +1024,10 @@ describe('resolve dimension', () => { vw: 10.26 } }); - assert.strictEqual(res.isNull, true, 'result'); + assert.strictEqual(res, '600px', 'result'); }); - it('should get null object', () => { + it('should get value', () => { const token = [ 'dimension-token', '100ex', @@ -1047,10 +1047,10 @@ describe('resolve dimension', () => { vw: 10.26 } }); - assert.strictEqual(res.isNull, true, 'result'); + assert.strictEqual(res, '600px', 'result'); }); - it('should get null object', () => { + it('should get value', () => { const token = [ 'dimension-token', '100lh', @@ -1070,10 +1070,10 @@ describe('resolve dimension', () => { vw: 10.26 } }); - assert.strictEqual(res.isNull, true, 'result'); + assert.strictEqual(res, '1440px', 'result'); }); - it('should get null object', () => { + it('should get value', () => { const token = [ 'dimension-token', '10rch', @@ -1093,10 +1093,10 @@ describe('resolve dimension', () => { vw: 10.26 } }); - assert.strictEqual(res.isNull, true, 'result'); + assert.strictEqual(res, '80px', 'result'); }); - it('should get null object', () => { + it('should get value', () => { const token = [ 'dimension-token', '10rex', @@ -1116,10 +1116,10 @@ describe('resolve dimension', () => { vw: 10.26 } }); - assert.strictEqual(res.isNull, true, 'result'); + assert.strictEqual(res, '80px', 'result'); }); - it('should get null object', () => { + it('should get value', () => { const token = [ 'dimension-token', '100rlh', @@ -1139,7 +1139,7 @@ describe('resolve dimension', () => { vw: 10.26 } }); - assert.strictEqual(res.isNull, true, 'result'); + assert.strictEqual(res, '1920px', 'result'); }); it('should get null object', () => { diff --git a/test/util.test.ts b/test/util.test.ts index e13def5..0096ce9 100644 --- a/test/util.test.ts +++ b/test/util.test.ts @@ -646,3 +646,188 @@ describe('interpolate hue', () => { assert.deepEqual(res, [240, 30], 'result'); }); }); + +describe('resolve length in pixels', () => { + const func = util.resolveLengthInPixels; + + it('should get NaN', () => { + const res = func(); + assert.deepEqual(res, Number.NaN, 'result'); + }); + + it('should get NaN', () => { + const res = func('foo'); + assert.deepEqual(res, Number.NaN, 'result'); + }); + + it('should get number', () => { + const res = func('medium', null, { + dimension: { + rem: 16 + } + }); + assert.deepEqual(res, 16, 'result'); + }); + + it('should get number', () => { + const res = func('smaller', null, { + dimension: { + em: 12 + } + }); + assert.deepEqual(res, 10, 'result'); + }); + + it('should get NaN', () => { + const res = func(3); + assert.deepEqual(res, Number.NaN, 'result'); + }); + + it('should get NaN', () => { + const res = func(3, 'foo'); + assert.deepEqual(res, Number.NaN, 'result'); + }); + + it('should get number', () => { + const res = func(3, 'in', { + dimension: { + in: 96 + } + }); + assert.deepEqual(res, 288, 'result'); + }); + + it('should get number', () => { + const res = func(3, 'in', { + dimension: { + callback: unit => { + if (unit === 'in') { + return 96; + } + } + } + }); + assert.deepEqual(res, 288, 'result'); + }); + + it('should get number', () => { + const res = func(3, 'rem', { + dimension: { + rem: 16 + } + }); + assert.deepEqual(res, 48, 'result'); + }); + + it('should get number', () => { + const res = func(3, 'in', { + dimension: { + rem: 16 + } + }); + assert.deepEqual(res, 288, 'result'); + }); + + it('should get number', () => { + const res = func(3, 'rex', { + dimension: { + rem: 16 + } + }); + assert.deepEqual(res, 24, 'result'); + }); + + it('should get number', () => { + const res = func(3, 'em', { + dimension: { + em: 12 + } + }); + assert.deepEqual(res, 36, 'result'); + }); + + it('should get number', () => { + const res = func(3, 'ex', { + dimension: { + em: 12 + } + }); + assert.deepEqual(res, 18, 'result'); + }); + + it('should get number', () => { + const res = func(3, 'vh', { + dimension: { + vh: 576 / 100 + } + }); + assert.deepEqual(res, 17.28, 'result'); + }); + + it('should get number', () => { + const res = func(3, 'vw', { + dimension: { + vw: 1024 / 100 + } + }); + assert.deepEqual(res, 30.72, 'result'); + }); + + it('should get number', () => { + const res = func(3, 'vb', { + dimension: { + vw: 1024 / 100 + } + }); + assert.deepEqual(res, 30.72, 'result'); + }); + + it('should get number', () => { + const res = func(3, 'vi', { + dimension: { + vw: 1024 / 100 + } + }); + assert.deepEqual(res, 30.72, 'result'); + }); + + it('should get number', () => { + const res = func(3, 'vmax', { + dimension: { + vh: 576 / 100, + vw: 1024 / 100 + } + }); + assert.deepEqual(res, 30.72, 'result'); + }); + + it('should get number', () => { + const res = func(3, 'vmax', { + dimension: { + vw: 576 / 100, + vh: 1024 / 100 + } + }); + assert.deepEqual(res, 30.72, 'result'); + }); + + it('should get number', () => { + const res = func(3, 'vmin', { + dimension: { + vh: 576 / 100, + vw: 1024 / 100 + } + }); + assert.deepEqual(res, 17.28, 'result'); + }); + + it('should get number', () => { + const res = func(3, 'vmin', { + dimension: { + vw: 576 / 100, + vh: 1024 / 100 + } + }); + assert.deepEqual(res, 17.28, 'result'); + }); +});