-
Notifications
You must be signed in to change notification settings - Fork 5
support for parsing arbitrary weight numbers and stretch percentages #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
support for parsing arbitrary weight numbers and stretch percentages #11
Conversation
Codecov Report
@@ Coverage Diff @@
## master #11 +/- ##
=====================================
Coverage 100% 100%
=====================================
Files 2 2
Lines 67 115 +48
Branches 18 34 +16
=====================================
+ Hits 67 115 +48
Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR! Looks like we need to create a new project called parse-css-integer
that would handle some of the logic in this PR though.
src/helpers.ts
Outdated
|| isOnlyNumber(value, true); | ||
} | ||
|
||
function isOnlyNumber(value: string, isPercent: boolean): boolean { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we change the 2nd arg to { isPercent = false } = {}
? It makes the implementation read better:
isOnlyNumber(value, { isPercent: true });
Also, since false
is the default, no need to indicate it:
isOnlyNumber(value);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, instead of isPercent
here, would it make more sense to name it isPercentAccepted
? Or am I misunderstanding something?
src/helpers.ts
Outdated
} | ||
|
||
function isOnlyNumber(value: string, isPercent: boolean): boolean { | ||
const match = /^(\+|-)?(\d|\.)/.exec(value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if provided a solo .
or +.
w/o any digits to follow? Also, you might want to cache this regex outside of the function for (marginal) performance reasons.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, let's exit early below this line with:
if (!match) {
return false;
}
const [sign, value, percent] = match;
This would only work, of course, if the percent were included in the regex like:
const cssNumberPattern = /^(\+|-)?([\d\.]+)(%)?$/;
Which makes me wonder, can't this CSS number parsing be its own package, used outside of parse-css-font
? Or is it only ever used in a font?
Also, what is the value of the isPercent
flag if we can just detect it in the regex?
const isPercent = !!percent;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure about this (changing the regular expression):
-
as to the first question, this regular expression is only a pre-test: any non-number value would get rejected at
parseFloat(val).toString() === val
-
this regular expression is also used for determining if there is leading zero missing for fraction-only values (e.g.
.24
): in this case a zero has to be prepended, so that the testparseFloat(val).toString() === val
works
-> so changing the regular expression would also require to re-implement the test (for checking if the string really only contains a number, or something more, or something else entirely)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either way, for readability, changing the variable names to sign
, value
and percent
would be much more clear.
src/helpers.ts
Outdated
|
||
function isOnlyNumber(value: string, isPercent: boolean): boolean { | ||
const match = /^(\+|-)?(\d|\.)/.exec(value); | ||
if (match && (!isPercent || /%$/.test(value))) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean you're accepting values that end with %
when isPercent = false
? I'm a bit confused by that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the meaning of isPercent
is something like "only return true
if it is number and also has a percent sign", so the naming is probably a bit of a poor choice, maybe onlyPercent
would make it more clear:
it means, either accept any numbers or, if isPercent
is true
, require that a percent sign follows the number
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "and also" part is the confusing part and violates the SRP. Try to make your functions do one and only one thing to clear up any confusion.
src/helpers.ts
Outdated
const match = /^(\+|-)?(\d|\.)/.exec(value); | ||
if (match && (!isPercent || /%$/.test(value))) { | ||
let val = isPercent ? value.substring(0, value.length - 1) : value; | ||
if (match[1] === '+') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the above were implemented, this would read:
if (sign === '+') {
src/helpers.ts
Outdated
} | ||
if (match[2] === '.') { | ||
if (match[1] === '-') { | ||
val = '-0' + val.substring(1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit hard to read. I think some refactoring is in order to make it clear what the intention is here.
src/index.ts
Outdated
} else if (prelimStretchNum) { | ||
font.size = font.stretch; | ||
font.stretch = 'normal'; | ||
font.family = cssListHelpers.splitByCommas(tokens.join(' ')).map(unquote) as string[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto on the as string[]
.
src/index.ts
Outdated
@@ -119,6 +146,23 @@ function parseLineHeight(value: string) { | |||
const parsed = parseFloat(value); | |||
if (parsed.toString() === value) { | |||
return parsed; | |||
} else { | |||
const match = /^(\+|-)?(\.)?/.exec(value) as RegExpMatchArray; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can probably reuse the above-mentioned regex that I asked you to cache here.
Also, the TypeScript compiler will infer the type, so you shouldn't need as RegExpMatchArray
here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
without the cast, the compiler will complain that the match
could be null
:
but the regular expression will always match, since it has 2 optional parts.
So either have this cast, or add a not-null
check to convince the compiler that using match[1]
is OK -- I decided to use the cast, but if you prefer the not-null
check, I will change it.
Similar for those other casts above:
it's i.m.o. a matter of taste, if you use the cast or add an explicit check -- I prefer the cast for instances where I know that the explicit check is unnecessary, as it does not add any JavaScript code after the compilation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you so sure the value wouldn't be null
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it checks for (a) a beginning "^" and (b) two optional groups, so as long as the string has a beginning it will match
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what's wrong w/ your compiler or at what point it's giving you these errors, but I can tell you that the TypeScript playground has no problems with the following code:
function foo(value: string) {
const match = /^(foo)?(bar)?/.exec(value);
return match[1];
}
If I mouse-over const match
it tells me that match
is a RegExpExecArray
.
Are you seeing something different? Note that RegExpExecArray
and RegExpMatchArray
are two different interfaces.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no you are right, it should be RegExpExecArray
(since this was only for silencing the compiler I did not look closely enough)
still: for exec()
my compiler shows me a return value of RegExpExecArray | null
, so without the cast there would be an explicit check for non-null
src/index.ts
Outdated
const match = /^(\+|-)?(\.)?/.exec(value) as RegExpMatchArray; | ||
let val: string = value; | ||
if (match[1] === '+') { | ||
val = val.substring(1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This shouldn't be needed once we deconstruct the match into [sign, value, percent]
.
src/index.ts
Outdated
val = val.substring(1); | ||
} | ||
if (match[2] === '.') { | ||
// NOTE although not specifically prohibited, we do not consider negative numbers for line-height valid: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Were all these comments left here on purpose? Were these issues ever resolved?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, I left this comment on purpose, although I will happily remove it:
The underlying reason for the comment was that I am not really sure why there is a conversion from string
to number
at this point.
I did not want to touch the line-height
parsing code at all, but the test framework requires the conversion of the string
values to number
(otherwise the extended number tests could not have been added, or would have needed to exclude tests where line-height
was part of the test input).
The comment is wrong in that really negative numbers are not allowed for line-height
, but as I said, I did not want to mess around too much with this code as I was not really sure what the intention was here:
why convert to numbers? and why omit the units?
src/index.ts
Outdated
@@ -119,6 +146,23 @@ function parseLineHeight(value: string) { | |||
const parsed = parseFloat(value); | |||
if (parsed.toString() === value) { | |||
return parsed; | |||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another superfluous else
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This else
can still be removed. To be clear, the code inside the else
block can be preserved, but the scope of this block can be unindented by one level.
I will work on those changes & post an update for some that were not fully clear to me, I added comments |
* extract regular expressions for reusing instances * split parse-as-number-only and parse-as-percent-number-only * decompose regular expression match
I think I addressed most of your concerns except for some few where I decided on a slightly different solution: mainly
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking better. Couple more things and I think I'm going to take a stab at this locally to see if any more improvements can be made.
Any thoughts/opinions about extracting any of this logic into a separate project? Specifically, some of the helper functions?
src/index.ts
Outdated
@@ -119,6 +146,23 @@ function parseLineHeight(value: string) { | |||
const parsed = parseFloat(value); | |||
if (parsed.toString() === value) { | |||
return parsed; | |||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This else
can still be removed. To be clear, the code inside the else
block can be preserved, but the scope of this block can be unindented by one level.
src/index.ts
Outdated
@@ -119,6 +140,19 @@ function parseLineHeight(value: string) { | |||
const parsed = parseFloat(value); | |||
if (parsed.toString() === value) { | |||
return parsed; | |||
} else { | |||
const match = /^(\+)?(\.)?/.exec(value) as RegExpExecArray; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add the following to the tsconfig.json
compiler options:
"downlevelIteration": true,
And then you can remove this as RegExpExecArray;
here.
I made the suggested changes. As for a separate number-parsing project: If a separate module, it should be able to parse/handle |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm very concerned by the amount of code that this PR is introducing, so my comments are meant to reduce as much code as possible and defer specific parsing tasks to other packages.
@@ -120,5 +141,17 @@ function parseLineHeight(value: string) { | |||
if (parsed.toString() === value) { | |||
return parsed; | |||
} | |||
const match = /^(\+)?(\.)?/.exec(value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK the more I look at this logic, the more I think installing parse-css-dimension
might be useful in replacing this logic. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No broken tests with the following implementation:
function parseLineHeight(value: string) {
try {
const parsed = parseCssDimension(value);
if (parsed.type === 'number') {
return parsed.value;
}
} catch (err) {
// noop
}
return value;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes that would probably be better -- ideally the parsing should also be extended to reject negative values for line height (and invalid dimensions/units)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see what you mean. Looks like there's a validation aspect that needs to be employed here. I just tried this:
.foo {
line-height: -1;
}
on http://jigsaw.w3.org/css-validator/#validate_by_input and got the following error:
Value Error : line-height -1 negative values are not allowed
We could easily add the following:
if (parsed.value < 0) {
throw error(`Value Error: line-height: ${parsed.value}; negative values are not allowed.`);
}
return false; | ||
} | ||
|
||
function isOnlyNumber(value: string): boolean { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
parse-css-dimension
might also be useful here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure:
the "allow number only" parsing is not really provided by parse-css-dimension
, or more precisely, it does not make sure that the input-string does not contain more than the number and/or the the unit
E.g. for "43asdfa7rem"
it will return a valid result with number 43
and unit "rem"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aha. Sounds like it needs a strict mode then. That can be arranged.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, then most of this PR should be removed (or moved to parse-css-dimension
where appropriate) & replaced by using parse-css-dimension
Would you start on parse-css-dimension
, e.g. how it should be invoked for "strict mode"?
e.g. maybe something like parseCssDimension('42dpi', 'strict')
or maybe parseCssDimension('42dpi', {strict: true})
, or ...?
I would then transfer/add parts from this PR for the strict-number testing as much as possible
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should go with an options hash of { strict: true }
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@russaa see jednano/parse-css-dimension#4. Also, I'm updating that project to TypeScript/AVA like this one. Feel free to submit a PR for jednano/parse-css-dimension#4.
@@ -6,6 +6,57 @@ const fontWeightKeywords = require('css-font-weight-keywords'); | |||
const fontStyleKeywords = require('css-font-style-keywords'); | |||
const fontStretchKeywords = require('css-font-stretch-keywords'); | |||
|
|||
const numberValues = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you'll find that many of these tests cases are covered here, so as not to reinvent the wheel.
Closing for now, due to inactivity. |
@russaa I have a 🎁 for you: https://www.npmjs.com/package/parse-css-number |
@jedmao very nice 😃 using |
adds support for parsing extended capabilities defined in CSS Fonts Module Level 4 for
font-weight
: allow arbitrary numbers from 1 to 1000font-stretch
: allow (positive) percentage numbersthis PR also extends parsing of valid numbers:
namely considering leading
+
and-
when parsing numbersSee also
https://developer.mozilla.org/en-US/docs/Web/CSS/integer
https://drafts.csswg.org/css-values-3/#numeric-types