@joao.dadas/lume 0.0.0-stage → 0.4.0

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/src/texto.mjs ADDED
@@ -0,0 +1,244 @@
1
+ // Leitura de código sem comentários, portada de scripts/check.ts: strings e
2
+ // literais de regex são copiados inteiros, então um "//" dentro de "https://"
3
+ // não vira comentário, e as quebras de linha ficam no lugar para o número da
4
+ // linha continuar certo.
5
+
6
+ function copiaString(src, inicio, q, out) {
7
+ const n = src.length;
8
+ let i = inicio;
9
+ out.push(q);
10
+ i++;
11
+ while (i < n) {
12
+ const ch = src[i] ?? "";
13
+ if (ch === "\\") {
14
+ out.push(ch, src[i + 1] ?? "");
15
+ i += 2;
16
+ continue;
17
+ }
18
+ if (ch === q) {
19
+ out.push(ch);
20
+ return i + 1;
21
+ }
22
+ if (q === "`" && ch === "$" && src[i + 1] === "{") {
23
+ out.push("${");
24
+ i += 2;
25
+ let prof = 1;
26
+ while (i < n && prof > 0) {
27
+ const e = src[i] ?? "";
28
+ if (e === '"' || e === "'" || e === "`") {
29
+ i = copiaString(src, i, e, out);
30
+ continue;
31
+ }
32
+ if (e === "{") prof++;
33
+ else if (e === "}") prof--;
34
+ out.push(e);
35
+ i++;
36
+ }
37
+ continue;
38
+ }
39
+ if (q !== "`" && ch === "\n") {
40
+ out.push(ch);
41
+ return i + 1;
42
+ }
43
+ out.push(ch);
44
+ i++;
45
+ }
46
+ return i;
47
+ }
48
+
49
+ /** O código com os comentários trocados por espaço. */
50
+ export function semComentarios(src) {
51
+ const out = [];
52
+ const n = src.length;
53
+ let i = 0;
54
+ let ultimo = "";
55
+ while (i < n) {
56
+ const c = src[i] ?? "";
57
+ const d = src[i + 1];
58
+ if (c === "/" && d === "/") {
59
+ while (i < n && src[i] !== "\n") {
60
+ out.push(" ");
61
+ i++;
62
+ }
63
+ continue;
64
+ }
65
+ if (c === "/" && d === "*") {
66
+ out.push(" ");
67
+ i += 2;
68
+ while (i < n && !(src[i] === "*" && src[i + 1] === "/")) {
69
+ out.push(src[i] === "\n" ? "\n" : " ");
70
+ i++;
71
+ }
72
+ out.push(" ");
73
+ i += 2;
74
+ continue;
75
+ }
76
+ if (c === '"' || c === "'" || c === "`") {
77
+ i = copiaString(src, i, c, out);
78
+ ultimo = c;
79
+ continue;
80
+ }
81
+ if (
82
+ c === "/" &&
83
+ (ultimo === "" || /[(,=:[!&|?{};+\-*%<>~^]/.test(ultimo))
84
+ ) {
85
+ out.push(c);
86
+ i++;
87
+ let classe = false;
88
+ while (i < n && src[i] !== "\n") {
89
+ const ch = src[i] ?? "";
90
+ if (ch === "\\") {
91
+ out.push(ch, src[i + 1] ?? "");
92
+ i += 2;
93
+ continue;
94
+ }
95
+ if (ch === "[") classe = true;
96
+ else if (ch === "]") classe = false;
97
+ out.push(ch);
98
+ i++;
99
+ if (ch === "/" && !classe) break;
100
+ }
101
+ ultimo = "/";
102
+ continue;
103
+ }
104
+ out.push(c);
105
+ if (!/\s/.test(c)) ultimo = c;
106
+ i++;
107
+ }
108
+ return out.join("");
109
+ }
110
+
111
+ /** CSS sem comentários de bloco, com as quebras de linha preservadas. */
112
+ export function semComentariosCss(src) {
113
+ return src.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "));
114
+ }
115
+
116
+ /** JSON com comentários e vírgula sobrando (tsconfig, components.json). */
117
+ export function lerJsonc(texto) {
118
+ const limpo = semComentarios(texto).replace(/,(\s*[}\]])/g, "$1");
119
+ return JSON.parse(limpo);
120
+ }
121
+
122
+ const IMPORT_RE =
123
+ /((?:^|\n)\s*(?:import|export)\s+(?:type\s+)?(?:[\s\S]*?\s+from\s+)?)(["'])([^"']+)\2|(import\(\s*)(["'])([^"']+)\5(\s*\))/g;
124
+
125
+ /**
126
+ * O import aponta para o próprio projeto? Local é o que começa por `.`, `/`,
127
+ * `@/`, `~/`, `#` ou por um dos `prefixos` (aliases do components.json e
128
+ * chaves de paths do tsconfig). O resto, inclusive escopo `@x/y`, é pacote.
129
+ */
130
+ export function importLocal(spec, prefixos = []) {
131
+ if (/^(\.|\/|@\/|~\/|#)/.test(spec)) return true;
132
+ return prefixos.some((p) => p && spec.startsWith(p));
133
+ }
134
+
135
+ /** Uma string entre aspas simples vira a mesma string entre aspas duplas. */
136
+ function aspasDuplas(corpo, q) {
137
+ let saida = "";
138
+ for (let i = 0; i < corpo.length; i++) {
139
+ const ch = corpo[i];
140
+ if (ch === "\\") {
141
+ const prox = corpo[i + 1] ?? "";
142
+ saida += prox === "'" ? "'" : `\\${prox}`;
143
+ i++;
144
+ continue;
145
+ }
146
+ saida += ch === '"' && q === "'" ? '\\"' : ch;
147
+ }
148
+ return `"${saida}"`;
149
+ }
150
+
151
+ /**
152
+ * Tokens que o formatador troca sem mudar o código: aspas simples viram
153
+ * duplas, sai o `;` do fim da instrução e a vírgula antes de `)`, `]` e `}`,
154
+ * e some o espaço em volta de pontuação. Template e regex ficam como estão.
155
+ */
156
+ function semFormatacao(src) {
157
+ const out = [];
158
+ const n = src.length;
159
+ let i = 0;
160
+ let ultimo = "";
161
+ while (i < n) {
162
+ const c = src[i] ?? "";
163
+ if (c === '"' || c === "'") {
164
+ let j = i + 1;
165
+ while (j < n && src[j] !== c && src[j] !== "\n") {
166
+ j += src[j] === "\\" ? 2 : 1;
167
+ }
168
+ out.push(aspasDuplas(src.slice(i + 1, j), c));
169
+ i = j + 1;
170
+ ultimo = c;
171
+ continue;
172
+ }
173
+ if (c === "`") {
174
+ const parte = [];
175
+ i = copiaString(src, i, c, parte);
176
+ out.push(parte.join(""));
177
+ ultimo = c;
178
+ continue;
179
+ }
180
+ if (
181
+ c === "/" &&
182
+ (ultimo === "" || /[(,=:[!&|?{};+\-*%<>~^]/.test(ultimo))
183
+ ) {
184
+ let j = i + 1;
185
+ let classe = false;
186
+ while (j < n && src[j] !== "\n") {
187
+ const ch = src[j];
188
+ if (ch === "\\") {
189
+ j += 2;
190
+ continue;
191
+ }
192
+ if (ch === "[") classe = true;
193
+ else if (ch === "]") classe = false;
194
+ j++;
195
+ if (ch === "/" && !classe) break;
196
+ }
197
+ out.push(src.slice(i, j));
198
+ i = j;
199
+ ultimo = "/";
200
+ continue;
201
+ }
202
+ out.push(c);
203
+ if (!/\s/.test(c)) ultimo = c;
204
+ i++;
205
+ }
206
+ return out
207
+ .join("")
208
+ .replace(/;[ \t]*(?=\n|$)/g, "")
209
+ .replace(/;(\s*[}])/g, "$1")
210
+ .replace(/,(\s*[)\]}])/g, "$1")
211
+ .replace(/\s+/g, " ")
212
+ .replace(/\s*([(){}[\],;])\s*/g, "$1")
213
+ .trim();
214
+ }
215
+
216
+ /**
217
+ * Forma comparável de um arquivo da registry: sem comentários, sem a diretiva
218
+ * "use client" (a CLI do shadcn tira quando rsc é false), com cada import
219
+ * local reduzido ao nome do arquivo (a CLI reescreve o caminho pelos aliases
220
+ * do projeto) e sem o que o formatador muda (aspas, `;`, vírgula final e
221
+ * espaços). `prefixos` são os aliases do projeto que contam como import local.
222
+ */
223
+ export function normalizar(src, prefixos = []) {
224
+ const reduzido = semComentarios(src)
225
+ .replace(/^\s*["']use client["'];?\s*$/gm, "")
226
+ .replace(IMPORT_RE, (todo, a1, q1, s1, a2, q2, s2, f2) => {
227
+ const spec = s1 ?? s2;
228
+ if (!importLocal(spec, prefixos)) return todo;
229
+ const nome = spec.split("/").pop();
230
+ return s1 !== undefined
231
+ ? `${a1}${q1}@/${nome}${q1}`
232
+ : `${a2}${q2}@/${nome}${q2}${f2}`;
233
+ });
234
+ return semFormatacao(reduzido);
235
+ }
236
+
237
+ /** Número da linha (1-based) de um índice no texto. */
238
+ export function linhaDe(texto, indice) {
239
+ let n = 1;
240
+ for (let i = 0; i < indice && i < texto.length; i++) {
241
+ if (texto[i] === "\n") n++;
242
+ }
243
+ return n;
244
+ }