synthesisui 0.16.406 → 0.16.408
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/absorb-plan.js +77 -15
- package/dist/commands/absorb.js +35 -6
- package/dist/commands/doctor.js +10 -4
- package/dist/doctor/apply-fix.js +66 -3
- package/dist/doctor/ci-format.js +22 -1
- package/dist/doctor/ledger.js +18 -1
- package/dist/doctor/scan.js +100 -16
- package/dist/install-marks.js +18 -2
- package/dist/proposed-name.js +161 -1
- package/dist/use-by-property.js +143 -38
- package/package.json +1 -1
package/dist/absorb-plan.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { deltaE, JND } from "./doctor/color-distance.js";
|
|
2
2
|
import { familySays, nearestToken, normalizeValue, } from "./doctor/tokens.js";
|
|
3
|
-
import { proposeFromTheirScale, proposeFromTheirUse, tooCloseToPropose, whyNotProposedFromUse, } from "./proposed-name.js";
|
|
3
|
+
import { proposeFromTheirScale, proposeFromTheirUse, proposeFromTheirValue, tooCloseToPropose, whyNotProposedFromUse, whyNotProposedFromValue, } from "./proposed-name.js";
|
|
4
4
|
/** Onde cada tipo de valor mora na fundação. `color` é o único com dois segmentos. */
|
|
5
5
|
const HOME = {
|
|
6
6
|
color: "color",
|
|
@@ -116,7 +116,13 @@ export function absorbPlan(d, theirs, have, cap,
|
|
|
116
116
|
* ocorrência do defeito em que a tela promete o que a chamada não passou. Quem não tem a contagem
|
|
117
117
|
* passa um mapa vazio e a fonte 2 cala - explicitamente.
|
|
118
118
|
*/
|
|
119
|
-
uses
|
|
119
|
+
uses,
|
|
120
|
+
/**
|
|
121
|
+
* A RAIZ MEDIDA no projeto dele, com a ambiguidade - e OBRIGATÓRIA pelo mesmo motivo que `uses`:
|
|
122
|
+
* `theirs.rootPx` carrega o número e perde o "não sei", e é o "não sei" que impede a proposta de
|
|
123
|
+
* afirmar `spacing.32` sobre um `2rem` de um projeto com duas raízes. Ver `unconvertible`.
|
|
124
|
+
*/
|
|
125
|
+
root) {
|
|
120
126
|
/**
|
|
121
127
|
* A REPETIÇÃO É UM FILTRO DE RUÍDO, E RUÍDO É UMA PROPRIEDADE DA ESCALA - não do valor.
|
|
122
128
|
*
|
|
@@ -220,14 +226,27 @@ uses) {
|
|
|
220
226
|
* A fonte 1 é escala de cor: medido no `frontend-hub` real em 08/09, ela propôs
|
|
221
227
|
* `dashboard.blue.650` para `30px`, `40px` e `50px`, porque `lightness("30px")` lia um prefixo
|
|
222
228
|
* hexadecimal de "3300pp" - consertado na raiz em `color-distance.ts`, e a porta fechada aqui
|
|
223
|
-
* também. A fonte 2
|
|
224
|
-
*
|
|
225
|
-
* sendo o de `nearestOwn`.
|
|
229
|
+
* também. A fonte 2 (a FUNÇÃO) é só para cor porque a medição de 08/09 mostrou que a função não
|
|
230
|
+
* separa espaçamento (`8px` é 409× padding e 303× margin no `web-subscribe`): comprimento é
|
|
231
|
+
* proposto pelo próprio VALOR, logo abaixo. O `≈` de vizinhança continua sendo o de `nearestOwn`.
|
|
226
232
|
*/
|
|
227
|
-
const
|
|
233
|
+
const colour = r.kind !== "color" || theirName || tooCloseToPropose(r.literal, theirs)
|
|
228
234
|
? null
|
|
229
235
|
: (proposeFromTheirScale(r.literal, theirs) ??
|
|
230
236
|
proposeFromTheirUse(r.literal, uses, theirs.rootPx, r.count));
|
|
237
|
+
/** "≈ perto de um token seu" só interessa a quem não tem um EXATO - com o nome na mão, a dica
|
|
238
|
+
* vira ruído, e no arquivo de proposta ela vira uma segunda opção que não é opção. */
|
|
239
|
+
const near = theirName ? undefined : nearestOwn(theirs, r.literal, r.kind);
|
|
240
|
+
/**
|
|
241
|
+
* ESPAÇO E RAIO: A TERCEIRA FONTE, e a ordem é a D5 (lead, 08/09) - nome dele > `≈` um token
|
|
242
|
+
* dele > o próprio valor. O `≈` vem ANTES da proposta porque um `15px` a 1px do `--spacing-md`
|
|
243
|
+
* dele é a mesma decisão escrita duas vezes, e propor `spacing.15` ali empurraria para BATIZAR
|
|
244
|
+
* o que a leitura já diz para NORMALIZAR. Ver `proposeFromTheirValue`.
|
|
245
|
+
*/
|
|
246
|
+
const length = (r.kind === "spacing" || r.kind === "radius") && !theirName && !near
|
|
247
|
+
? proposeFromTheirValue(r.literal, r.kind, uses, root, r.count)
|
|
248
|
+
: null;
|
|
249
|
+
const proposal = colour ?? length;
|
|
231
250
|
const own = pathFor(r.kind, theirName);
|
|
232
251
|
/** Já existe na fundação com OUTRO valor: absorver aqui seria repintar o token deles. */
|
|
233
252
|
if (own && have.has(own))
|
|
@@ -245,17 +264,24 @@ uses) {
|
|
|
245
264
|
const path = own || (collided || rival ? "" : (proposal?.path ?? ""));
|
|
246
265
|
if (proposal !== null && !own && !collided && !rival)
|
|
247
266
|
taken.set(proposal.path, { literal: r.literal, count: r.count });
|
|
248
|
-
/**
|
|
249
|
-
*
|
|
250
|
-
|
|
251
|
-
|
|
267
|
+
/**
|
|
268
|
+
* E QUANDO AS FONTES CALAM E NÃO HÁ VIZINHO, O CAMPO EM BRANCO DIZ POR QUÊ - em toda natureza.
|
|
269
|
+
* Cor: a função medida. Espaço e raio: a unidade, o degrau, o zero, o negativo. Motion: a
|
|
270
|
+
* contagem, e que duração não é proposta ainda. E o que nem chegou ao passe: o motivo genérico.
|
|
271
|
+
* Nenhuma linha em branco calada é a promessa da `INV-VOC-08`, estendida às outras naturezas
|
|
272
|
+
* em 08/09.
|
|
273
|
+
*/
|
|
274
|
+
const silent = !theirName && !proposal && !near;
|
|
252
275
|
const unproposed = collided
|
|
253
|
-
? `would be ${proposal?.path}, but you already named a different colour that way`
|
|
276
|
+
? `would be ${proposal?.path}, but you already named a different ${r.kind === "color" ? "colour" : "value"} that way`
|
|
254
277
|
: rival
|
|
255
278
|
? `would be ${proposal?.path}, but ${rival.literal} (${rival.count}×) takes that position this round - name one of them and the other follows`
|
|
256
|
-
: r.kind === "color"
|
|
279
|
+
: silent && r.kind === "color"
|
|
257
280
|
? whyNotProposedFromUse(r.literal, uses, theirs.rootPx, r.count)
|
|
258
|
-
:
|
|
281
|
+
: silent && r.kind !== "font"
|
|
282
|
+
? (whyNotProposedFromValue(r.literal, r.kind, uses, root, r.count) ??
|
|
283
|
+
"never inside a style property - declared or computed, not painted")
|
|
284
|
+
: null;
|
|
259
285
|
entries.push({
|
|
260
286
|
value: r.literal,
|
|
261
287
|
kind: r.kind,
|
|
@@ -277,6 +303,28 @@ uses) {
|
|
|
277
303
|
unnamed: d.repeats.length + d.once.length,
|
|
278
304
|
};
|
|
279
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* NO TAILWIND v4 O NOME É A UTILIDADE - e aprovar `spacing.16` muda o que `p-16` já significa.
|
|
308
|
+
*
|
|
309
|
+
* `absorb --send` no caminho "code" faz ele colar `@theme { --spacing-16: 16px }`, e na v4
|
|
310
|
+
* `--spacing-<n>` é o namespace que resolve `p-16`, `m-16`, `gap-16`, `w-16`, `size-16` e
|
|
311
|
+
* `space-y-16`. Com o padrão (`--spacing: 0.25rem`), `p-16` vale 64px; depois do paste vale 16px, e
|
|
312
|
+
* o projeto dele muda inteiro, calado. Todo degrau de 1 a 96 colide, sempre.
|
|
313
|
+
*
|
|
314
|
+
* A PLATAFORMA NÃO MUDA O NOME (decisão do lead, 08/09, reabrível): o degrau é o VALOR dele, e isso
|
|
315
|
+
* é a promessa da `INV-VOC-09`. Ela DECLARA a consequência quando a stack é v4 - `tailwindMajor` já
|
|
316
|
+
* sabe qual é -, na linha da proposta e no bloco que ele cola. Renomear é escolha dele, no arquivo.
|
|
317
|
+
*
|
|
318
|
+
* Uma frase, um lugar: a lista e o bloco a leem daqui, senão as duas telas divergem no dia em que
|
|
319
|
+
* alguém editar uma.
|
|
320
|
+
*/
|
|
321
|
+
export function spacingUtilitiesRedefined(paths) {
|
|
322
|
+
const hit = paths.find((p) => /^spacing\.\d+$/.test(p));
|
|
323
|
+
if (!hit)
|
|
324
|
+
return null;
|
|
325
|
+
const step = hit.slice("spacing.".length);
|
|
326
|
+
return `On Tailwind v4 the name IS the utility: --spacing-${step} is what p-${step}, m-${step}, gap-${step} and w-${step} read, so approving ${hit} changes what those already mean in your project. The value is yours - rename the step in the file if you want the utilities untouched.`;
|
|
327
|
+
}
|
|
280
328
|
/** Quantas linhas da proposta ainda precisam de um nome humano. */
|
|
281
329
|
export const needingName = (plan) => plan.entries.filter((e) => !e.path);
|
|
282
330
|
/**
|
|
@@ -286,7 +334,14 @@ export const needingName = (plan) => plan.entries.filter((e) => !e.path);
|
|
|
286
334
|
* um repo em migração já tem nome no vocabulário do próprio cliente, e essas viajam sem ninguém digitar
|
|
287
335
|
* nada.
|
|
288
336
|
*/
|
|
289
|
-
export function describeAbsorb(plan
|
|
337
|
+
export function describeAbsorb(plan,
|
|
338
|
+
/**
|
|
339
|
+
* A STACK DELE, e OBRIGATÓRIA de propósito: o aviso do namespace da v4 (ver
|
|
340
|
+
* `spacingUtilitiesRedefined`) é a metade que faz a proposta ser honesta naquele projeto, e um
|
|
341
|
+
* argumento opcional que dá para esquecer é a próxima ocorrência do defeito em que a tela promete
|
|
342
|
+
* o que a chamada não passou. `false` é resposta explícita.
|
|
343
|
+
*/
|
|
344
|
+
tailwind4) {
|
|
290
345
|
/**
|
|
291
346
|
* O QUE ELE JÁ NOMEIA E O QUE A PLATAFORMA PROPÔS SÃO DUAS LISTAS - e juntá-las é dizer que ele
|
|
292
347
|
* nomeou o que ele não nomeou.
|
|
@@ -332,11 +387,18 @@ export function describeAbsorb(plan) {
|
|
|
332
387
|
if (proposed.length > 0) {
|
|
333
388
|
if (ready.length > 0)
|
|
334
389
|
lines.push("");
|
|
335
|
-
lines.push(`${proposed.length} ${proposed.length === 1 ? "value has" : "values have"} a name PROPOSED from your own vocabulary - your scale,
|
|
390
|
+
lines.push(`${proposed.length} ${proposed.length === 1 ? "value has" : "values have"} a name PROPOSED from your own vocabulary - your scale, how you use the value, or the value itself - approve, edit, or leave ${proposed.length === 1 ? "it" : "them"} hard-coded:`);
|
|
336
391
|
for (const e of proposed.slice(0, 8))
|
|
337
392
|
lines.push(` ${e.value.padEnd(24)} ${e.path.padEnd(28)} ${e.proposed}`);
|
|
338
393
|
if (proposed.length > 8)
|
|
339
394
|
lines.push(` (${proposed.length - 8} more)`);
|
|
395
|
+
const redefined = tailwind4
|
|
396
|
+
? spacingUtilitiesRedefined(proposed.map((e) => e.path))
|
|
397
|
+
: null;
|
|
398
|
+
if (redefined) {
|
|
399
|
+
lines.push("");
|
|
400
|
+
lines.push(redefined);
|
|
401
|
+
}
|
|
340
402
|
}
|
|
341
403
|
if (pending.length > 0) {
|
|
342
404
|
const close = pending.filter((e) => e.near);
|
package/dist/commands/absorb.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import { absorbPlan, describeAbsorb, needingName, } from "../absorb-plan.js";
|
|
3
|
+
import { absorbPlan, describeAbsorb, needingName, spacingUtilitiesRedefined, } from "../absorb-plan.js";
|
|
4
4
|
import { readProjectConfig, readToken, resolveRegistry } from "../config.js";
|
|
5
|
+
import { DEFAULT_ROOT_PX, rootSizeOf } from "../doctor/root-size.js";
|
|
5
6
|
import { diagnose, IS_FIXTURE, scanSource, } from "../doctor/scan.js";
|
|
6
7
|
import { withTheirNames } from "../doctor/their-names.js";
|
|
7
8
|
import { measuredScope, scopePaths } from "../measured-scope.js";
|
|
8
9
|
import { body, paint, section, snippet } from "../output.js";
|
|
9
10
|
import { resolveDeps, tailwindMajor } from "../stack.js";
|
|
10
11
|
import { countUseByProperty } from "../use-by-property.js";
|
|
11
|
-
import { loadSystem, walkAll } from "./doctor.js";
|
|
12
|
+
import { loadSystem, sheetsIn, walkAll } from "./doctor.js";
|
|
12
13
|
/**
|
|
13
14
|
* `synthesisui absorb` - O SISTEMA APRENDE O DESIGN QUE O CÓDIGO JÁ TEM.
|
|
14
15
|
*
|
|
@@ -34,7 +35,26 @@ export async function absorb(opts) {
|
|
|
34
35
|
const path = join(root, FILE);
|
|
35
36
|
if (opts.send)
|
|
36
37
|
return sendProposal(root, path, opts.registry);
|
|
37
|
-
|
|
38
|
+
/**
|
|
39
|
+
* A RAIZ, MEDIDA ANTES DE PROPOR NOME NENHUM - e sem isto este comando AFIRMA um número falso.
|
|
40
|
+
*
|
|
41
|
+
* Este comando chamava `loadSystem(root)` sem a raiz medida, então `1rem` valia 16px sempre. Até a
|
|
42
|
+
* Parte 2 isso só o fazia CASAR menos - um `0.25rem` dele não achava o token em `4px`, e o erro
|
|
43
|
+
* era por omissão. A proposta pelo valor (`INV-VOC-09`) transformou a omissão em afirmação: num
|
|
44
|
+
* projeto que escreve `html { font-size: 62.5% }`, um `padding: 2rem` saía como `spacing.32` e
|
|
45
|
+
* *"32px, used 2× as margin and padding"* - e `2rem` ali vale 20px. Um nome errado e um número
|
|
46
|
+
* falso no terminal dele, sobre o repositório dele. Medido no comando real em 08/09.
|
|
47
|
+
*
|
|
48
|
+
* A medição é a MESMA do `doctor` (`rootSizeOf` sobre as folhas de estilo, só o bloco
|
|
49
|
+
* `html`/`:root`), sobre a RAIZ do projeto - que é a população de onde `loadSystem` colhe o
|
|
50
|
+
* vocabulário dele, mesmo quando a leitura é escopada. Ambíguo (duas raízes, um `calc()`) cai no
|
|
51
|
+
* padrão do navegador, como no `doctor`: quem decide isso é `root-size.ts`, não este comando.
|
|
52
|
+
*/
|
|
53
|
+
const rootSize = rootSizeOf(await sheetsIn([root]));
|
|
54
|
+
const installed = await loadSystem(root, {
|
|
55
|
+
px: rootSize.ambiguous ? DEFAULT_ROOT_PX : rootSize.px,
|
|
56
|
+
from: rootSize.from,
|
|
57
|
+
});
|
|
38
58
|
/**
|
|
39
59
|
* SEM SISTEMA INSTALADO ISTO CONTINUA VALENDO - e recusar aqui fechava o dia 1.
|
|
40
60
|
*
|
|
@@ -99,15 +119,17 @@ export async function absorb(opts) {
|
|
|
99
119
|
const rel = file.slice(root.length + 1);
|
|
100
120
|
reports.push(scanSource(rel, source, table));
|
|
101
121
|
if (!IS_FIXTURE.test(rel))
|
|
102
|
-
countUseByProperty(source, uses,
|
|
122
|
+
countUseByProperty(source, uses, rootSize);
|
|
103
123
|
}
|
|
104
|
-
const plan = absorbPlan(diagnose(reports), theirs, pathsInSystem(installed.documents), opts.cap ?? 40, uses);
|
|
124
|
+
const plan = absorbPlan(diagnose(reports), theirs, pathsInSystem(installed.documents), opts.cap ?? 40, uses, rootSize);
|
|
105
125
|
console.log(section(hasSystem
|
|
106
126
|
? "What the system could absorb"
|
|
107
127
|
: "Name what you wrote by hand"));
|
|
108
128
|
console.log(body(paint.dim(`read from ${rel.length > 0 ? rel.join(", ") : "this project"} · your own vocabulary read from the root`)));
|
|
109
129
|
console.log("");
|
|
110
|
-
|
|
130
|
+
/** A stack dele decide uma frase da lista - ver `spacingUtilitiesRedefined`. */
|
|
131
|
+
const major = tailwindMajor(await resolveDeps(root));
|
|
132
|
+
for (const line of describeAbsorb(plan, major === 4))
|
|
111
133
|
console.log(body(line));
|
|
112
134
|
if (plan.entries.length === 0)
|
|
113
135
|
return;
|
|
@@ -225,6 +247,13 @@ async function sendProposal(root, path, registry) {
|
|
|
225
247
|
for (const e of ready)
|
|
226
248
|
console.log(` --${e.path.replace(/\./g, "-")}: ${e.value};`);
|
|
227
249
|
console.log(" }");
|
|
250
|
+
/** E O QUE ESSE PASTE MUDA NO PROJETO DELE, dito antes de ele colar - ver
|
|
251
|
+
* `spacingUtilitiesRedefined`. Na v4 o nome É a utilidade. */
|
|
252
|
+
const redefined = major === 4 ? spacingUtilitiesRedefined(ready.map((e) => e.path)) : null;
|
|
253
|
+
if (redefined) {
|
|
254
|
+
console.log("");
|
|
255
|
+
console.log(body(redefined));
|
|
256
|
+
}
|
|
228
257
|
console.log("");
|
|
229
258
|
console.log(body(major === 4
|
|
230
259
|
? "In `@theme` and not `:root`: on Tailwind v4 only `@theme` generates the utility, so this is what makes `text-ink-700` exist in your code."
|
package/dist/commands/doctor.js
CHANGED
|
@@ -8,7 +8,7 @@ import { emptyTally, internalSpecifiers, scanComponentsInto, tallyToInventory, }
|
|
|
8
8
|
import { checkContracts } from "../doctor/contract-check.js";
|
|
9
9
|
import { describeMissing, missingDependencies, summarizeMissing, } from "../doctor/dependencies.js";
|
|
10
10
|
import { findFrozenBindings } from "../doctor/frozen.js";
|
|
11
|
-
import { appendEvent, COVERAGE_RULE, readEvents, suggestionsFrom, summarize, unrepresentedFrom, } from "../doctor/ledger.js";
|
|
11
|
+
import { appendEvent, COVERAGE_RULE, COVERAGE_RULE_REASON, readEvents, suggestionsFrom, summarize, unrepresentedFrom, } from "../doctor/ledger.js";
|
|
12
12
|
import { bindingsFromDocument, countComponents, findOverrides, } from "../doctor/overrides.js";
|
|
13
13
|
import { checkableName, readRequests, verifyAndCloseRequests, } from "../doctor/requests.js";
|
|
14
14
|
import { DEFAULT_ROOT_PX, rootSizeOf, saidOfRoot, } from "../doctor/root-size.js";
|
|
@@ -385,7 +385,7 @@ measured) {
|
|
|
385
385
|
* common path pays nothing for it.
|
|
386
386
|
*/
|
|
387
387
|
/** As folhas de estilo dele, para a medição da raiz. Só css - nada de `.tsx`. */
|
|
388
|
-
async function sheetsIn(roots) {
|
|
388
|
+
export async function sheetsIn(roots) {
|
|
389
389
|
const out = [];
|
|
390
390
|
for await (const file of walkAll(roots)) {
|
|
391
391
|
if (!/\.(css|scss|sass|less)$/i.test(file))
|
|
@@ -1419,9 +1419,15 @@ export async function doctor(opts) {
|
|
|
1419
1419
|
* para 37% sem uma linha de código mudada. Somar as duas fotos anunciaria um progresso
|
|
1420
1420
|
* inventado; calar a mudança deixaria alguém procurar o que fez o número pular.
|
|
1421
1421
|
*/
|
|
1422
|
-
/**
|
|
1422
|
+
/**
|
|
1423
|
+
* E SEM O NÚMERO DA VERSÃO: o que mudou a conta é a conta, não a nossa release.
|
|
1424
|
+
*
|
|
1425
|
+
* A RAZÃO VEM DA RÉGUA, nunca de uma frase cravada aqui: a de 13/08 (*"your own tokens count
|
|
1426
|
+
* now too"*) continuou impressa quando a régua subiu por outro motivo em 09/09, explicando a
|
|
1427
|
+
* causa errada para quem lê. Ver `COVERAGE_RULE_REASON`.
|
|
1428
|
+
*/
|
|
1423
1429
|
if (record.ruleChanged)
|
|
1424
|
-
console.log(body(
|
|
1430
|
+
console.log(body(` the way this is counted changed - ${COVERAGE_RULE_REASON[COVERAGE_RULE] ?? "the ruler moved"}, so the trend restarts here.`));
|
|
1425
1431
|
}
|
|
1426
1432
|
}
|
|
1427
1433
|
/**
|
package/dist/doctor/apply-fix.js
CHANGED
|
@@ -96,6 +96,30 @@ function wholeValue(line, at, literal) {
|
|
|
96
96
|
const after = line[at + literal.length] ?? "";
|
|
97
97
|
return !/[0-9a-z#]/i.test(before) && !/[0-9a-z]/i.test(after);
|
|
98
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* A troca nas colunas medidas, da DIREITA para a esquerda: escrever `var(--x)` é mais longo que o
|
|
101
|
+
* literal, e fazer isso da esquerda para a direita moveria as colunas seguintes.
|
|
102
|
+
*/
|
|
103
|
+
function swapAtCols(line, literal, name, cols) {
|
|
104
|
+
const guarded = selfRanges(line, name);
|
|
105
|
+
let out = line;
|
|
106
|
+
let count = 0;
|
|
107
|
+
let blocked = 0;
|
|
108
|
+
for (const at of [...cols].sort((a, b) => b - a)) {
|
|
109
|
+
/** A linha mudou desde a medição: o comando tem um motivo para isso, e ele não é este. */
|
|
110
|
+
if (!out.startsWith(literal, at))
|
|
111
|
+
continue;
|
|
112
|
+
if (!wholeValue(out, at, literal))
|
|
113
|
+
continue;
|
|
114
|
+
if (guarded.some(([start, end]) => at >= start && at + literal.length <= end)) {
|
|
115
|
+
blocked++;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
count++;
|
|
119
|
+
out = `${out.slice(0, at)}var(${name})${out.slice(at + literal.length)}`;
|
|
120
|
+
}
|
|
121
|
+
return { line: out, count, blocked };
|
|
122
|
+
}
|
|
99
123
|
/** Onde este valor está escrito INTEIRO nesta linha - a mesma pergunta que a troca faz. */
|
|
100
124
|
function wholeValuesIn(line, literal) {
|
|
101
125
|
const out = [];
|
|
@@ -106,7 +130,21 @@ function wholeValuesIn(line, literal) {
|
|
|
106
130
|
}
|
|
107
131
|
return out;
|
|
108
132
|
}
|
|
109
|
-
|
|
133
|
+
/**
|
|
134
|
+
* A TROCA ACONTECE NAS COLUNAS QUE A VARREDURA MEDIU - ver `Finding.cols`.
|
|
135
|
+
*
|
|
136
|
+
* Trocar TODA ocorrência do literal na linha era o defeito que o contador de shorthand destapou:
|
|
137
|
+
* com um achado de `8px` em `padding: 4px 8px`, o comando reescrevia também o `width: 8px`, o
|
|
138
|
+
* `top: 8px`, o `font-size: 8px` e os dois offsets de `box-shadow` da mesma linha - seis escritas
|
|
139
|
+
* onde havia um achado. A/B medido em 09/09 com os dois binários sobre o mesmo arquivo.
|
|
140
|
+
*
|
|
141
|
+
* SEM COLUNAS, O COMPORTAMENTO É O DE ANTES, e isso é deliberado: um literal DERIVADO (`duration-300`
|
|
142
|
+
* -> `300ms`) não está escrito na linha, então não há coluna para ele, e a resposta certa continua
|
|
143
|
+
* sendo a que o comando já dá (nada foi escrito, e ele diz por quê).
|
|
144
|
+
*/
|
|
145
|
+
function swapAll(line, literal, name, cols) {
|
|
146
|
+
if (cols && cols.length > 0)
|
|
147
|
+
return swapAtCols(line, literal, name, cols);
|
|
110
148
|
const guarded = selfRanges(line, name);
|
|
111
149
|
let out = "";
|
|
112
150
|
let from = 0;
|
|
@@ -168,8 +206,17 @@ export function planFix(d, read) {
|
|
|
168
206
|
* DE BAIXO PARA CIMA no arquivo não é necessário - a troca não muda a contagem de linhas - mas a
|
|
169
207
|
* ORDEM por arquivo é, para dois achados na mesma linha não se atropelarem: o segundo procura o seu
|
|
170
208
|
* literal no texto que o primeiro já deixou.
|
|
209
|
+
*
|
|
210
|
+
* E DA DIREITA PARA A ESQUERDA DENTRO DA LINHA, desde que a troca acontece nas COLUNAS medidas
|
|
211
|
+
* (ver `Finding.cols`): `var(--ds-spacing-8)` é mais longo que `8px`, então trocar o primeiro
|
|
212
|
+
* valor de um `padding: 8px 24px` empurra o segundo para a direita e a coluna dele deixa de
|
|
213
|
+
* casar - o `24px` ficava literal, e o comando dizia que a linha tinha mudado desde a medição.
|
|
214
|
+
* Medido no próprio caso desta rodada, com o teste ponta a ponta em `apply-fix.spec.ts`.
|
|
171
215
|
*/
|
|
172
|
-
|
|
216
|
+
const rightToLeft = [...d.findings].sort((a, b) => a.file === b.file && a.line === b.line
|
|
217
|
+
? Math.max(...(b.cols ?? [-1])) - Math.max(...(a.cols ?? [-1]))
|
|
218
|
+
: 0);
|
|
219
|
+
for (const f of rightToLeft) {
|
|
173
220
|
/**
|
|
174
221
|
* O NOME DELE GANHA - ver `nameToWrite` em `scan.ts`.
|
|
175
222
|
*
|
|
@@ -286,7 +333,7 @@ export function planFix(d, read) {
|
|
|
286
333
|
const already = `${f.file}:${f.line}:${f.literal}`;
|
|
287
334
|
if (done.has(already))
|
|
288
335
|
continue;
|
|
289
|
-
const swapped = current === undefined ? null : swapAll(current, f.literal, name);
|
|
336
|
+
const swapped = current === undefined ? null : swapAll(current, f.literal, name, f.cols);
|
|
290
337
|
/**
|
|
291
338
|
* TODA OCORRÊNCIA DA LINHA ERA A PRÓPRIA DECLARAÇÃO - o caso que o guard sempre recusou, dito
|
|
292
339
|
* com a mesma frase de antes. O que mudou é que uma linha que declara E usa deixou de cair aqui.
|
|
@@ -469,5 +516,21 @@ export function describeFix(result, dry) {
|
|
|
469
516
|
lines.push(`${unread} file${unread === 1 ? "" : "s"} could not be read.`);
|
|
470
517
|
if (decisions > 0)
|
|
471
518
|
lines.push(`${decisions} finding${decisions === 1 ? "" : "s"} left: values your system has no name for. Those are decisions - name them, or file a request: npx synthesisui request token`);
|
|
519
|
+
/**
|
|
520
|
+
* OS QUE SOBRARAM NA LINHA QUE O COMANDO ACABOU DE REESCREVER - e sem esta linha o recibo não os
|
|
521
|
+
* liga a nada.
|
|
522
|
+
*
|
|
523
|
+
* Desde que cada comprimento de um shorthand tem achado próprio, uma linha volta do `--fix` com
|
|
524
|
+
* token E literal lado a lado: `padding: 8px var(--ds-spacing-24)`. O número de "sobraram" já
|
|
525
|
+
* aparecia, mas não dizia que parte dele está em linhas que este mesmo comando tocou - e é
|
|
526
|
+
* justamente onde ele vai olhar primeiro quando abrir o diff.
|
|
527
|
+
*
|
|
528
|
+
* Mesmo formato das outras linhas por motivo (`selfRef`, `notWritten`, `relative`): o número, o
|
|
529
|
+
* porquê, e o comando para ver.
|
|
530
|
+
*/
|
|
531
|
+
const rewritten = new Set(applied.map((a) => `${a.file}:${a.line}`));
|
|
532
|
+
const halfWritten = skipped.filter((s) => rewritten.has(`${s.file}:${s.line}`)).length;
|
|
533
|
+
if (halfWritten > 0)
|
|
534
|
+
lines.push(` ${halfWritten} of those ${halfWritten === 1 ? "sits" : "sit"} on a line this command just rewrote - a shorthand comes back with a token and a literal side by side (\`padding: 8px var(--ds-spacing-24)\`). See them by value: npx synthesisui doctor --migrate`);
|
|
472
535
|
return lines;
|
|
473
536
|
}
|
package/dist/doctor/ci-format.js
CHANGED
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
* NADA AQUI FALA COM A REDE nem pede login. É a mesma decisão que mantém o hook instalado: uma
|
|
23
23
|
* checagem que telefona para casa é uma checagem que alguém desinstala.
|
|
24
24
|
*/
|
|
25
|
+
import { isOlderCli } from "../cli-version.js";
|
|
26
|
+
import { CHECKER_SINCE } from "../install-marks.js";
|
|
25
27
|
const byFileOf = (d) => {
|
|
26
28
|
const out = {};
|
|
27
29
|
for (const f of d.files)
|
|
@@ -63,8 +65,23 @@ export function compareToBaseline(d, base, now) {
|
|
|
63
65
|
const drift = { was: base.totals.drift, now: d.findings.length };
|
|
64
66
|
const phantoms = { was: base.totals.phantoms, now: d.phantomUses };
|
|
65
67
|
const scopeChanged = (base.scope ?? "") !== (now.scope ?? "");
|
|
68
|
+
/**
|
|
69
|
+
* O LEITOR MUDOU DESDE O BASELINE, e aí a subida é NOSSA - reprovar o PR dele por isso é o alarme
|
|
70
|
+
* falso que ensina um time a passar `--no-verify`.
|
|
71
|
+
*
|
|
72
|
+
* MEDIDO EM 09/09: um baseline escrito pela 0.16.407 dizia 8799, a rodada de hoje contou 9531 -
|
|
73
|
+
* cada comprimento de um shorthand conta agora -, e a catraca respondia `worse: true`, *"Fix those
|
|
74
|
+
* lines"*, saída 1, num repositório onde ninguém tocou uma linha. O tipo deste arquivo já
|
|
75
|
+
* prometia o contrário desde o primeiro dia (*"um número que sobe porque o leitor melhorou não é
|
|
76
|
+
* regressão"*), e `incomparable` existia para exatamente isto.
|
|
77
|
+
*
|
|
78
|
+
* A RÉGUA É A MARCA QUE JÁ EXISTE: `CHECKER_SINCE` é a última versão em que a leitura que o CI
|
|
79
|
+
* compara mudou de resposta, e ela sobe no mesmo PR que muda a leitura. Nada de limiar novo.
|
|
80
|
+
*/
|
|
81
|
+
const readerMoved = base.cli !== undefined && isOlderCli(base.cli, CHECKER_SINCE);
|
|
66
82
|
return {
|
|
67
83
|
worse: !scopeChanged &&
|
|
84
|
+
!readerMoved &&
|
|
68
85
|
(drift.now > drift.was ||
|
|
69
86
|
phantoms.now > phantoms.was ||
|
|
70
87
|
regressed.length > 0),
|
|
@@ -76,7 +93,11 @@ export function compareToBaseline(d, base, now) {
|
|
|
76
93
|
? {
|
|
77
94
|
incomparable: `the baseline was measured on ${base.scope ?? "the whole repo"} and this run read ${now.scope ?? "the whole repo"} - different inputs, so nothing is failed on the difference`,
|
|
78
95
|
}
|
|
79
|
-
:
|
|
96
|
+
: readerMoved
|
|
97
|
+
? {
|
|
98
|
+
incomparable: `the baseline was written by CLI ${base.cli}, and this run counts every length in a shorthand - that version counted only the first. The rise is in the reading, not in your code. Rewrite the line before comparing: npx synthesisui doctor --write-baseline`,
|
|
99
|
+
}
|
|
100
|
+
: {}),
|
|
80
101
|
};
|
|
81
102
|
}
|
|
82
103
|
/**
|
package/dist/doctor/ledger.js
CHANGED
|
@@ -215,8 +215,25 @@ export async function readEvents(root) {
|
|
|
215
215
|
*
|
|
216
216
|
* A 2 é a camada de token DELE entrando na conta (13/08): `var(--color-ocean-500)`, que ele declara
|
|
217
217
|
* no CSS global, deixou de ser invisível e passou a contar como valor governado.
|
|
218
|
+
*
|
|
219
|
+
* A 3 é cada comprimento de um shorthand entrando na conta (09/09): `padding: 8px 24px` contava um
|
|
220
|
+
* valor e agora conta dois. Sem subir a régua, a tendência anunciava uma QUEDA que ninguém fez -
|
|
221
|
+
* medido: `coverage 40% -> 0% since the first run` na cópia do `web-subscribe`, e `30% -> 28%` no
|
|
222
|
+
* `frontend-hub`. O denominador cresceu porque a leitura melhorou, e comparar as duas fotos é
|
|
223
|
+
* comparar réguas diferentes.
|
|
224
|
+
*/
|
|
225
|
+
export const COVERAGE_RULE = 3;
|
|
226
|
+
/**
|
|
227
|
+
* POR QUE A TENDÊNCIA RECOMEÇOU - uma frase por régua, e a saída lê ESTA, nunca uma fixa.
|
|
228
|
+
*
|
|
229
|
+
* A frase estava cravada no `doctor` desde 13/08 (*"your own tokens count now too"*), então na
|
|
230
|
+
* primeira vez que a régua subiu por outro motivo ela explicaria a causa ERRADA para quem lê. A
|
|
231
|
+
* razão viaja com a régua: quem sobe uma escreve a outra ao lado.
|
|
218
232
|
*/
|
|
219
|
-
export const
|
|
233
|
+
export const COVERAGE_RULE_REASON = {
|
|
234
|
+
2: "your own tokens count now too",
|
|
235
|
+
3: "every length in a shorthand counts now - `padding: 8px 24px` was one value and is two",
|
|
236
|
+
};
|
|
220
237
|
export function summarize(events) {
|
|
221
238
|
const hooks = events.filter((e) => e.kind === "hook" && e.file);
|
|
222
239
|
const byFile = new Map();
|
package/dist/doctor/scan.js
CHANGED
|
@@ -74,10 +74,43 @@ export const COLOR = /#[0-9a-fA-F]{8}\b|#[0-9a-fA-F]{6}\b|#[0-9a-fA-F]{3}\b|rgba
|
|
|
74
74
|
* JSX also camelCases, so `borderRadius` has to be as welcome as
|
|
75
75
|
* `border-radius`.
|
|
76
76
|
*/
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
*
|
|
80
|
-
|
|
77
|
+
/**
|
|
78
|
+
* CADA COMPRIMENTO DA DECLARAÇÃO, e não só o primeiro - a dívida que a `INV-VOC-09` declarou.
|
|
79
|
+
*
|
|
80
|
+
* O padrão exigia o número CONTÍGUO à propriedade, então `padding: 8px 24px` rendia `8px` e o
|
|
81
|
+
* `24px` não existia para a esteira: nem como achado, nem na fila do `absorb`. Medido em 09/09 nas
|
|
82
|
+
* duas populações:
|
|
83
|
+
*
|
|
84
|
+
* declarações de espaço/raio com comprimento 3694 (web-subscribe) 2970 (frontend-hub)
|
|
85
|
+
* com 2+ comprimentos (shorthand) 501 (14%) 482 (16%)
|
|
86
|
+
* ocorrências que a lista NÃO via 619 540
|
|
87
|
+
* na PRIMEIRA TELA (as 40 mais repetidas) 5 entram, 5 saem 4 entram, 4 saem
|
|
88
|
+
* maior subcontagem de um valor 16px: 287 -> 398 577 -> 773
|
|
89
|
+
*
|
|
90
|
+
* A promessa *"o arquivo tem os 40 mais repetidos"* era falsa, porque a contagem não era a real - e
|
|
91
|
+
* na MESMA linha impressa havia dois números discordando, o `because` do passe de função (que conta
|
|
92
|
+
* todos os comprimentos) e a coluna `<spacing, N files>` do scan (parcial).
|
|
93
|
+
*
|
|
94
|
+
* A IMUNIDADE À ÂNCORA É PRESERVADA, e é o que separa esta leitura da do passe de função: o valor é
|
|
95
|
+
* lido DENTRO da declaração (até `;`, `}`, `]`, aspa ou fim de linha), então um `width: 100px` na
|
|
96
|
+
* mesma linha nunca é lido como padding. Duas réguas, a mesma resposta.
|
|
97
|
+
*/
|
|
98
|
+
const DECL_VALUE = `\\s*:\\s*["'\`]?([^;}\\]"'\`\\n]*)`;
|
|
99
|
+
/**
|
|
100
|
+
* O comprimento dentro de um valor. O lookbehind impede `4px` dentro de `24px`; o guard de trás
|
|
101
|
+
* aceita o `_` que o Tailwind usa por espaço (`8px_16px`), como o literal de tempo já faz.
|
|
102
|
+
*/
|
|
103
|
+
const LENGTH_IN_VALUE = /(?<![\d.])(-?\d*\.?\d+)(px|rem|em)(?![a-zA-Z0-9%])/g;
|
|
104
|
+
/** `border-radius: 14px 8px`, `borderRadius: "14px"` - a declaração inteira. */
|
|
105
|
+
const RADIUS_DECL = new RegExp(`border-?[Rr]adius${DECL_VALUE}`, "g");
|
|
106
|
+
/**
|
|
107
|
+
* `rounded-[14px]`, `rounded-[4px_8px]` - o que está entre os colchetes é um VALOR, e o Tailwind
|
|
108
|
+
* escreve o espaço como `_`. A primeira versão desta rodada capturava um comprimento por par de
|
|
109
|
+
* colchetes, então `rounded-[4px_8px]` rendia só o `4px` - o mesmo defeito que o outro lado
|
|
110
|
+
* consertou, e o comentário de `LENGTH_IN_VALUE` já citava `8px_16px` como a razão de aceitar `_`.
|
|
111
|
+
* N=0 nas três populações medidas em 09/09: o defeito não estava na amostra, estava na promessa.
|
|
112
|
+
*/
|
|
113
|
+
const RADIUS_UTIL = /rounded(?:-[a-z]+)?-\[([^\]]*)/g;
|
|
81
114
|
/**
|
|
82
115
|
* Arbitrary spacing: `p-[18px]`, `gap-[7px]`, `margin: 18px`, `gap: "18px"`.
|
|
83
116
|
* The JSX side also writes `paddingLeft`, `marginTop` and friends.
|
|
@@ -106,7 +139,9 @@ const RADIUS = new RegExp(`(?:border-?[Rr]adius\\s*:\\s*${OPEN}|rounded(?:-[a-z]
|
|
|
106
139
|
* Duas gramáticas para a mesma propriedade é o tipo de coisa que só aparece quando alguém aponta
|
|
107
140
|
* para um arquivo e conta na mão.
|
|
108
141
|
*/
|
|
109
|
-
const
|
|
142
|
+
const SPACING_DECL = new RegExp(`(?:padding|margin|gap)(?:[A-Z][a-z]+|(?:-[a-z]+)+)?${DECL_VALUE}`, "g");
|
|
143
|
+
/** `p-[18px]`, `gap-[7px]`, `p-[8px_16px]` - o valor inteiro entre os colchetes. */
|
|
144
|
+
const SPACING_UTIL = /(?:\b[pmg](?:[trblxy])?-\[|gap-\[)([^\]]*)/g;
|
|
110
145
|
/** A font stack written by hand rather than taken from the type scale. */
|
|
111
146
|
const FONT = /font-family\s*:\s*([^;}\n]+)/g;
|
|
112
147
|
/**
|
|
@@ -438,6 +473,16 @@ function scanCore(file, source, table) {
|
|
|
438
473
|
let svgDepth = 0;
|
|
439
474
|
source.split("\n").forEach((raw, i) => {
|
|
440
475
|
const line = raw.trim();
|
|
476
|
+
/**
|
|
477
|
+
* O RECUO DA LINHA, para as COLUNAS do achado serem as do ARQUIVO.
|
|
478
|
+
*
|
|
479
|
+
* A varredura trabalha na linha aparada, e todos os guards de posição (fallback, markup,
|
|
480
|
+
* conteúdo) medem contra ela. `Finding.cols` sai para fora - é o `--fix` que a usa para escrever
|
|
481
|
+
* no arquivo DELE -, então ela precisa da coluna real: sem somar o recuo, um `border-radius: 4px`
|
|
482
|
+
* indentado com dois espaços apontava para dois caracteres antes do literal, e a troca não
|
|
483
|
+
* acontecia (o comando dizia que a linha havia mudado desde a medição).
|
|
484
|
+
*/
|
|
485
|
+
const indent = line ? raw.indexOf(line) : 0;
|
|
441
486
|
const at = i + 1;
|
|
442
487
|
tokenUses += countTokenUses(line, table);
|
|
443
488
|
/**
|
|
@@ -519,8 +564,20 @@ function scanCore(file, source, table) {
|
|
|
519
564
|
return;
|
|
520
565
|
}
|
|
521
566
|
const key = `${kind}:${literal}`;
|
|
522
|
-
|
|
567
|
+
/**
|
|
568
|
+
* A MESMA DECISÃO É UM ACHADO E VÁRIAS ESCRITAS: a segunda ocorrência não vira uma linha nova
|
|
569
|
+
* no relatório, mas a coluna dela entra no achado que já existe - senão o `--fix` deixaria
|
|
570
|
+
* metade da linha literal.
|
|
571
|
+
*/
|
|
572
|
+
const written = col >= 0 && line.startsWith(literal, col) ? col + indent : -1;
|
|
573
|
+
if (seen.has(key)) {
|
|
574
|
+
if (written >= 0) {
|
|
575
|
+
const first = findings.find((f) => f.kind === kind && f.literal === literal && f.line === at);
|
|
576
|
+
if (first && !first.cols?.includes(written))
|
|
577
|
+
first.cols = [...(first.cols ?? []), written];
|
|
578
|
+
}
|
|
523
579
|
return;
|
|
580
|
+
}
|
|
524
581
|
seen.add(key);
|
|
525
582
|
// The kind is already known here, and without passing it the lookup
|
|
526
583
|
// answers a `gap` with a radius token.
|
|
@@ -536,6 +593,7 @@ function scanCore(file, source, table) {
|
|
|
536
593
|
kind,
|
|
537
594
|
line: at,
|
|
538
595
|
literal,
|
|
596
|
+
...(written >= 0 ? { cols: [written] } : {}),
|
|
539
597
|
token: match?.token ?? null,
|
|
540
598
|
...(/(?<!r)em$/i.test(literal.trim())
|
|
541
599
|
? { fontRelative: true }
|
|
@@ -576,16 +634,42 @@ function scanCore(file, source, table) {
|
|
|
576
634
|
}
|
|
577
635
|
push("color", m[0], m.index ?? -1);
|
|
578
636
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
637
|
+
/**
|
|
638
|
+
* CADA COMPRIMENTO DA DECLARAÇÃO, com a COLUNA DELE - e a coluna importa: os três guards de
|
|
639
|
+
* posição (fallback de `var()`, markup como texto, conteúdo que a página mostra) decidem por
|
|
640
|
+
* ela. Passar a coluna da propriedade para todos faria o segundo valor de um shorthand ser
|
|
641
|
+
* julgado pelo lugar do primeiro.
|
|
642
|
+
*/
|
|
643
|
+
const pushLengths = (kind, m) => {
|
|
644
|
+
const whole = m[1] ?? "";
|
|
645
|
+
/**
|
|
646
|
+
* O VALOR PARA NA PRÓXIMA PROPRIEDADE ESCRITA - e sem isto a leitura vazava por duas formas
|
|
647
|
+
* que o QA reproduziu em 09/09 (N=0 nas três populações, 1335 arquivos Sass/Less varridos):
|
|
648
|
+
*
|
|
649
|
+
* @supports (padding: 8px) and (width: 100px) contava o `100px` como padding
|
|
650
|
+
* $r: (border-radius: 6px, font-size: 14px) contava o `14px` como raio
|
|
651
|
+
*
|
|
652
|
+
* Os terminadores de `DECL_VALUE` (`;`, `}`, `]`, aspa, fim de linha) não fecham nenhuma das
|
|
653
|
+
* duas: a declaração de verdade acabou onde a PRÓXIMA começou.
|
|
654
|
+
*/
|
|
655
|
+
const cut = /(?<![\w-])[a-zA-Z-]{2,}\s*:/.exec(whole);
|
|
656
|
+
const value = cut ? whole.slice(0, cut.index) : whole;
|
|
657
|
+
const base = (m.index ?? 0) + m[0].length - whole.length;
|
|
658
|
+
for (const l of value.matchAll(LENGTH_IN_VALUE)) {
|
|
659
|
+
const literal = `${l[1]}${l[2]}`;
|
|
660
|
+
/** `0`, `1px`, `9999px`: idiom em QUALQUER posição, não só na primeira. */
|
|
661
|
+
if (!IDIOM.has(literal))
|
|
662
|
+
push(kind, literal, base + (l.index ?? 0));
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
for (const m of line.matchAll(RADIUS_UTIL))
|
|
666
|
+
pushLengths("radius", m);
|
|
667
|
+
for (const m of line.matchAll(RADIUS_DECL))
|
|
668
|
+
pushLengths("radius", m);
|
|
669
|
+
for (const m of line.matchAll(SPACING_UTIL))
|
|
670
|
+
pushLengths("spacing", m);
|
|
671
|
+
for (const m of line.matchAll(SPACING_DECL))
|
|
672
|
+
pushLengths("spacing", m);
|
|
589
673
|
for (const m of line.matchAll(FONT)) {
|
|
590
674
|
const stack = m[1].trim();
|
|
591
675
|
// A stack already reading from the system is the point, not a problem.
|
package/dist/install-marks.js
CHANGED
|
@@ -250,7 +250,15 @@ export const MATERIALISER_SINCE = "0.16.403";
|
|
|
250
250
|
* `#6b6b78 is already --color-ink-500` numa frase da landing e manda tokenizar TEXTO. Um hook
|
|
251
251
|
* pinado antes desta versão aconselha OUTRA coisa, que é a régua desta marca.
|
|
252
252
|
*/
|
|
253
|
-
|
|
253
|
+
/**
|
|
254
|
+
* 0.16.378 -> 0.16.408 em 09/09: o hook passa a ver CADA comprimento de uma declaração. `scanSource`
|
|
255
|
+
* exigia o número contíguo à propriedade, então num `padding: 8px 24px` ele aconselhava sobre o
|
|
256
|
+
* `8px` e ficava calado sobre o `24px` - mesmo com um `--spacing-24` do próprio repositório dele
|
|
257
|
+
* nomeando aquele valor. Medido em 09/09 nas duas populações: 619 e 540 ocorrências que a leitura
|
|
258
|
+
* não via, 14% e 16% das declarações de espaço/raio. Um hook pinado antes desta versão aconselha
|
|
259
|
+
* MENOS, que é a régua desta marca.
|
|
260
|
+
*/
|
|
261
|
+
export const CHECKER_SINCE = "0.16.408";
|
|
254
262
|
/**
|
|
255
263
|
* A ÚLTIMA VERSÃO EM QUE OS LEITORES PASSARAM A PRODUZIR UM CENSO DIFERENTE.
|
|
256
264
|
*
|
|
@@ -691,7 +699,15 @@ export const CHECKER_SINCE = "0.16.378";
|
|
|
691
699
|
* muda um byte. Medido em 06/09: `frontend-hub/apps/web-dashboard` tem UM grupo acima (6 393), e
|
|
692
700
|
* `packages/ui` e `apps/web-review` nenhum.
|
|
693
701
|
*/
|
|
694
|
-
|
|
702
|
+
/**
|
|
703
|
+
* 0.16.383 -> 0.16.408 em 09/09: o censo passou a carregar cada comprimento de um shorthand. Um
|
|
704
|
+
* `padding: 8px 24px` gravava só o `8px`, então o censo de quem já importou afirma menos valores à
|
|
705
|
+
* mão do que o repositório dele tem - 619 ocorrências no `web-subscribe` e 540 no `frontend-hub`, e
|
|
706
|
+
* na primeira tela do `absorb` (as 40 mais repetidas) 5 e 4 valores ocupavam o lugar de outros que
|
|
707
|
+
* deveriam estar lá. Quem tem censo gravado precisa de um `sync` para a fila de trabalho dele ser a
|
|
708
|
+
* real; ver o livro-razão de `corpus.spec.ts`.
|
|
709
|
+
*/
|
|
710
|
+
export const READER_SINCE = "0.16.408";
|
|
695
711
|
/**
|
|
696
712
|
* O QUE ESTÁ INSTALADO AQUI FICOU PARA TRÁS - e as DUAS condições que fazem isso ser verdade.
|
|
697
713
|
*
|
package/dist/proposed-name.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { chroma, deltaE, lightness } from "./doctor/color-distance.js";
|
|
2
2
|
import { normalizeValue } from "./doctor/tokens.js";
|
|
3
|
-
import { SVG } from "./use-by-property.js";
|
|
3
|
+
import { lengthKey, ROOTS_OF_KIND, SVG, } from "./use-by-property.js";
|
|
4
4
|
function stepsOf(theirs) {
|
|
5
5
|
const out = [];
|
|
6
6
|
for (const [name, value] of theirs.byName) {
|
|
@@ -216,3 +216,163 @@ export function whyNotProposedFromUse(literal, uses, rootPx, counted) {
|
|
|
216
216
|
return "only in svg";
|
|
217
217
|
return `${u.total}× in ${root}, but the value does not read as a colour`;
|
|
218
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* A TERCEIRA FONTE - O PRÓPRIO VALOR, para espaço e raio. Especificada em 08/09
|
|
221
|
+
* (`steps/03-parte-3-espacamento-na-lista.md`), depois de a medição descartar a função como
|
|
222
|
+
* separadora de espaçamento: no `web-subscribe` real, `8px` é 409× padding, 303× margin e 18× gap -
|
|
223
|
+
* um degrau de espaço é usado em padding E margin por definição, e "fonte 2 para spacing" proporia
|
|
224
|
+
* na cauda e calaria na cabeça, exatamente onde ele olha.
|
|
225
|
+
*
|
|
226
|
+
* O QUE O CLIENTE GANHA: depois da cor, a primeira tela do `absorb` no `web-subscribe` tinha 24 de 40
|
|
227
|
+
* linhas de espaçamento em branco e SEM motivo. Agora `16px` chega como `spacing.16` com *"16px, used
|
|
228
|
+
* 642× as padding, margin and gap - one step of your spacing"*; ele renomeia na revisão (`spacing.md`)
|
|
229
|
+
* ou deixa hard-coded. E `1em` chega com o motivo: depende da fonte de cada elemento, não há degrau
|
|
230
|
+
* fixo.
|
|
231
|
+
*
|
|
232
|
+
* O CAMINHO É O PRÓPRIO VALOR (D2): é o único nome derivável do repositório dele que é VERDADEIRO. A
|
|
233
|
+
* palavra `spacing`/`radius` é a casa que o produto já usa nos caminhos dos nomes DELE (`HOME[kind]`
|
|
234
|
+
* em `absorb-plan.ts`) - não é uma palavra nossa sobre o valor, é onde o valor mora. `sm`, `md`,
|
|
235
|
+
* `tight` seriam invenção, e não entram nem como sugestão.
|
|
236
|
+
*
|
|
237
|
+
* AS PORTAS (D2-D4), todas para o lado do "propor de menos":
|
|
238
|
+
*
|
|
239
|
+
* natureza só `spacing` e `radius`; motion tem motivo e não tem proposta (fora, declarado)
|
|
240
|
+
* unidade FIXA px ou rem; `em` vale 16px só quando a fonte do elemento é a da raiz - é a regra
|
|
241
|
+
* do `fontRelative` que o `--fix` já segue (D3)
|
|
242
|
+
* degrau INTEIRO `3.2px` (de `0.2em` ou `0.2rem`) não é um degrau (D4)
|
|
243
|
+
* positivo zero não é degrau; negativo não é degrau (D4)
|
|
244
|
+
* contado o valor chegou ao passe em alguma função da natureza certa - sem contagem não
|
|
245
|
+
* há distribuição para dizer, e o motivo é o genérico do plano
|
|
246
|
+
*/
|
|
247
|
+
/** `16px` → 16 · `1rem` → 16 · `0.2rem` → 3.2 · não-comprimento → null. */
|
|
248
|
+
function pxOf(literal, rootPx) {
|
|
249
|
+
const m = /^(-?\d*\.?\d+)px$/.exec(normalizeValue(literal, rootPx));
|
|
250
|
+
return m ? Number.parseFloat(m[1] ?? "") : null;
|
|
251
|
+
}
|
|
252
|
+
const isEm = (literal) => /(?<!r)em$/i.test(literal.trim());
|
|
253
|
+
const isRem = (literal) => /rem$/i.test(literal.trim());
|
|
254
|
+
/**
|
|
255
|
+
* RAIZ SEM UMA RESPOSTA NÃO CONVERTE `rem` - e é o mesmo lado do erro que o `doctor` já escolheu.
|
|
256
|
+
*
|
|
257
|
+
* Duas declarações de `font-size` na raiz (ou uma que não dá para ler) e `1rem` deixa de ter um
|
|
258
|
+
* valor em px: propor `spacing.32` ali seria afirmar uma conversão que a plataforma não sabe fazer.
|
|
259
|
+
* `px` continua propondo, porque px não depende da raiz de nada. Decisão de implementação do lead
|
|
260
|
+
* (08/09), reabrível pelo dono; a régua que decide o que é ambíguo é `root-size.ts`, não esta.
|
|
261
|
+
*/
|
|
262
|
+
const unconvertible = (literal, root) => Boolean(root.ambiguous) && isRem(literal);
|
|
263
|
+
/** As funções da natureza certa em que o valor aparece, mais usada primeiro, com o total. */
|
|
264
|
+
function lengthUses(literal, kind, uses, root) {
|
|
265
|
+
const roots = ROOTS_OF_KIND[kind];
|
|
266
|
+
if (!roots)
|
|
267
|
+
return null;
|
|
268
|
+
const per = uses.get(lengthKey(literal, root));
|
|
269
|
+
if (!per)
|
|
270
|
+
return null;
|
|
271
|
+
const entries = [...per.entries()]
|
|
272
|
+
.filter(([root, n]) => n > 0 && roots.includes(root))
|
|
273
|
+
.sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1));
|
|
274
|
+
if (entries.length === 0)
|
|
275
|
+
return null;
|
|
276
|
+
return { per: entries, total: entries.reduce((n, [, c]) => n + c, 0) };
|
|
277
|
+
}
|
|
278
|
+
/** `padding, margin and gap` · `padding and margin` · `padding`. */
|
|
279
|
+
const listed = (roots) => roots.length <= 1
|
|
280
|
+
? (roots[0] ?? "")
|
|
281
|
+
: `${roots.slice(0, -1).join(", ")} and ${roots[roots.length - 1]}`;
|
|
282
|
+
/**
|
|
283
|
+
* O NOME PELO PRÓPRIO VALOR: `spacing.16`, `radius.6`.
|
|
284
|
+
*
|
|
285
|
+
* O `because` carrega a distribuição por função, contada por ocorrência no mesmo passe da cor, para
|
|
286
|
+
* ele julgar pela origem: *"16px, used 642× as padding, margin and gap - one step of your spacing"*.
|
|
287
|
+
*/
|
|
288
|
+
export function proposeFromTheirValue(literal, kind, uses,
|
|
289
|
+
/**
|
|
290
|
+
* A RAIZ MEDIDA, com a ambiguidade - o objeto inteiro, e não os pixels soltos: a ambiguidade é
|
|
291
|
+
* parte da medição, e perdê-la no caminho é o que fazia um `2rem` sair `spacing.32` num projeto
|
|
292
|
+
* cuja raiz não tem uma resposta. Ver `unconvertible`.
|
|
293
|
+
*/
|
|
294
|
+
root,
|
|
295
|
+
/**
|
|
296
|
+
* QUANTAS OCORRÊNCIAS O SCAN VIU - e a proposta cala quando o passe alcançou MENOS: a
|
|
297
|
+
* distribuição seria uma afirmação sobre um pedaço. É a mesma metade `unknown` que a cor já tem
|
|
298
|
+
* (`INV-VOC-08`), e é obrigatória por isso.
|
|
299
|
+
*/
|
|
300
|
+
counted) {
|
|
301
|
+
if (kind !== "spacing" && kind !== "radius")
|
|
302
|
+
return null;
|
|
303
|
+
if (isEm(literal) || unconvertible(literal, root))
|
|
304
|
+
return null;
|
|
305
|
+
const px = pxOf(literal, root.px);
|
|
306
|
+
if (px === null || !Number.isInteger(px) || px <= 0)
|
|
307
|
+
return null;
|
|
308
|
+
const u = lengthUses(literal, kind, uses, root);
|
|
309
|
+
/**
|
|
310
|
+
* DUAS OCORRÊNCIAS, A MESMA RÉGUA DA COR (D2 do dono, 07/09: 100% e >= 2, *"na dúvida,
|
|
311
|
+
* hard-coded"*). Uma cor vista uma vez sai "seen once" sem proposta; um comprimento visto uma vez
|
|
312
|
+
* saía `spacing.3 · "one step of your spacing"` - duas réguas de repetição na mesma tela, e a
|
|
313
|
+
* mais frouxa era a que propunha. A D2 é do dono; estendê-la a comprimento é decisão do lead
|
|
314
|
+
* (08/09), reabrível.
|
|
315
|
+
*/
|
|
316
|
+
if (!u || u.total < 2 || u.total < counted)
|
|
317
|
+
return null;
|
|
318
|
+
return {
|
|
319
|
+
path: `${kind}.${px}`,
|
|
320
|
+
because: `${px}px, used ${u.total}× as ${listed(u.per.map(([root]) => root))} - one step of your ${kind}`,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* POR QUE NÃO HÁ PROPOSTA PELO VALOR, em uma linha - e nunca `null` quando há contagem.
|
|
325
|
+
*
|
|
326
|
+
* *"font-relative (em): 597× across padding and margin - depends on each element's font, so no fixed
|
|
327
|
+
* step"* é a leitura que ele consegue agir sobre. `counted` é o que o scan viu, e serve a motion, que
|
|
328
|
+
* o passe não conta. `null` só quando o literal nem chegou ao passe - aí o plano usa o motivo
|
|
329
|
+
* genérico *"never inside a style property…"*. Nunca mente sozinha: devolve `null` onde
|
|
330
|
+
* `proposeFromTheirValue` propõe.
|
|
331
|
+
*/
|
|
332
|
+
export function whyNotProposedFromValue(literal, kind, uses, root, counted) {
|
|
333
|
+
if (kind === "motion")
|
|
334
|
+
return `${counted}× - durations are not proposed yet`;
|
|
335
|
+
if (proposeFromTheirValue(literal, kind, uses, root, counted))
|
|
336
|
+
return null;
|
|
337
|
+
if (kind !== "spacing" && kind !== "radius")
|
|
338
|
+
return null;
|
|
339
|
+
/**
|
|
340
|
+
* A RAIZ AMBÍGUA É O MOTIVO ANTES DE QUALQUER CONTAGEM - ela não depende de quantas vezes ele
|
|
341
|
+
* escreveu o valor, e dizer "read 0 of 4 uses" ali culparia a propriedade por uma lacuna que é da
|
|
342
|
+
* raiz. Ver `unconvertible`.
|
|
343
|
+
*/
|
|
344
|
+
if (unconvertible(literal, root)) {
|
|
345
|
+
const saw = root.ambiguous?.saw.length ?? 0;
|
|
346
|
+
return `your root font-size is not one answer (${saw} declaration${saw === 1 ? "" : "s"}) - px and rem are not compared here, so no fixed step from ${literal.trim().toLowerCase()}`;
|
|
347
|
+
}
|
|
348
|
+
const u = lengthUses(literal, kind, uses, root);
|
|
349
|
+
/**
|
|
350
|
+
* O PASSE NÃO ALCANÇOU O QUE O SCAN CONTOU: `unknown` é estado, e a frase diz qual é a lacuna.
|
|
351
|
+
*
|
|
352
|
+
* `scroll-padding: 18px`, `grid-gap: 22px` e `scroll-margin: 26px` chegam ao scan e não a este
|
|
353
|
+
* passe - a propriedade não está nas que ele lê. O motivo genérico dizia *"never inside a style
|
|
354
|
+
* property - declared or computed, not painted"*, falso em dois pontos: o valor ESTÁ numa
|
|
355
|
+
* propriedade de estilo, e "painted" é vocabulário de cor numa linha de espaçamento.
|
|
356
|
+
*/
|
|
357
|
+
if ((u?.total ?? 0) < counted)
|
|
358
|
+
return `read ${u?.total ?? 0} of ${counted} uses - the property it sits in is not one this reads yet`;
|
|
359
|
+
if (!u)
|
|
360
|
+
return null;
|
|
361
|
+
const across = listed(u.per.map(([root]) => root));
|
|
362
|
+
if (isEm(literal))
|
|
363
|
+
return `font-relative (em): ${u.total}× across ${across} - depends on each element's font, so no fixed step`;
|
|
364
|
+
const px = pxOf(literal, root.px);
|
|
365
|
+
if (px === null)
|
|
366
|
+
return `${u.total}× across ${across} - not a length we can read`;
|
|
367
|
+
if (px === 0)
|
|
368
|
+
return "zero is not a step";
|
|
369
|
+
if (px < 0)
|
|
370
|
+
return "negative values are not steps";
|
|
371
|
+
if (!Number.isInteger(px)) {
|
|
372
|
+
const written = literal.trim().toLowerCase();
|
|
373
|
+
const from = written === `${px}px` ? "" : ` (from ${written})`;
|
|
374
|
+
return `${px}px${from} is not a whole step`;
|
|
375
|
+
}
|
|
376
|
+
/** Uma ocorrência não é uso medido - a mesma frase que a cor já usa (D2). */
|
|
377
|
+
return "seen once";
|
|
378
|
+
}
|
package/dist/use-by-property.js
CHANGED
|
@@ -2,9 +2,22 @@ import { COLOR } from "./doctor/scan.js";
|
|
|
2
2
|
import { normalizeValue } from "./doctor/tokens.js";
|
|
3
3
|
/** A raiz que conta como função e NUNCA vira palavra: cor de ícone, não do sistema dele. */
|
|
4
4
|
export const SVG = "svg";
|
|
5
|
+
/** As raízes que recebem COMPRIMENTO - espaço e raio. Tudo o mais recebe cor. */
|
|
6
|
+
const LENGTH_ROOTS = new Set([
|
|
7
|
+
"padding",
|
|
8
|
+
"margin",
|
|
9
|
+
"gap",
|
|
10
|
+
"border-radius",
|
|
11
|
+
]);
|
|
12
|
+
/** As raízes de espaço, por natureza do achado - o `because` só lista as da natureza certa. */
|
|
13
|
+
export const ROOTS_OF_KIND = {
|
|
14
|
+
spacing: ["padding", "margin", "gap"],
|
|
15
|
+
radius: ["border-radius"],
|
|
16
|
+
};
|
|
5
17
|
/**
|
|
6
18
|
* UTILIDADE TAILWIND → PROPRIEDADE CSS QUE ELA ESCREVE. A palavra é a da direita, sempre: `ring` não
|
|
7
19
|
* é propriedade CSS nenhuma - o Tailwind a compila em `box-shadow`, e é isso que o código dele pinta.
|
|
20
|
+
* `px` é `padding` (dos dois lados), `mt` é `margin`, `rounded` é `border-radius`.
|
|
8
21
|
*/
|
|
9
22
|
export const TAILWIND_WRITES = {
|
|
10
23
|
bg: "background",
|
|
@@ -24,7 +37,35 @@ export const TAILWIND_WRITES = {
|
|
|
24
37
|
caret: "caret-color",
|
|
25
38
|
fill: SVG,
|
|
26
39
|
stroke: SVG,
|
|
40
|
+
p: "padding",
|
|
41
|
+
px: "padding",
|
|
42
|
+
py: "padding",
|
|
43
|
+
pt: "padding",
|
|
44
|
+
pr: "padding",
|
|
45
|
+
pb: "padding",
|
|
46
|
+
pl: "padding",
|
|
47
|
+
ps: "padding",
|
|
48
|
+
pe: "padding",
|
|
49
|
+
m: "margin",
|
|
50
|
+
mx: "margin",
|
|
51
|
+
my: "margin",
|
|
52
|
+
mt: "margin",
|
|
53
|
+
mr: "margin",
|
|
54
|
+
mb: "margin",
|
|
55
|
+
ml: "margin",
|
|
56
|
+
ms: "margin",
|
|
57
|
+
me: "margin",
|
|
58
|
+
gap: "gap",
|
|
59
|
+
rounded: "border-radius",
|
|
27
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* `16px` · `1.5rem` · `.5em` · `-1px` · `1PX` - o número e a unidade, como o scan os lê. O
|
|
63
|
+
* lookbehind impede `2px` dentro de `12px`; o lookahead impede `em` dentro de `emphasis`.
|
|
64
|
+
*
|
|
65
|
+
* A CAIXA NÃO DECIDE: CSS não distingue `1px` de `1PX`, e código gerado escreve das duas formas.
|
|
66
|
+
* Sem a flag `i` um `margin: 1REM` não era contado, e a linha dele saía sem motivo.
|
|
67
|
+
*/
|
|
68
|
+
const LENGTH = /(?<![\w.#-])-?\d*\.?\d+(?:px|rem|em)(?![\w-])/gi;
|
|
28
69
|
/**
|
|
29
70
|
* `background:` · `backgroundColor:` · `"background-color":` · `fill="`
|
|
30
71
|
*
|
|
@@ -32,21 +73,23 @@ export const TAILWIND_WRITES = {
|
|
|
32
73
|
* ternário não é uma propriedade), e a aspa de abertura, quando existe, tem de fechar antes do `:` -
|
|
33
74
|
* senão `"#fff" :` de um ternário viraria a âncora `fff`. O `=` só vale na forma de ATRIBUTO - colado
|
|
34
75
|
* ao nome e seguido de aspa ou `{` -, porque `const BRAND = "#0f5132"` é uma constante, não uma
|
|
35
|
-
* função. O que casa aqui ainda passa por `propertyRoot`: só o que
|
|
76
|
+
* função. O que casa aqui ainda passa por `propertyRoot`: só o que pinta ou espaça vira âncora.
|
|
36
77
|
*/
|
|
37
78
|
const PROPERTY = /(?<![\w.$@#-])(["']?)[a-zA-Z][\w-]*\1(?:\s*:|=(?=["'{]))/g;
|
|
38
79
|
/**
|
|
39
|
-
* `bg-[#…]` · `hover:text-[#…]` · `border-t-[rgba(…)]` · `
|
|
40
|
-
* com o sufixo de lado que algumas aceitam.
|
|
80
|
+
* `bg-[#…]` · `hover:text-[#…]` · `border-t-[rgba(…)]` · `gap-x-[…]` · `rounded-tl-[…]` - as
|
|
81
|
+
* utilidades da tabela, com o sufixo de lado que algumas aceitam.
|
|
41
82
|
*/
|
|
42
83
|
const UTILITY = new RegExp(`(?<![\\w-])(${Object.keys(TAILWIND_WRITES)
|
|
43
84
|
.sort((a, b) => b.length - a.length)
|
|
44
|
-
.join("|")})(?:-[trblxyse])?-\\[`, "g");
|
|
85
|
+
.join("|")})(?:-[trblxyse]{1,2})?-\\[`, "g");
|
|
45
86
|
/**
|
|
46
|
-
* A RAIZ DE UMA PROPRIEDADE CSS QUE CARREGA COR (D1) - ou `null` para o que não é
|
|
47
|
-
* que ele escreveu, sem o sufixo que só diz o lado ou o dialeto: `background-color`,
|
|
87
|
+
* A RAIZ DE UMA PROPRIEDADE CSS QUE CARREGA COR OU COMPRIMENTO (D1) - ou `null` para o que não é
|
|
88
|
+
* uma. A palavra que ele escreveu, sem o sufixo que só diz o lado ou o dialeto: `background-color`,
|
|
48
89
|
* `backgroundColor` e `background-image` são `background`; `border-top-color` e `borderColor` são
|
|
49
|
-
* `border
|
|
90
|
+
* `border`; `padding-top`, `paddingTop` são `padding`; `border-top-left-radius` e `borderRadius`
|
|
91
|
+
* são `border-radius`. Uma chave de objeto, uma variável, uma propriedade que não pinta nem espaça:
|
|
92
|
+
* `null`.
|
|
50
93
|
*/
|
|
51
94
|
export function propertyRoot(raw) {
|
|
52
95
|
const kebab = raw
|
|
@@ -54,6 +97,14 @@ export function propertyRoot(raw) {
|
|
|
54
97
|
.replace(/^["']|["']$/g, "")
|
|
55
98
|
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
56
99
|
.toLowerCase();
|
|
100
|
+
if (/^border(-[a-z]+)*-radius$/.test(kebab))
|
|
101
|
+
return "border-radius";
|
|
102
|
+
if (/^padding(-|$)/.test(kebab))
|
|
103
|
+
return "padding";
|
|
104
|
+
if (/^margin(-|$)/.test(kebab))
|
|
105
|
+
return "margin";
|
|
106
|
+
if (/^(gap|row-gap|column-gap)$/.test(kebab))
|
|
107
|
+
return "gap";
|
|
57
108
|
if (/^background(-|$)/.test(kebab))
|
|
58
109
|
return "background";
|
|
59
110
|
if (/^border(-|$)/.test(kebab))
|
|
@@ -72,28 +123,78 @@ export function propertyRoot(raw) {
|
|
|
72
123
|
return kebab;
|
|
73
124
|
return null;
|
|
74
125
|
}
|
|
75
|
-
/**
|
|
76
|
-
|
|
126
|
+
/**
|
|
127
|
+
* A CHAVE DE UM COMPRIMENTO: o valor em px sobre a raiz medida, e `@em` quando a unidade escrita
|
|
128
|
+
* é `em` - porque aí o valor não é fixo, e uma contagem que o somasse ao `16px` afirmaria um degrau
|
|
129
|
+
* sobre ocorrências que dependem da fonte de cada elemento (D3). `1rem` e `16px` são a mesma chave.
|
|
130
|
+
*/
|
|
131
|
+
export function lengthKey(literal, root) {
|
|
132
|
+
const px = normalizeValue(literal, root.px);
|
|
133
|
+
const written = literal.trim();
|
|
134
|
+
if (/(?<!r)em$/i.test(written))
|
|
135
|
+
return `${px}@em`;
|
|
136
|
+
if (root.ambiguous && /rem$/i.test(written))
|
|
137
|
+
return `${px}@rem?`;
|
|
138
|
+
return px;
|
|
139
|
+
}
|
|
140
|
+
/** Toda declaração escrita na linha, em ordem, com o limite de cada uma. */
|
|
141
|
+
function marksOf(line) {
|
|
77
142
|
const out = [];
|
|
78
|
-
for (const m of line.matchAll(PROPERTY))
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
143
|
+
for (const m of line.matchAll(PROPERTY))
|
|
144
|
+
out.push({
|
|
145
|
+
start: m.index,
|
|
146
|
+
end: m.index + m[0].length,
|
|
147
|
+
root: propertyRoot(m[0].replace(/\s*[:=]$/, "")),
|
|
148
|
+
until: Number.POSITIVE_INFINITY,
|
|
149
|
+
});
|
|
83
150
|
for (const m of line.matchAll(UTILITY)) {
|
|
84
151
|
const root = TAILWIND_WRITES[m[1] ?? ""];
|
|
85
|
-
if (root)
|
|
86
|
-
|
|
152
|
+
if (!root)
|
|
153
|
+
continue;
|
|
154
|
+
const end = m.index + m[0].length;
|
|
155
|
+
const close = line.indexOf("]", end);
|
|
156
|
+
out.push({
|
|
157
|
+
start: m.index,
|
|
158
|
+
end,
|
|
159
|
+
root,
|
|
160
|
+
until: close < 0 ? Number.POSITIVE_INFINITY : close,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
out.sort((a, b) => a.start - b.start);
|
|
164
|
+
/** O limite de cada uma é o começo da próxima - quem escreveu outra propriedade fechou esta. */
|
|
165
|
+
for (const [i, mark] of out.entries()) {
|
|
166
|
+
const next = out[i + 1];
|
|
167
|
+
if (next)
|
|
168
|
+
mark.until = Math.min(mark.until, next.start);
|
|
87
169
|
}
|
|
88
|
-
return out
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* A DECLARAÇÃO QUE COBRE ESTE LITERAL - ou `null`, e `null` é resposta: o valor não está numa
|
|
174
|
+
* propriedade que este passe lê, então ele não exerce função nenhuma que a gente possa afirmar.
|
|
175
|
+
* Sem declaração na linha, vale a que ficou aberta na anterior.
|
|
176
|
+
*/
|
|
177
|
+
function anchorFor(marks, at, carried) {
|
|
178
|
+
if (marks.length === 0)
|
|
179
|
+
return carried;
|
|
180
|
+
let hit = null;
|
|
181
|
+
for (const mark of marks) {
|
|
182
|
+
if (mark.end > at)
|
|
183
|
+
break;
|
|
184
|
+
hit = mark;
|
|
185
|
+
}
|
|
186
|
+
if (hit === null || hit.root === null)
|
|
187
|
+
return null;
|
|
188
|
+
return at < hit.until ? hit.root : null;
|
|
89
189
|
}
|
|
90
190
|
/** A linha FECHOU a declaração: o que vier depois não é continuação dela. */
|
|
91
191
|
const ENDS_DECLARATION = /[;{}]\s*$/;
|
|
92
192
|
/**
|
|
93
|
-
* Conta cada ocorrência de cor de UM arquivo, linha a linha, na função da âncora
|
|
94
|
-
* antes dela. `box-shadow: 0 1px #000, 0 2px #fff` conta as duas em
|
|
95
|
-
* `
|
|
96
|
-
*
|
|
193
|
+
* Conta cada ocorrência de cor e de comprimento de UM arquivo, linha a linha, na função da âncora
|
|
194
|
+
* mais próxima antes dela. `box-shadow: 0 1px #000, 0 2px #fff` conta as duas cores em
|
|
195
|
+
* `box-shadow` (e nenhum dos comprimentos); `padding: 1px 2px` conta `1px` e `2px` uma vez cada em
|
|
196
|
+
* `padding`; uma linha com `background` e `borderColor` do mesmo hex conta as duas funções. Acumula
|
|
197
|
+
* em `into` para que o repositório inteiro caiba num único mapa.
|
|
97
198
|
*
|
|
98
199
|
* A DECLARAÇÃO PODE CONTINUAR NA LINHA SEGUINTE - regra do CSS, não de um repositório:
|
|
99
200
|
*
|
|
@@ -106,29 +207,33 @@ const ENDS_DECLARATION = /[;{}]\s*$/;
|
|
|
106
207
|
* não tem âncora e a anterior não FECHOU a declaração (não termina em `;`, `}` ou `{`), a âncora da
|
|
107
208
|
* anterior vale para esta. `background: #111;` seguido de `#222` NÃO herda: o `;` fechou.
|
|
108
209
|
*/
|
|
109
|
-
export function countUseByProperty(source, into,
|
|
210
|
+
export function countUseByProperty(source, into,
|
|
211
|
+
/** A raiz MEDIDA, com a ambiguidade dela - ver `lengthKey`. */
|
|
212
|
+
root) {
|
|
213
|
+
const bump = (key, root) => {
|
|
214
|
+
const per = into.get(key) ?? new Map();
|
|
215
|
+
per.set(root, (per.get(root) ?? 0) + 1);
|
|
216
|
+
into.set(key, per);
|
|
217
|
+
};
|
|
110
218
|
/** A âncora da declaração ainda aberta, vinda das linhas anteriores. */
|
|
111
219
|
let carried = null;
|
|
112
220
|
for (const line of source.split("\n")) {
|
|
113
|
-
const
|
|
221
|
+
const marks = marksOf(line);
|
|
114
222
|
for (const m of line.matchAll(COLOR)) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
continue;
|
|
124
|
-
const key = normalizeValue(m[0], rootPx);
|
|
125
|
-
const per = into.get(key) ?? new Map();
|
|
126
|
-
per.set(root, (per.get(root) ?? 0) + 1);
|
|
127
|
-
into.set(key, per);
|
|
223
|
+
const at = anchorFor(marks, m.index, carried);
|
|
224
|
+
if (at !== null && !LENGTH_ROOTS.has(at))
|
|
225
|
+
bump(normalizeValue(m[0], root.px), at);
|
|
226
|
+
}
|
|
227
|
+
for (const m of line.matchAll(LENGTH)) {
|
|
228
|
+
const at = anchorFor(marks, m.index, carried);
|
|
229
|
+
if (at !== null && LENGTH_ROOTS.has(at))
|
|
230
|
+
bump(lengthKey(m[0], root), at);
|
|
128
231
|
}
|
|
232
|
+
/** A ÚLTIMA declaração da linha é a que pode continuar - e se ela é de uma propriedade que este
|
|
233
|
+
* passe não lê, o que continua é o silêncio dela, nunca a âncora de antes. */
|
|
129
234
|
if (ENDS_DECLARATION.test(line))
|
|
130
235
|
carried = null;
|
|
131
|
-
else if (
|
|
132
|
-
carried =
|
|
236
|
+
else if (marks.length > 0)
|
|
237
|
+
carried = marks[marks.length - 1]?.root ?? null;
|
|
133
238
|
}
|
|
134
239
|
}
|
package/package.json
CHANGED