@designesy/score 0.9.2 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine.js +179 -13
- package/package.json +1 -1
package/dist/engine.js
CHANGED
|
@@ -5051,12 +5051,176 @@ function v46FluidFloor(value) {
|
|
|
5051
5051
|
const statics = args.filter((a) => a !== null);
|
|
5052
5052
|
return statics.length ? Math.max(...statics) : null;
|
|
5053
5053
|
}
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5054
|
+
// Engine 1.5.2: the `font` shorthand sets a size too, and until 1.5.2 v46 read
|
|
5055
|
+
// only `font-size`, so `font: 500 0.66rem/1 var(--mono)` sat outside the check.
|
|
5056
|
+
// The size is the first token that is a length, a size keyword or a size
|
|
5057
|
+
// function, ahead of the line-height slash and the family; a weight (500) has
|
|
5058
|
+
// no unit and a family name is no length, so neither is taken. A system font
|
|
5059
|
+
// keyword (caption, menu…) or a CSS-wide keyword sets no measurable size.
|
|
5060
|
+
const V46_FONT_SYSTEM = /^(?:caption|icon|menu|message-box|small-caption|status-bar|inherit|initial|unset|revert|revert-layer)$/i;
|
|
5061
|
+
const V46_SIZE_WORDS = new Set(['small', 'large', 'x-small', 'x-large', 'xx-small', 'xx-large', 'xxx-small', 'xxx-large', 'medium', 'smaller', 'larger']);
|
|
5062
|
+
/** `s` with A to Z lower-cased and every other code unit kept: the case folding of the /i flag without the u flag. */
|
|
5063
|
+
function foldAsciiCase(s) {
|
|
5064
|
+
return s.replace(/[A-Z]+/g, (run) => run.toLowerCase());
|
|
5065
|
+
}
|
|
5066
|
+
/**
|
|
5067
|
+
* Whether a shorthand token is a size, as
|
|
5068
|
+
* /^(?:\d*\.?\d+(?:[a-z]+|%)|(?:xx?-|xxx-)?(?:small|large)|medium|smaller|larger|(?:clamp|min|max|calc)\(.*\))$/i
|
|
5069
|
+
* answers: a number and then a unit or %, a size keyword, or a size function
|
|
5070
|
+
* whose parenthesis closes at the end of the token with no line break inside.
|
|
5071
|
+
* Read in one pass: the number in that pattern can split a run of digits
|
|
5072
|
+
* between \d* and \d+ in as many ways as the run is long.
|
|
5073
|
+
*/
|
|
5074
|
+
function isV46SizeToken(s) {
|
|
5075
|
+
const n = s.length;
|
|
5076
|
+
let k = 0;
|
|
5077
|
+
let dot = false;
|
|
5078
|
+
for (; k < n; k++) {
|
|
5079
|
+
const c = s.charCodeAt(k);
|
|
5080
|
+
if (c === 46) {
|
|
5081
|
+
if (dot)
|
|
5082
|
+
return false; // a second dot ends every reading
|
|
5083
|
+
dot = true;
|
|
5084
|
+
}
|
|
5085
|
+
else if (!isDigit(c))
|
|
5086
|
+
break;
|
|
5087
|
+
}
|
|
5088
|
+
if (k > 0) {
|
|
5089
|
+
if (k === n || s.charCodeAt(k - 1) === 46)
|
|
5090
|
+
return false; // the number ends in a digit, and a unit follows it
|
|
5091
|
+
if (k === n - 1 && s.charCodeAt(k) === 37)
|
|
5092
|
+
return true; // %
|
|
5093
|
+
for (let i = k; i < n; i++) {
|
|
5094
|
+
const c = s.charCodeAt(i) | 32;
|
|
5095
|
+
if (c < 97 || c > 122)
|
|
5096
|
+
return false;
|
|
5097
|
+
}
|
|
5098
|
+
return true;
|
|
5099
|
+
}
|
|
5100
|
+
if (n <= 9 && V46_SIZE_WORDS.has(foldAsciiCase(s)))
|
|
5101
|
+
return true;
|
|
5102
|
+
const open = s.indexOf('(');
|
|
5103
|
+
if (open < 3 || open > 5 || n < open + 2 || s.charCodeAt(n - 1) !== 41)
|
|
5104
|
+
return false;
|
|
5105
|
+
const fn = foldAsciiCase(s.slice(0, open));
|
|
5106
|
+
if (fn !== 'clamp' && fn !== 'min' && fn !== 'max' && fn !== 'calc')
|
|
5107
|
+
return false;
|
|
5108
|
+
for (let i = open + 1; i < n - 1; i++)
|
|
5109
|
+
if (isLineTerminator(s.charCodeAt(i)))
|
|
5110
|
+
return false;
|
|
5111
|
+
return true;
|
|
5112
|
+
}
|
|
5113
|
+
/** The size written inside a `font` shorthand value, or null when it carries none. */
|
|
5114
|
+
function v46ShorthandSize(value, tokens) {
|
|
5115
|
+
const whole = resolveVar(value, tokens); // a whole-shorthand token alias
|
|
5116
|
+
if (V46_FONT_SYSTEM.test(whole))
|
|
5117
|
+
return null;
|
|
5118
|
+
for (const part of splitCssTopLevel(whole.replace(/\//g, ' / ').replace(/\s+/g, ' '), ' ')) {
|
|
5119
|
+
if (part === '/')
|
|
5120
|
+
return null; // a slash before any size: not a shorthand v46 can read
|
|
5121
|
+
if (isV46SizeToken(resolveVar(part, tokens)))
|
|
5122
|
+
return part;
|
|
5123
|
+
}
|
|
5124
|
+
return null;
|
|
5125
|
+
}
|
|
5126
|
+
/**
|
|
5127
|
+
* The declarations v46 reads: matches of
|
|
5128
|
+
* /(font-size|(?<![\w-])font)\s*:\s*([^;}]+)/gi, groups [property, value].
|
|
5129
|
+
* A font-size match is read past its value, as the global pattern reads it.
|
|
5130
|
+
* After a `font` match the scan goes on from the end of the word, so a
|
|
5131
|
+
* shorthand never swallows a font-size written after it. A `font` match is
|
|
5132
|
+
* dropped when it sits inside the value of the shorthand before it (it is
|
|
5133
|
+
* that value's own text), or when its value holds a `{`: an @supports
|
|
5134
|
+
* condition such as `(font: -apple-system-body) and …{` declares nothing.
|
|
5135
|
+
*
|
|
5136
|
+
* Engine 1.5.2. Each start is tried once, and the starts that share a value's
|
|
5137
|
+
* end share one search for it, so the scan reads the sheet in linear time.
|
|
5138
|
+
*/
|
|
5139
|
+
function v46SizeDecls(css) {
|
|
5140
|
+
const start = /font/gi;
|
|
5141
|
+
const sizeWord = /-size/iy;
|
|
5142
|
+
const valueEnd = nextMatchFinder(css, /[;}]/g);
|
|
5143
|
+
const brace = nextMatchFinder(css, /\{/g);
|
|
5144
|
+
const out = [];
|
|
5145
|
+
let shorthandEnd = -1;
|
|
5146
|
+
for (let pos = 0;;) {
|
|
5147
|
+
start.lastIndex = pos;
|
|
5148
|
+
const m = start.exec(css);
|
|
5149
|
+
if (!m)
|
|
5150
|
+
break;
|
|
5151
|
+
const at = m.index;
|
|
5152
|
+
pos = at + 1;
|
|
5153
|
+
sizeWord.lastIndex = at + 4;
|
|
5154
|
+
const size = sizeWord.test(css);
|
|
5155
|
+
if (!size && (at < shorthandEnd || (at > 0 && isNameChar(css.charCodeAt(at - 1)))))
|
|
5156
|
+
continue;
|
|
5157
|
+
const word = size ? 9 : 4;
|
|
5158
|
+
const colon = wsEnd(css, at + word);
|
|
5159
|
+
if (css.charCodeAt(colon) !== 58)
|
|
5160
|
+
continue;
|
|
5161
|
+
const ws = wsEnd(css, colon + 1);
|
|
5162
|
+
let from = ws;
|
|
5163
|
+
let end = ws;
|
|
5164
|
+
if (ws < css.length && css.charCodeAt(ws) !== 59 && css.charCodeAt(ws) !== 125) {
|
|
5165
|
+
const r = valueEnd(ws);
|
|
5166
|
+
end = r === Infinity ? css.length : r;
|
|
5167
|
+
}
|
|
5168
|
+
else if (ws > colon + 1) {
|
|
5169
|
+
from = ws - 1; // only white space before the end: the value is its last character
|
|
5170
|
+
}
|
|
5171
|
+
else
|
|
5172
|
+
continue;
|
|
5173
|
+
if (size) {
|
|
5174
|
+
pos = end;
|
|
5175
|
+
}
|
|
5176
|
+
else {
|
|
5177
|
+
pos = at + 4;
|
|
5178
|
+
if (brace(from) < end)
|
|
5179
|
+
continue;
|
|
5180
|
+
shorthandEnd = end;
|
|
5181
|
+
}
|
|
5182
|
+
out.push({ index: at, text: css.slice(at, end), groups: [css.slice(at, at + word), css.slice(from, end)] });
|
|
5183
|
+
}
|
|
5184
|
+
return out;
|
|
5185
|
+
}
|
|
5186
|
+
/**
|
|
5187
|
+
* The selector of the rule a v46 finding sits in, by offset: the text from
|
|
5188
|
+
* the `{`, `}` or `;` before the last `{` at or before the offset up to that
|
|
5189
|
+
* brace, without comments, white space collapsed, cut to 80 characters (''
|
|
5190
|
+
* when no `{` comes before the offset).
|
|
5191
|
+
*
|
|
5192
|
+
* Engine 1.5.2: the sheet's braces and semicolons are listed once, each offset
|
|
5193
|
+
* is answered by binary search, and the findings of one rule share one
|
|
5194
|
+
* reading of its selector. Before, each finding ran four lastIndexOf scans,
|
|
5195
|
+
* and on a sheet with few braces or semicolons each one read back toward the
|
|
5196
|
+
* start of the sheet, so the check's cost grew with the square of its length.
|
|
5197
|
+
*/
|
|
5198
|
+
function v46SelectorFinder(css) {
|
|
5199
|
+
const opens = [];
|
|
5200
|
+
const marks = [];
|
|
5201
|
+
for (let i = 0; i < css.length; i++) {
|
|
5202
|
+
const c = css.charCodeAt(i);
|
|
5203
|
+
if (c === 123) {
|
|
5204
|
+
opens.push(i);
|
|
5205
|
+
marks.push(i);
|
|
5206
|
+
}
|
|
5207
|
+
else if (c === 125 || c === 59)
|
|
5208
|
+
marks.push(i);
|
|
5209
|
+
}
|
|
5210
|
+
const read = new Map();
|
|
5211
|
+
return (at) => {
|
|
5212
|
+
const k = lowerBound(opens, at + 1) - 1; // the last `{` at or before the offset
|
|
5213
|
+
if (k < 0)
|
|
5214
|
+
return '';
|
|
5215
|
+
const open = opens[k];
|
|
5216
|
+
let selector = read.get(open);
|
|
5217
|
+
if (selector === undefined) {
|
|
5218
|
+
const j = lowerBound(marks, open) - 1; // the `{`, `}` or `;` before it
|
|
5219
|
+
selector = withoutCssComments(css.slice(j < 0 ? 0 : marks[j] + 1, open), ' ').replace(/\s+/g, ' ').trim().slice(0, 80);
|
|
5220
|
+
read.set(open, selector);
|
|
5221
|
+
}
|
|
5222
|
+
return selector;
|
|
5223
|
+
};
|
|
5060
5224
|
}
|
|
5061
5225
|
function checkTypeScale(css, tokens) {
|
|
5062
5226
|
const scale = v46TypeScale(tokens);
|
|
@@ -5078,10 +5242,12 @@ function checkTypeScale(css, tokens) {
|
|
|
5078
5242
|
};
|
|
5079
5243
|
let judged = 0;
|
|
5080
5244
|
let fluid = 0;
|
|
5081
|
-
const
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
const written =
|
|
5245
|
+
const selectorAt = v46SelectorFinder(css);
|
|
5246
|
+
for (const m of v46SizeDecls(css)) { // engine 1.5.2: font-size and the font shorthand
|
|
5247
|
+
const declared = withoutCssComments(m.groups[1], '').replace(/!important/i, '').trim();
|
|
5248
|
+
const written = m.groups[0].length === 4 ? v46ShorthandSize(declared, tokens) : declared;
|
|
5249
|
+
if (written === null)
|
|
5250
|
+
continue;
|
|
5085
5251
|
const value = resolveVar(written, tokens);
|
|
5086
5252
|
if (/^var\(/i.test(value))
|
|
5087
5253
|
continue; // an alias the token table cannot resolve
|
|
@@ -5089,7 +5255,7 @@ function checkTypeScale(css, tokens) {
|
|
|
5089
5255
|
fluid++;
|
|
5090
5256
|
const low = v46FluidFloor(value);
|
|
5091
5257
|
if (low !== null && below(low)) {
|
|
5092
|
-
add({ selector:
|
|
5258
|
+
add({ selector: selectorAt(m.index), value: written, px: Math.round(low * 100) / 100, issue: 'fluid minimum below the floor', nearest: floor });
|
|
5093
5259
|
}
|
|
5094
5260
|
continue;
|
|
5095
5261
|
}
|
|
@@ -5098,10 +5264,10 @@ function checkTypeScale(css, tokens) {
|
|
|
5098
5264
|
continue; // em, %, keywords, calc(): relative or not static
|
|
5099
5265
|
judged++;
|
|
5100
5266
|
if (below(px)) {
|
|
5101
|
-
add({ selector:
|
|
5267
|
+
add({ selector: selectorAt(m.index), value: written, px: Math.round(px * 100) / 100, issue: 'below the floor', nearest: floor });
|
|
5102
5268
|
}
|
|
5103
5269
|
else if (!onScale(px)) {
|
|
5104
|
-
add({ selector:
|
|
5270
|
+
add({ selector: selectorAt(m.index), value: written, px: Math.round(px * 100) / 100, issue: 'off the scale', nearest: nearest(px) });
|
|
5105
5271
|
}
|
|
5106
5272
|
}
|
|
5107
5273
|
const scaleText = `${steps.length}-step scale (ratio ${ratio.toFixed(3)}, ${scale.count} type tokens, floor ${floor}px)`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@designesy/score",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"description": "Design quality gate for AI-generated UI: score any URL on 45 deterministic checks, A–F grade. Runs locally; browser checks return MANUAL. No required dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Designesy <hello@designesy.org>",
|