@teacss/engine 0.9.4 → 0.10.2
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/LICENSE +22 -0
- package/README.md +28 -109
- package/binding-header.d.ts +8 -0
- package/binding.d.ts +26 -0
- package/binding.js +56 -55
- package/contract.d.ts +114 -16
- package/dist/entry.d.ts +20 -0
- package/dist/entry.js +279 -0
- package/dist/errors.d.ts +1 -7
- package/dist/errors.js +1 -13
- package/dist/fonts.d.ts +21 -0
- package/dist/fonts.js +32 -0
- package/dist/index.d.ts +3 -4
- package/dist/index.js +4 -7
- package/dist/project.d.ts +80 -0
- package/dist/project.js +136 -0
- package/package.json +19 -49
- package/dist/config-view.d.ts +0 -36
- package/dist/config-view.js +0 -135
- package/dist/constants.d.ts +0 -22
- package/dist/constants.js +0 -23
- package/dist/countable-set.d.ts +0 -57
- package/dist/countable-set.js +0 -205
- package/dist/generator.d.ts +0 -163
- package/dist/generator.js +0 -893
- package/dist/result.d.ts +0 -83
- package/dist/result.js +0 -142
- package/dist/select.d.ts +0 -183
- package/dist/select.js +0 -741
- package/dist/transformer.d.ts +0 -44
- package/dist/transformer.js +0 -59
- package/dist/ts-fallback.d.ts +0 -41
- package/dist/ts-fallback.js +0 -79
package/contract.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Mirror of
|
|
1
|
+
// Mirror of notes/specs/v02.2-engine-contract.md §3 (spec-revision 24). Do not edit without bumping the spec revision.
|
|
2
2
|
export interface InspectEntryRequest {
|
|
3
3
|
/** 项目根(绝对路径)。相对 @source 以声明它的文件所在目录为基准。 */
|
|
4
4
|
root: string;
|
|
@@ -32,10 +32,9 @@ export interface LoadProjectRequest {
|
|
|
32
32
|
inline?: StaticConfig | undefined | null;
|
|
33
33
|
envMode: "dev" | "build";
|
|
34
34
|
unmatched: "warn" | "error" | "off";
|
|
35
|
-
/** 对应 UserConfig.outputToCssLayers
|
|
36
|
-
outputToCssLayers?: boolean |
|
|
35
|
+
/** 对应 UserConfig.outputToCssLayers;cssLayerName 只有静态表的形状(v10.2 §2.1)。 */
|
|
36
|
+
outputToCssLayers?: boolean | OutputToCssLayersOptions | undefined | null;
|
|
37
37
|
shortcutsLayer?: string | undefined | null;
|
|
38
|
-
virtualModulePrefix?: string | undefined | null;
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
export interface LoadProjectResult {
|
|
@@ -44,10 +43,47 @@ export interface LoadProjectResult {
|
|
|
44
43
|
changed: boolean;
|
|
45
44
|
/** 声明文件中解析失败的位置(不阻止加载,oracle 行为相同)。 */
|
|
46
45
|
warnings: EntryDiagnostic[];
|
|
47
|
-
/** @custom 中不属于任何命名空间的属性名(oracle `ResolveEntryResult.unknown
|
|
46
|
+
/** @custom 中不属于任何命名空间的属性名(oracle `ResolveEntryResult.unknown`),调用方原样透传。 */
|
|
48
47
|
unknownCustom: string[];
|
|
49
48
|
}
|
|
50
49
|
|
|
50
|
+
/** 修订 18:未知键报 E_INVALID_ARGUMENT(details.path 为 "request")。 */
|
|
51
|
+
export interface LoadProjectFromRootRequest {
|
|
52
|
+
/** 项目根(绝对路径)。 */
|
|
53
|
+
root: string;
|
|
54
|
+
/** 入口文件(绝对路径)或 null:null 时按 v10.1 §4.1 发现;找不到入口时 entry 为 null、presets 为空。 */
|
|
55
|
+
entry?: string | undefined | null;
|
|
56
|
+
/** 入口内容(未保存的编辑器缓冲等),只在 entry 给定时有意义。 */
|
|
57
|
+
entryCss?: string | undefined | null;
|
|
58
|
+
/** 与 LoadProjectRequest 相同;presets 不能传,由入口的 @presets 决定。 */
|
|
59
|
+
inline?: StaticConfig | undefined | null;
|
|
60
|
+
envMode: "dev" | "build";
|
|
61
|
+
unmatched: "warn" | "error" | "off";
|
|
62
|
+
outputToCssLayers?: boolean | OutputToCssLayersOptions | undefined | null;
|
|
63
|
+
shortcutsLayer?: string | undefined | null;
|
|
64
|
+
/** 覆盖内置预设的选项(没有入口的项目也能开 Standard):键是预设名。 */
|
|
65
|
+
presetOptions?: { standard?: PresetStandardOptions; icons?: PresetIconsOptions } | undefined | null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface LoadProjectFromRootResult extends LoadProjectResult {
|
|
69
|
+
/** 实际用的入口;null 表示没有入口。 */
|
|
70
|
+
entryPath: string | null;
|
|
71
|
+
/** @presets 展开后的 PresetSpec 的名字,顺序即加载顺序。 */
|
|
72
|
+
presets: string[];
|
|
73
|
+
/** @source 枚举得到并归一化后的声明文件(architecture §5)。 */
|
|
74
|
+
declarationFiles: string[];
|
|
75
|
+
/** 入口、import、声明文件之外,@source 的 glob 匹配到的全部文件(绝对路径,排序),供壳子的 watch 与 extractFilesUnion 用。 */
|
|
76
|
+
sourceFiles: string[];
|
|
77
|
+
/** @source 的 glob 起点下列不出内容的目录(绝对路径,排序):里面的文件不在 sourceFiles 里,壳子像读不了的文件一样警告。 */
|
|
78
|
+
unreadableDirectories: string[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface OutputToCssLayersOptions {
|
|
82
|
+
allLayers?: boolean | undefined | null;
|
|
83
|
+
/** 内部层名 → 输出的 @layer 名;null 表示该层不写 @layer;没列出的层用内部名。 */
|
|
84
|
+
cssLayerName?: Record<string, string | null> | undefined | null;
|
|
85
|
+
}
|
|
86
|
+
|
|
51
87
|
export type PresetSpec = { kind: "standard"; options?: PresetStandardOptions | undefined | null } | { kind: "official" } | { kind: "icons"; options?: PresetIconsOptions | undefined | null } | { kind: "static"; preset: StaticPreset };
|
|
52
88
|
|
|
53
89
|
export interface PresetStandardOptions {
|
|
@@ -72,11 +108,14 @@ export interface PresetIconsOptions {
|
|
|
72
108
|
warn?: boolean | undefined | null;
|
|
73
109
|
layer?: string | undefined | null;
|
|
74
110
|
unit?: string | undefined | null;
|
|
75
|
-
/**
|
|
76
|
-
collections?: string[] | undefined | null;
|
|
111
|
+
/** 内置集合名列表(每项视为 true),或对象:内置集合名为 true,自定义集合的值是 Iconify JSON 对象或 .json 文件的绝对路径。 */
|
|
112
|
+
collections?: string[] | Record<string, true | string | IconifyJson> | undefined | null;
|
|
77
113
|
}
|
|
78
114
|
|
|
79
|
-
/**
|
|
115
|
+
/** 一个 Iconify JSON 集合(`IconifyJSON`);字段以 Iconify JSON 规范为准,合同不展开。 */
|
|
116
|
+
export type IconifyJson = Record<string, unknown>;
|
|
117
|
+
|
|
118
|
+
/** 本地/第三方 preset 的可静态表示部分;不能静态表示的字段没有形状,不进入这里。v10.2 起壳子不传 preset 对象,只有 `loadProject` 的直接调用方用到它。 */
|
|
80
119
|
export interface StaticPreset {
|
|
81
120
|
name: string;
|
|
82
121
|
enforce?: "pre" | "post" | undefined | null;
|
|
@@ -95,7 +134,16 @@ export interface StaticConfig {
|
|
|
95
134
|
blocklist?: string[] | undefined | null;
|
|
96
135
|
layers?: Record<string, number> | undefined | null;
|
|
97
136
|
theme?: Record<string, string> | undefined | null;
|
|
98
|
-
|
|
137
|
+
/** 变体与 token 的分隔符;省略为 [":", "-"]。每项非空、互不为前缀。 */
|
|
138
|
+
separators?: string[] | undefined | null;
|
|
139
|
+
content?:
|
|
140
|
+
| {
|
|
141
|
+
filesystem?: string[] | undefined | null;
|
|
142
|
+
/** 内联内容:字符串即代码,id 省略时为 "inline:<序号>"。 */
|
|
143
|
+
inline?: Array<string | { code: string; id?: string | undefined | null }> | undefined | null;
|
|
144
|
+
}
|
|
145
|
+
| undefined
|
|
146
|
+
| null;
|
|
99
147
|
}
|
|
100
148
|
|
|
101
149
|
export interface ModuleUpdateResult {
|
|
@@ -120,15 +168,15 @@ export interface ExtractFilesUnionResult {
|
|
|
120
168
|
|
|
121
169
|
export interface TransformResult {
|
|
122
170
|
code: string;
|
|
123
|
-
/** 从原文到 code 的替换序列,UTF-16 code unit 偏移,按 (start, end)
|
|
171
|
+
/** 从原文到 code 的替换序列,UTF-16 code unit 偏移,按 (start, end) 升序且互不重叠。自己持有字符串的宿主按顺序回放(`replayEdits`,或 MagicString 的 `start === end ? s.appendLeft(start, text) : s.update(start, end, text)`;magic-string 1.x 的 `update` 与 `overwrite` 除 `storeName` 默认值外相同),使宿主生成的 source map 与 0.8.2 完全一致(v02.8 §7)。 */
|
|
124
172
|
edits: Array<{ start: number; end: number; text: string }>;
|
|
125
173
|
/** 独立使用者(无 MagicString)用的 Source Map v3:sources = [id],sourcesContent = [原文]。 */
|
|
126
174
|
map: SourceMap;
|
|
127
175
|
/** oracle SourceCodeTransformer 返回的高亮区间(compileClass 提供)。 */
|
|
128
176
|
highlightAnnotations: HighlightAnnotation[];
|
|
129
|
-
/** transformSource:本次登记的 compile-class
|
|
177
|
+
/** transformSource:本次登记的 compile-class 类名(已进该模块的 token 集,v02.9 §6);transformStylesheet 为空数组。 */
|
|
130
178
|
compiledClasses: string[];
|
|
131
|
-
/** transformSource
|
|
179
|
+
/** transformSource:该模块的定义集相对上一次是否变化(壳子据此失效生成的样式表);transformStylesheet 为 false。 */
|
|
132
180
|
definitionsChanged: boolean;
|
|
133
181
|
}
|
|
134
182
|
|
|
@@ -160,7 +208,7 @@ export interface GenerateOptions {
|
|
|
160
208
|
/** 默认 false;为 true 时填充 matchedInfo */
|
|
161
209
|
extendedInfo?: boolean | undefined | null;
|
|
162
210
|
scope?: string | undefined | null;
|
|
163
|
-
/** 默认 true;false 时不拼、不传整表 css,result.css 为 ""
|
|
211
|
+
/** 默认 true;false 时不拼、不传整表 css,result.css 为 ""。自己用 layers 拼样式表的调用方传 false。 */
|
|
164
212
|
css?: boolean | undefined | null;
|
|
165
213
|
}
|
|
166
214
|
|
|
@@ -174,7 +222,7 @@ export interface GenerateResult {
|
|
|
174
222
|
/** extendedInfo 时填充。input 为 {kind:"code"} 时 count 是该 token 在抽取结果中的出现次数(oracle CountableSet 语义);tokens/all 输入时为 -1(oracle 对不可计数输入的语义)。 */
|
|
175
223
|
matchedInfo: Array<{ token: string; data: StringifiedUtil[]; count: number }> | null;
|
|
176
224
|
programKey: string;
|
|
177
|
-
/** 有 shortcut 留下警告时才有这个键,按 token 的访问顺序(输入,再 safelist),每个 token
|
|
225
|
+
/** 有 shortcut 留下警告时才有这个键,按 token 的访问顺序(输入,再 safelist),每个 token 至多一项。调用方据此打印 oracle `stringifyShortcuts` 文本的警告(§6.8)。 */
|
|
178
226
|
shortcutWarnings?: ShortcutWarning[];
|
|
179
227
|
}
|
|
180
228
|
|
|
@@ -185,7 +233,7 @@ export interface GenerateResult {
|
|
|
185
233
|
*/
|
|
186
234
|
export type ShortcutWarning = { shortcut: string; unmatched: string[] } | { shortcut: string; leafLimit: number };
|
|
187
235
|
|
|
188
|
-
/** oracle `StringifiedUtil` 元组的对象形式;`context`
|
|
236
|
+
/** oracle `StringifiedUtil` 元组的对象形式;`context` 字段不跨边界,需要时由调用方以 undefined 填充。 */
|
|
189
237
|
export interface StringifiedUtil {
|
|
190
238
|
index: number;
|
|
191
239
|
selector: string | null;
|
|
@@ -251,7 +299,6 @@ export interface ResolvedConfigView {
|
|
|
251
299
|
envMode: "dev" | "build";
|
|
252
300
|
unmatched: "warn" | "error" | "off";
|
|
253
301
|
shortcutsLayer: string;
|
|
254
|
-
virtualModulePrefix: string;
|
|
255
302
|
mergeSelectors: boolean;
|
|
256
303
|
warn: boolean;
|
|
257
304
|
details: boolean;
|
|
@@ -273,6 +320,8 @@ export interface RuleInfo {
|
|
|
273
320
|
/** `MatcherKey` 的变体:Property、PropertyPrefix、Keyword;shortcut 按名作 keyword。 */
|
|
274
321
|
matcher: { kind: "property" | "prefix" | "keyword"; value: string };
|
|
275
322
|
source: "standard" | "official" | "icons" | "shortcut";
|
|
323
|
+
/** `RuleDescriptor` 的补全模板(teacss-ts `parseAutocomplete` 语法,v10.3 §2.1);一条 teacss-ts 规则拆成多个描述符时只在第一个上,shortcut 与没有模板的规则为空数组。 */
|
|
324
|
+
autocomplete: string[];
|
|
276
325
|
}
|
|
277
326
|
|
|
278
327
|
export interface VariantInfo {
|
|
@@ -281,6 +330,8 @@ export interface VariantInfo {
|
|
|
281
330
|
/** 接受它的解析器:pseudo、relational、attribute、language、media、at-rule、container、breakpoint、arbitrary。 */
|
|
282
331
|
kind: string;
|
|
283
332
|
source: RuleInfo["source"];
|
|
333
|
+
/** 变体的补全模板;teacss-ts 0.9.4 没有声明任何变体模板,今天全为空数组。 */
|
|
334
|
+
autocomplete: string[];
|
|
284
335
|
}
|
|
285
336
|
|
|
286
337
|
export interface EngineStats {
|
|
@@ -295,3 +346,50 @@ export interface EngineStats {
|
|
|
295
346
|
/** 上一次 generate 结果:css、各层 css、matched 的字节和;没有时为 0。 */
|
|
296
347
|
memoBytes: number;
|
|
297
348
|
}
|
|
349
|
+
|
|
350
|
+
/** 修订 21:`Engine.suggest` 的选项;未知键报 E_INVALID_ARGUMENT(details.path 为 "options")。 */
|
|
351
|
+
export interface SuggestOptions {
|
|
352
|
+
/** 默认 "prefix":候选以输入开头(无匹配时两边去掉 `-` 再比);"fuzzy" 用 nucleo-matcher 对整个输入打分。 */
|
|
353
|
+
matchType?: "prefix" | "fuzzy" | undefined | null;
|
|
354
|
+
/** 默认 false;true 只留引擎能编译的候选(`alc:$`、`alc:[]` 这类占位候选不出现)。 */
|
|
355
|
+
strict?: boolean | undefined | null;
|
|
356
|
+
/** 截断条数,默认 1000(LSP 设置 `autocomplete.maxItems` 的默认值);0 返回空;负数或非整数报 E_INVALID_ARGUMENT。 */
|
|
357
|
+
limit?: number | undefined | null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** 修订 21:一条补全候选(v10.3 §2.2)。 */
|
|
361
|
+
export interface Suggestion {
|
|
362
|
+
/** 要插入的完整文本,输入里剥掉的变体(`@hover`、结尾 `!`)已接回。 */
|
|
363
|
+
text: string;
|
|
364
|
+
/** utility:规则模板展开、静态规则 token 或输入本身;shortcut:配置的 shortcut 名;variant:补全最后一个 `@` 段的条件名;themeValue:`$theme.path` 模板部分贡献的展开。 */
|
|
365
|
+
kind: "utility" | "shortcut" | "variant" | "themeValue";
|
|
366
|
+
/** themeValue 给主题值,variant 给解析器种类,其余 null。 */
|
|
367
|
+
detail: string | null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** 修订 21:sRGB 颜色,各通道 0–1。 */
|
|
371
|
+
export interface Rgba {
|
|
372
|
+
r: number;
|
|
373
|
+
g: number;
|
|
374
|
+
b: number;
|
|
375
|
+
a: number;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** 修订 21:一个匹配的 token 在编辑器里展示的信息(v10.3 §2.2)。 */
|
|
379
|
+
export interface TokenInfo {
|
|
380
|
+
/** `generate({kind:"tokens"})` 只对这一个 token 的输出:不出 preflight、不包 `@layer`、不加 scope,每层去掉 `/* layer: … *\/` 注释后以 `\n` 连接。 */
|
|
381
|
+
css: string;
|
|
382
|
+
/** 第一个 util 的层(规则没写时为 "utilities")。 */
|
|
383
|
+
layer: string;
|
|
384
|
+
/** 第一个 util 的 parent at-rule 链(如 "@media (min-width: 1024px)");没有为 null。 */
|
|
385
|
+
parent: string | null;
|
|
386
|
+
/** 声明里解析出的第一个颜色(经 util 的自定义属性与主题取值),没有为 null。 */
|
|
387
|
+
color: Rgba | null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** 修订 21:源码里一个被抽取 token 的 UTF-16 区间与它是否匹配当前 program(v10.3 §2.2)。 */
|
|
391
|
+
export interface MatchedPosition {
|
|
392
|
+
start: number;
|
|
393
|
+
end: number;
|
|
394
|
+
matched: boolean;
|
|
395
|
+
}
|
package/dist/entry.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { SourceMap } from "../contract.js";
|
|
2
|
+
/** `css` with its first top-level `@teacss;` replaced by `generate()` and the entry directives removed; null when it has no `@teacss;`. */
|
|
3
|
+
export declare function replaceEntry(css: string, generate: () => string): string | null;
|
|
4
|
+
export interface EntrySource {
|
|
5
|
+
/** The id the map names as its source. */
|
|
6
|
+
id: string;
|
|
7
|
+
/** The stylesheet as read, before the transform that produced the `css` handed to `replaceEntryWithMap`. */
|
|
8
|
+
original: string;
|
|
9
|
+
/** The transform's map from `original` to that `css`; null when the transform left it unchanged. */
|
|
10
|
+
map: SourceMap | null;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* `replaceEntry` with a source map back to `source.original`: hand-written CSS keeps its original lines and
|
|
14
|
+
* columns, and every line of the generated CSS maps to the `@teacss;` it replaced.
|
|
15
|
+
*/
|
|
16
|
+
export declare function replaceEntryWithMap(css: string, generate: () => string, source: EntrySource): {
|
|
17
|
+
code: string;
|
|
18
|
+
map: SourceMap;
|
|
19
|
+
} | null;
|
|
20
|
+
//# sourceMappingURL=entry.d.ts.map
|
package/dist/entry.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// Statement-form entry directives the engine consumed from the entry file; the Rust side
|
|
2
|
+
// (`teacss-transform/src/entry_strip.rs`) leaves an entry that carries `@teacss;` to its shell.
|
|
3
|
+
const ENTRY_STATEMENTS = new Set(["presets", "source", "shortcut", "safelist", "primary", "neutral", "success", "warning", "failure", "general"]);
|
|
4
|
+
function skipComment(css, index) {
|
|
5
|
+
const close = css.indexOf("*/", index + 2);
|
|
6
|
+
return close < 0 ? css.length : close + 2;
|
|
7
|
+
}
|
|
8
|
+
function skipString(css, index) {
|
|
9
|
+
const quote = css[index];
|
|
10
|
+
for (let cursor = index + 1; cursor < css.length;) {
|
|
11
|
+
if (css[cursor] === "\\")
|
|
12
|
+
cursor += 2;
|
|
13
|
+
else if (css[cursor] === quote)
|
|
14
|
+
return cursor + 1;
|
|
15
|
+
else
|
|
16
|
+
cursor++;
|
|
17
|
+
}
|
|
18
|
+
return css.length;
|
|
19
|
+
}
|
|
20
|
+
/** The index after the comment, string or backslash escape that starts at `index`, or `index` when none does. */
|
|
21
|
+
function skipOpaque(css, index) {
|
|
22
|
+
if (css.startsWith("/*", index))
|
|
23
|
+
return skipComment(css, index);
|
|
24
|
+
const char = css[index];
|
|
25
|
+
if (char === '"' || char === "'")
|
|
26
|
+
return skipString(css, index);
|
|
27
|
+
if (char === "\\")
|
|
28
|
+
return Math.min(index + 2, css.length);
|
|
29
|
+
return index;
|
|
30
|
+
}
|
|
31
|
+
function findClosingBrace(css, open) {
|
|
32
|
+
let depth = 1;
|
|
33
|
+
for (let cursor = open + 1; cursor < css.length;) {
|
|
34
|
+
const skipped = skipOpaque(css, cursor);
|
|
35
|
+
if (skipped !== cursor) {
|
|
36
|
+
cursor = skipped;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (css[cursor] === "{")
|
|
40
|
+
depth++;
|
|
41
|
+
else if (css[cursor] === "}" && --depth === 0)
|
|
42
|
+
return cursor;
|
|
43
|
+
cursor++;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
/** The prelude text with each comment read as one space and strings kept as written, trimmed. */
|
|
48
|
+
function cleanPrelude(css, start, end) {
|
|
49
|
+
let out = "";
|
|
50
|
+
let segment = start;
|
|
51
|
+
for (let cursor = start; cursor < end;) {
|
|
52
|
+
if (css.startsWith("/*", cursor) && cursor + 1 < end) {
|
|
53
|
+
out += `${css.slice(segment, cursor)} `;
|
|
54
|
+
cursor = Math.min(skipComment(css, cursor), end);
|
|
55
|
+
segment = cursor;
|
|
56
|
+
}
|
|
57
|
+
else if (css[cursor] === '"' || css[cursor] === "'") {
|
|
58
|
+
cursor = Math.min(skipString(css, cursor), end);
|
|
59
|
+
}
|
|
60
|
+
else
|
|
61
|
+
cursor++;
|
|
62
|
+
}
|
|
63
|
+
return (out + css.slice(segment, end)).trim();
|
|
64
|
+
}
|
|
65
|
+
function atRuleAt(css, start, name) {
|
|
66
|
+
const nameEnd = start + 1 + name.length;
|
|
67
|
+
const closers = [];
|
|
68
|
+
for (let cursor = nameEnd; cursor < css.length;) {
|
|
69
|
+
const skipped = skipOpaque(css, cursor);
|
|
70
|
+
if (skipped !== cursor) {
|
|
71
|
+
cursor = skipped;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const char = css[cursor];
|
|
75
|
+
if (closers.length === 0 && char === "{") {
|
|
76
|
+
const close = findClosingBrace(css, cursor);
|
|
77
|
+
if (close !== null)
|
|
78
|
+
return { name, prelude: cleanPrelude(css, nameEnd, cursor), start, end: close + 1, block: true };
|
|
79
|
+
}
|
|
80
|
+
if (char === "(")
|
|
81
|
+
closers.push(")");
|
|
82
|
+
else if (char === "[")
|
|
83
|
+
closers.push("]");
|
|
84
|
+
else if (char === "{")
|
|
85
|
+
closers.push("}");
|
|
86
|
+
else if (closers.at(-1) === char)
|
|
87
|
+
closers.pop();
|
|
88
|
+
if (closers.length === 0 && char === ";")
|
|
89
|
+
return { name, prelude: cleanPrelude(css, nameEnd, cursor), start, end: cursor + 1, block: false };
|
|
90
|
+
cursor++;
|
|
91
|
+
}
|
|
92
|
+
return { name, prelude: cleanPrelude(css, nameEnd, css.length), start, end: css.length, block: false };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Top-level at-rules exactly as `teacss-config`'s `scan_top_level_at_rules` finds them: comments, strings and
|
|
96
|
+
* backslash escapes skipped, nested rules ignored, comments in a prelude read as a space.
|
|
97
|
+
*/
|
|
98
|
+
function topLevelAtRules(css) {
|
|
99
|
+
const rules = [];
|
|
100
|
+
let depth = 0;
|
|
101
|
+
for (let index = 0; index < css.length;) {
|
|
102
|
+
const skipped = skipOpaque(css, index);
|
|
103
|
+
if (skipped !== index) {
|
|
104
|
+
index = skipped;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const char = css[index];
|
|
108
|
+
if (char === "{")
|
|
109
|
+
depth++;
|
|
110
|
+
else if (char === "}")
|
|
111
|
+
depth = Math.max(0, depth - 1);
|
|
112
|
+
else if (char === "@" && depth === 0) {
|
|
113
|
+
const name = /^[\w-]+/.exec(css.slice(index + 1))?.[0];
|
|
114
|
+
if (name) {
|
|
115
|
+
const rule = atRuleAt(css, index, name);
|
|
116
|
+
rules.push(rule);
|
|
117
|
+
index = rule.end;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
index++;
|
|
122
|
+
}
|
|
123
|
+
return rules;
|
|
124
|
+
}
|
|
125
|
+
function entryPieces(css, generate) {
|
|
126
|
+
const rules = topLevelAtRules(css);
|
|
127
|
+
const entry = rules.find((rule) => rule.name === "teacss" && !rule.block && rule.prelude === "");
|
|
128
|
+
if (!entry)
|
|
129
|
+
return null;
|
|
130
|
+
const pieces = [];
|
|
131
|
+
let cursor = 0;
|
|
132
|
+
for (const rule of rules) {
|
|
133
|
+
const remove = rule.name === "teacss" || rule.name === "custom" || (ENTRY_STATEMENTS.has(rule.name) && !rule.block);
|
|
134
|
+
if (!remove)
|
|
135
|
+
continue;
|
|
136
|
+
pieces.push({ from: cursor, to: rule.start });
|
|
137
|
+
if (rule === entry)
|
|
138
|
+
pieces.push({ generated: generate(), at: rule.start });
|
|
139
|
+
cursor = rule.end;
|
|
140
|
+
}
|
|
141
|
+
pieces.push({ from: cursor, to: css.length });
|
|
142
|
+
return pieces;
|
|
143
|
+
}
|
|
144
|
+
/** `css` with its first top-level `@teacss;` replaced by `generate()` and the entry directives removed; null when it has no `@teacss;`. */
|
|
145
|
+
export function replaceEntry(css, generate) {
|
|
146
|
+
const pieces = entryPieces(css, generate);
|
|
147
|
+
return pieces && pieces.map((piece) => ("generated" in piece ? piece.generated : css.slice(piece.from, piece.to))).join("");
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* `replaceEntry` with a source map back to `source.original`: hand-written CSS keeps its original lines and
|
|
151
|
+
* columns, and every line of the generated CSS maps to the `@teacss;` it replaced.
|
|
152
|
+
*/
|
|
153
|
+
export function replaceEntryWithMap(css, generate, source) {
|
|
154
|
+
const pieces = entryPieces(css, generate);
|
|
155
|
+
if (!pieces)
|
|
156
|
+
return null;
|
|
157
|
+
const points = source.map ? decodePoints(css, source.map.mappings) : identityPoints(css);
|
|
158
|
+
const out = [];
|
|
159
|
+
let code = "";
|
|
160
|
+
for (const piece of pieces) {
|
|
161
|
+
if ("generated" in piece) {
|
|
162
|
+
const original = originalAt(css, points, piece.at);
|
|
163
|
+
for (const start of lineStarts(piece.generated))
|
|
164
|
+
out.push({ offset: code.length + start, original });
|
|
165
|
+
code += piece.generated;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (piece.from === piece.to)
|
|
169
|
+
continue;
|
|
170
|
+
out.push({ offset: code.length, original: originalAt(css, points, piece.from) });
|
|
171
|
+
for (const point of points) {
|
|
172
|
+
if (point.offset > piece.from && point.offset < piece.to)
|
|
173
|
+
out.push({ offset: point.offset - piece.from + code.length, original: point.original });
|
|
174
|
+
}
|
|
175
|
+
code += css.slice(piece.from, piece.to);
|
|
176
|
+
}
|
|
177
|
+
const map = { version: 3, file: source.map?.file ?? null, sources: [source.id], sourcesContent: [source.original], names: [], mappings: encodePoints(code, out) };
|
|
178
|
+
return { code, map };
|
|
179
|
+
}
|
|
180
|
+
function lineStarts(text) {
|
|
181
|
+
const starts = [0];
|
|
182
|
+
for (let index = text.indexOf("\n"); index >= 0; index = text.indexOf("\n", index + 1))
|
|
183
|
+
starts.push(index + 1);
|
|
184
|
+
return starts;
|
|
185
|
+
}
|
|
186
|
+
function identityPoints(css) {
|
|
187
|
+
return lineStarts(css).map((offset, line) => ({ offset, original: [line, 0] }));
|
|
188
|
+
}
|
|
189
|
+
/** Where `offset` came from: the last point at or before it on the same line, moved by the columns between them. */
|
|
190
|
+
function originalAt(css, points, offset) {
|
|
191
|
+
let last;
|
|
192
|
+
for (const point of points) {
|
|
193
|
+
if (point.offset > offset)
|
|
194
|
+
break;
|
|
195
|
+
last = point;
|
|
196
|
+
}
|
|
197
|
+
if (!last?.original || css.slice(last.offset, offset).includes("\n"))
|
|
198
|
+
return null;
|
|
199
|
+
return [last.original[0], last.original[1] + offset - last.offset];
|
|
200
|
+
}
|
|
201
|
+
const BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
202
|
+
function decodeVlq(segment) {
|
|
203
|
+
const values = [];
|
|
204
|
+
let value = 0;
|
|
205
|
+
let shift = 0;
|
|
206
|
+
for (const char of segment) {
|
|
207
|
+
const digit = BASE64.indexOf(char);
|
|
208
|
+
value += (digit & 31) << shift;
|
|
209
|
+
if (digit & 32)
|
|
210
|
+
shift += 5;
|
|
211
|
+
else {
|
|
212
|
+
values.push(value & 1 ? -(value >>> 1) : value >>> 1);
|
|
213
|
+
value = 0;
|
|
214
|
+
shift = 0;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return values;
|
|
218
|
+
}
|
|
219
|
+
function encodeVlq(value) {
|
|
220
|
+
let rest = value < 0 ? (-value << 1) | 1 : value << 1;
|
|
221
|
+
let out = "";
|
|
222
|
+
do {
|
|
223
|
+
const digit = rest & 31;
|
|
224
|
+
rest >>>= 5;
|
|
225
|
+
out += BASE64[rest > 0 ? digit | 32 : digit];
|
|
226
|
+
} while (rest > 0);
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
/** The points of a single-source map's `mappings` over `generated`, in order. */
|
|
230
|
+
function decodePoints(generated, mappings) {
|
|
231
|
+
const starts = lineStarts(generated);
|
|
232
|
+
const points = [];
|
|
233
|
+
let line = 0;
|
|
234
|
+
let column = 0;
|
|
235
|
+
for (const [index, group] of mappings.split(";").entries()) {
|
|
236
|
+
let generatedColumn = 0;
|
|
237
|
+
for (const segment of group.split(",")) {
|
|
238
|
+
if (segment === "")
|
|
239
|
+
continue;
|
|
240
|
+
const [deltaColumn = 0, , deltaLine, deltaOriginalColumn] = decodeVlq(segment);
|
|
241
|
+
generatedColumn += deltaColumn;
|
|
242
|
+
let original = null;
|
|
243
|
+
if (deltaLine !== undefined && deltaOriginalColumn !== undefined) {
|
|
244
|
+
line += deltaLine;
|
|
245
|
+
column += deltaOriginalColumn;
|
|
246
|
+
original = [line, column];
|
|
247
|
+
}
|
|
248
|
+
points.push({ offset: (starts[index] ?? generated.length) + generatedColumn, original });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return points;
|
|
252
|
+
}
|
|
253
|
+
function encodePoints(code, points) {
|
|
254
|
+
const starts = lineStarts(code);
|
|
255
|
+
const lines = starts.map(() => []);
|
|
256
|
+
const kept = points.filter((point, index) => points[index + 1]?.offset !== point.offset);
|
|
257
|
+
let line = 0;
|
|
258
|
+
let previousColumn = 0;
|
|
259
|
+
let previousLine = 0;
|
|
260
|
+
let previousOriginalColumn = 0;
|
|
261
|
+
for (const point of kept) {
|
|
262
|
+
while (line + 1 < starts.length && starts[line + 1] <= point.offset) {
|
|
263
|
+
line++;
|
|
264
|
+
previousColumn = 0;
|
|
265
|
+
}
|
|
266
|
+
const column = point.offset - starts[line];
|
|
267
|
+
let segment = encodeVlq(column - previousColumn);
|
|
268
|
+
previousColumn = column;
|
|
269
|
+
if (point.original) {
|
|
270
|
+
const [originalLine, originalColumn] = point.original;
|
|
271
|
+
segment += encodeVlq(0) + encodeVlq(originalLine - previousLine) + encodeVlq(originalColumn - previousOriginalColumn);
|
|
272
|
+
previousLine = originalLine;
|
|
273
|
+
previousOriginalColumn = originalColumn;
|
|
274
|
+
}
|
|
275
|
+
lines[line]?.push(segment);
|
|
276
|
+
}
|
|
277
|
+
return lines.map((segments) => segments.join(",")).join(";");
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=entry.js.map
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
/** Errors the
|
|
1
|
+
/** Errors the TypeScript side raises itself; an `Error` from napi (code `E_*`) passes through unwrapped. */
|
|
2
2
|
export declare class TeacssEngineError extends Error {
|
|
3
3
|
code: string;
|
|
4
4
|
details: Record<string, unknown>;
|
|
5
5
|
constructor(code: string, message: string, details?: Record<string, unknown>);
|
|
6
6
|
}
|
|
7
|
-
/** v02.9 §2.1: the member needs `@teacss/core`, which is not installed. */
|
|
8
|
-
export declare function tsRequiredError(member: string): TeacssEngineError;
|
|
9
|
-
/** The generator was disposed; the native Engine reports the same code after its own `dispose()`. */
|
|
10
|
-
export declare function disposedError(member: string): TeacssEngineError;
|
|
11
|
-
/** v02.9 §3.3: the member needs the TypeScript engine, which has not been materialized. */
|
|
12
|
-
export declare function tsNotReadyError(member: string): TeacssEngineError;
|
|
13
7
|
//# sourceMappingURL=errors.d.ts.map
|
package/dist/errors.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Errors the
|
|
1
|
+
/** Errors the TypeScript side raises itself; an `Error` from napi (code `E_*`) passes through unwrapped. */
|
|
2
2
|
export class TeacssEngineError extends Error {
|
|
3
3
|
constructor(code, message, details = {}) {
|
|
4
4
|
super(message);
|
|
@@ -7,16 +7,4 @@ export class TeacssEngineError extends Error {
|
|
|
7
7
|
this.details = details;
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
|
-
/** v02.9 §2.1: the member needs `@teacss/core`, which is not installed. */
|
|
11
|
-
export function tsRequiredError(member) {
|
|
12
|
-
return new TeacssEngineError("E_TS_REQUIRED", `[@teacss/engine] ${member} requires @teacss/core (optional peer) to be installed`, { member });
|
|
13
|
-
}
|
|
14
|
-
/** The generator was disposed; the native Engine reports the same code after its own `dispose()`. */
|
|
15
|
-
export function disposedError(member) {
|
|
16
|
-
return new TeacssEngineError("E_DISPOSED", `[@teacss/engine] ${member} called after dispose()`);
|
|
17
|
-
}
|
|
18
|
-
/** v02.9 §3.3: the member needs the TypeScript engine, which has not been materialized. */
|
|
19
|
-
export function tsNotReadyError(member) {
|
|
20
|
-
return new TeacssEngineError("E_TS_REQUIRED", `[@teacss/engine] ${member} needs the TypeScript engine; await generator[Symbol.for('@teacss/engine/materialize')]() first or set engine: 'ts'`, { member });
|
|
21
|
-
}
|
|
22
10
|
//# sourceMappingURL=errors.js.map
|
package/dist/fonts.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Standard preset's preflight inlines its fonts as data URLs branded with the sha256 of the bytes
|
|
3
|
+
* (`crates/teacss-standard/src/preflight_fonts.rs`); only TeaCSS writes this URL. Groups: quote, digest, base64.
|
|
4
|
+
*/
|
|
5
|
+
export declare const BUNDLED_FONT_URL: RegExp;
|
|
6
|
+
export interface BundledFont {
|
|
7
|
+
digest: string;
|
|
8
|
+
bytes: Uint8Array;
|
|
9
|
+
/** What `pathFor` returned for the digest: the URL every `url()` of this font now carries. */
|
|
10
|
+
path: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Moves every bundled font out of `css`: each distinct digest is decoded once, checked against its bytes
|
|
14
|
+
* (a mismatch is `E_INVALID_ARGUMENT`) and handed to `pathFor`, whose result replaces the data URL.
|
|
15
|
+
* CSS without a bundled font comes back as the same string with no fonts.
|
|
16
|
+
*/
|
|
17
|
+
export declare function extractBundledFonts(css: string, pathFor: (digest: string, bytes: Uint8Array) => string): {
|
|
18
|
+
css: string;
|
|
19
|
+
fonts: BundledFont[];
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=fonts.d.ts.map
|
package/dist/fonts.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { TeacssEngineError } from "./errors.js";
|
|
3
|
+
/**
|
|
4
|
+
* The Standard preset's preflight inlines its fonts as data URLs branded with the sha256 of the bytes
|
|
5
|
+
* (`crates/teacss-standard/src/preflight_fonts.rs`); only TeaCSS writes this URL. Groups: quote, digest, base64.
|
|
6
|
+
*/
|
|
7
|
+
export const BUNDLED_FONT_URL = /url\(\s*(["']?)data:font\/woff2;teacss=v1-([a-f0-9]{64});base64,([A-Za-z0-9+/]*={0,2})\1\s*\)/g;
|
|
8
|
+
/**
|
|
9
|
+
* Moves every bundled font out of `css`: each distinct digest is decoded once, checked against its bytes
|
|
10
|
+
* (a mismatch is `E_INVALID_ARGUMENT`) and handed to `pathFor`, whose result replaces the data URL.
|
|
11
|
+
* CSS without a bundled font comes back as the same string with no fonts.
|
|
12
|
+
*/
|
|
13
|
+
export function extractBundledFonts(css, pathFor) {
|
|
14
|
+
if (!css.includes(";teacss=v1-"))
|
|
15
|
+
return { css, fonts: [] };
|
|
16
|
+
const fonts = new Map();
|
|
17
|
+
const out = css.replace(BUNDLED_FONT_URL, (_, _quote, digest, base64) => {
|
|
18
|
+
let font = fonts.get(digest);
|
|
19
|
+
if (font === undefined) {
|
|
20
|
+
const bytes = Buffer.from(base64, "base64");
|
|
21
|
+
const actual = createHash("sha256").update(bytes).digest("hex");
|
|
22
|
+
if (actual !== digest) {
|
|
23
|
+
throw new TeacssEngineError("E_INVALID_ARGUMENT", `[@teacss/engine] bundled font ${digest} does not match its bytes (sha256 ${actual})`, { path: `teacss=v1-${digest}`, expected: digest, actual });
|
|
24
|
+
}
|
|
25
|
+
font = { digest, bytes, path: pathFor(digest, bytes) };
|
|
26
|
+
fonts.set(digest, font);
|
|
27
|
+
}
|
|
28
|
+
return `url(${JSON.stringify(font.path)})`;
|
|
29
|
+
});
|
|
30
|
+
return { css: out, fonts: [...fonts.values()] };
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=fonts.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
export * from "../binding.js";
|
|
2
2
|
export type * from "../contract.js";
|
|
3
|
-
export {
|
|
3
|
+
export { type EntrySource, replaceEntry, replaceEntryWithMap } from "./entry.js";
|
|
4
4
|
export { TeacssEngineError } from "./errors.js";
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
7
|
-
export { createCompileClassTransformer } from "./transformer.js";
|
|
5
|
+
export { BUNDLED_FONT_URL, type BundledFont, extractBundledFonts } from "./fonts.js";
|
|
6
|
+
export { CSS_PLACEHOLDER, createProject, type Edit, isCssId, type Project, replayEdits, type ShellOptions, type SourceDirectory, type UnmatchedReport } from "./project.js";
|
|
8
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
//
|
|
2
|
-
// `../binding.js`, never from `index.js`, which re-exports them (a cycle otherwise).
|
|
3
|
-
// This file imports no peer (`@teacss/core`, `@teacss/config`).
|
|
1
|
+
// The package has no runtime dependency beyond the binding.
|
|
4
2
|
export * from "../binding.js";
|
|
5
|
-
export {
|
|
3
|
+
export { replaceEntry, replaceEntryWithMap } from "./entry.js";
|
|
6
4
|
export { TeacssEngineError } from "./errors.js";
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
9
|
-
export { createCompileClassTransformer } from "./transformer.js";
|
|
5
|
+
export { BUNDLED_FONT_URL, extractBundledFonts } from "./fonts.js";
|
|
6
|
+
export { CSS_PLACEHOLDER, createProject, isCssId, replayEdits } from "./project.js";
|
|
10
7
|
//# sourceMappingURL=index.js.map
|