feat: New checkAndEvaluateMath function that prevents unit mixing#357
feat: New checkAndEvaluateMath function that prevents unit mixing#357
Conversation
|
test: update tests to throw error for mixed units refactor: Add custom MixedUnitsExpressionError for better error handling refactor: simplify unit parsing logic and add debug logging fix: handle multiplication with different units in math expressions Detect units Unit parsing Unit tests Last tests
a0e9378 to
fd1a798
Compare
| expect( | ||
| checkAndEvaluateMath({ | ||
| value: 'linear-gradient(90deg, #354752 0%, #0b0d0e 100%)', | ||
| type: 'dimension', | ||
| }), | ||
| ).to.equal('linear-gradient(90deg, #354752 0%, #0b0d0e 100%)'); |
There was a problem hiding this comment.
This should be filtered out before this transform, linear gradient on a dimension token doesnt make any sense...
There was a problem hiding this comment.
As the comment says, it's just a smoke-test that ensures we don't handle values that aren't math expressions but do happen to have space seperated substrings. This could be put into its own test.
Math transform applies to all tokens that are strings. It doesn't exclude token types that aren't dimensions. We just need to check that a string value is actually a valid math expression and ignore it if it's not, that's what this expect verifies.
| --spacingSm: 8px; | ||
| --spacingXl: 64px; | ||
| --spacingMultiValue: 8px 64px; /* You can have multiple values in a single spacing token. Read more on these: https://docs.tokens.studio/available-tokens/spacing-tokens#multi-value-spacing-tokens */ | ||
| --spacingMultiValue: 8px 64px; /** You can have multiple values in a single spacing token. Read more on these: https://docs.tokens.studio/available-tokens/spacing-tokens#multi-value-spacing-tokens */ |
There was a problem hiding this comment.
Bug in new SD version? It's either missing an extra * at the end or the extra * should be removed.
There was a problem hiding this comment.
Feature! This makes generated comments, whether multi or single line, compatible with JSDocs, just a small enhancement. This way folks can put for example @deprecated into the comment.
| * Numbers without units will be represented in the units set with an empty string "". | ||
| */ | ||
| export function parseUnits(expr: string): { units: Set<string>; unitlessExpr: string } { | ||
| const unitRegex = /(\d+\.?\d*)(?<unit>([a-zA-Z]|%)+)?/g; |
There was a problem hiding this comment.
Additionally I would throw on any unsupported units which should be definable by the user in the platform config.
For penpot we would allow unitless|px|rem for example
|
@floscr do you mind to sum up a brief PR description? An example on what this PR is changing, would be nice. THX! |
Sure! I've moved internal discussion description and points here, but please concider that this is still a draft PR. |
08daf20 to
fef81ee
Compare
fix: pixel expression check not needed anymore chore: Cleanup
fef81ee to
5ba7cdf
Compare
5ba7cdf to
f2eeb9a
Compare
| mathChars.includes(right) && mathChars.includes(left), // left/right are both math chars | ||
| left === '' && mathChars.includes(right), // tail of expr, right is math char | ||
| right === '' && mathChars.includes(left), // head of expr, left is math char | ||
| mathChars.has(tok), // current token is a math char |
There was a problem hiding this comment.
Nice optimization 👍🏻
| end: '--sdDeepRef: italic 800 26px/1.25 Arial;', | ||
| }); | ||
| const expectedOutput = `--sdCompositionSize: 24px; | ||
| // TODO: Why did this change? |
There was a problem hiding this comment.
SD v5 refactored quite a few things about composite token expansion, optimizing it a lot. I suspect this is why the order of some tokens has changed because in the old SD we expanded tokens and then flattened, in the new SD we first flatten and then expand.
| expect(checkAndEvaluateMath({ value: '4em + 7em', type: 'dimension' })).to.equal('11em'); | ||
| expect(checkAndEvaluateMath({ value: '4 + 7rem', type: 'dimension' })).to.equal('4 + 7rem'); | ||
| expect(checkAndEvaluateMath({ value: '4em + 7rem', type: 'dimension' })).to.equal('4em + 7rem'); | ||
| expect(() => checkAndEvaluateMath({ value: '4 + 7rem', type: 'dimension' })).to.throw( |
There was a problem hiding this comment.
This seems fine by me but we do need to ensure we have an integration test somewhere that verifies that the token value remains unchanged if the math throws an error. So the behavior of the token value is actually the same (which is important for backwards compatibility), except this function throws now.
| expect(checkAndEvaluateMath({ value: '4px * 7em', type: 'dimension' })).to.equal('28em'); | ||
| // 50em would be incorrect when dividing, as em grows, result should shrink, but doesn't | ||
| expect(checkAndEvaluateMath({ value: '1000 / 20em', type: 'dimension' })).to.equal( | ||
| '1000 / 20em', | ||
| expect(() => checkAndEvaluateMath({ value: '4px * 7em', type: 'dimension' })).to.throw( | ||
| MixedUnitsExpressionError, |
There was a problem hiding this comment.
Technically you can mix units with pixels, assuming the unit that you mix with resolves to pixels (e.g. is a multiple of). Same for division.
What I think we should do, also for backwards compatibility, is make sure that we add a platform option "ts/resolveMath:strict" which is set to false by default and allows value mixing where possible. But if strict is set to true, it will throw errors even for unit mixing that technically "can work". We will document this option and recommend it be set to true for everyone, and probably even warn users that in the future we'll probably break this and set it to true by default, because unit-mixing in design token math expressions is just not a good practice. Or we can go a step further and deprecate the option altogether to be forced to strict in the future.
The reason I wanna call it "ts/resolveMath:strict" and essentially namespace the option by the transform name, is because at this time SD only allows passing options to transforms via the platform options. And due to that scope of platform options, I think it's smart that options meant to be used by transforms are namespaced by the transform name for now.
In SD v6 we'll probably redesign that a bit and make it easier to pass options to transforms, rather than via upper scope in a hacky way.
There was a problem hiding this comment.
The unit + unitless mixing is one of the issues that can't quite be solved easily with the current eval-expr, but would be solved by using the custom parser.
jorenbroekema
left a comment
There was a problem hiding this comment.
Looks good for the most part at least on the tests etc.
I think we need to reach a consensus on how we will provide backwards compatibility for right now:
- 2 separate transforms
- 1 transform but with an option (strict) to configure (I prefer this)
and make a plan for how we will do it in the future, e.g.:
- force it to strict and don't allow to configure it (I prefer this, but we have to document clearly and give users quite a long time to move and make their tokens math expr consistent with units)
- set it to strict default but allow not strict as opt-in, etc.
I might have missed it in the tests but I think that combining unitless with unit should always be allowed, given that the unit is consistent everywhere. Then we can just assume that 5rem * 5 = 25rem, and 5px * 5 = 5px etc.
I still have to more thoroughly review the actual implementation changes but that's something more easily done in a call together after this is out of draft state. So far I like what I see though.
|
Thanks for the feedback 🙌 |
Draft PR on sd-transform
checkAndEvaluateMathto get feedback from token-studio team and outside contributers if wanted.Changes
10em + 10pxthis doesn't make sense IMO to resolve as em is relative to the dom context that we don't know about.This is something that wont be allowed from penpot, if we really want to keep the previous way it could be done with a flag.
Issues
Composite token values error handling
Composite token values are an issue for this for this with the new error handling:
It will throw on style and color, which shouldn't be handled in the first place by the math expression.
I think it would make sense to explicitely filter out properties that can be evaluated.
Discussion Points
New major version / New transform / strict property in config?
Possibly we call this
strictCheckAndEvaluateMathwhich is much more simplified and doesn't result in unexpected outputs as the previous version did in regards to unit mixing.If users want to mix units as it would work in css (e.g.:
calc(10em + 10px)) there should be a new transform that runs after this one to wrap into css calc expressions.Px Conversion of unitless at the end
When no unit is given I don't think we should append
pxto it and keep it as an Int.If users want to have px values they should either define it in a config or add an extra transform.