# 架构与转换公式

[English](./architecture.md) · **简体中文**

数字是怎么算出来的，以及编译器刻意不做的事。选项速查见[配置参考](./configuration.zh-CN.md)。

## 编译流程

1. 插件初始化时把 `libraries` 展开成路由，追加在 `routes` 之后。
2. 根据文件路径执行 `include` / `exclude`。
3. 为每条规则解析归属画布，优先级见[配置参考](./configuration.zh-CN.md#adaptiveroute)：`@adaptive` > 属性名路由 > 选择器路由 > 文件路由 > `defaultProfile`。
4. 使用 PostCSS AST 遍历声明，属性和值经过过滤器。
5. 使用 `postcss-value-parser` 解析值，跳过字符串和 URL 函数。
6. 把目标长度转换成边界可选的流体表达式。
7. 规则内声明处理完毕后，若启用 `fixedContainingBlock` 且该规则自身声明了 `position: fixed`，修正其行内轴 inset 与宽度。
8. 把 `@adaptive` 改写为 `@media` 或 `@container`。
9. 如显式启用，最后追加低优先级根布局基础层。

第 7 步在第 6 步之后，因此它包裹的是换算后的流体值而不是原始像素。

## 普通长度

设设计宽度为 `D`、设计值为 `P`、流体下限和上限为 `L`、`U`。

```text
preferred = P / D × 100vw
minimum   = P × L / D px
maximum   = P × U / D px
result    = clamp(minimum, preferred, maximum)
```

负数会对边界重新排序，保证 `clamp()` 的最小值始终小于最大值。

上面的公式以同时提供两个可选边界为前提。对于正长度，只设下界生成 `max(minimum, preferred)`，只设上界生成 `min(preferred, maximum)`，不设边界生成 `calc(preferred)`。负长度的单侧操作符相反。例如，375px 画布上的 `-24px` 外边距只设 `maxWidth: 600` 时，输出为 `max(-6.4vw, -38.4px)`：边界限制的是绝对值，而非带符号的数值。显式使用 `strategy: 'viewport'` 会输出不带函数包装的视口表达式，并忽略边界。

### 单调性

`D`、`L`、`U` 为正数时，上式的**绝对值**对视口宽度单调不减：区间内按 `P` 的符号线性跟随，区间外是常数，且全程不改变符号。

`P` 为正时这就是「单调不减」。`P` 为负时公式是单调不增的——`-16px` 在 375 稿上编译成 `clamp(-20.48px, -4.26667vw, -13.65333px)`，视口越宽值越小。负长度（负外边距、外溢、反向偏移）本来就是靠远离零来变大的，所以真正守恒的量是绝对值，不是数值本身。

这条性质解释了一项有用的诊断：如果断点两侧采样值的绝对值变小，可能是跨断点切换画布时两张稿对同一元素给出了矛盾数字。命令行仅在两侧都能求值且至少一侧符合语法启发式时报告；它不是覆盖所有宽度的证明，也不追踪值的来源（见[断点处的倒退](./cli.zh-CN.md#断点处的倒退)）。

按绝对值比较不是细节。最早那版检查直接比数值，于是对每一条负长度都是反的：PC 稿要求更深的外溢会被报出来，而外溢在断点处几乎消失反倒不报。

## 文字长度与可访问性

纯 `vw` 文字无法充分响应浏览器文字缩放。设 `F` 为 `fontFluidity`：

```text
preferred = P × (1 - F) rem-part + P × F / D × 100vw
```

静态部分和上下界使用 `rem`，流体部分使用 `vw/cqi`。默认 `F = 0.35`，在设计宽度处仍严格等于设计值，同时在窗口变化与浏览器缩放之间取得平衡。

上下界使用 `rem`，因此会响应根字号变化。配置了下界时，根字号增大可能让表达式落到下界，此后按 `rem` 成比例增长。但这不保证根字号翻倍时文字达到原来的两倍：下表只达到 190%。省略 `fluid.minWidth` 时，没有下界接管，首选表达式仍混合根字号相对项和视口相对项。修改根字号也不等同于浏览器页面缩放测试，后者还可能改变 CSS 视口宽度。

Chrome 实测（375px 视口，默认配置，`font-size: 16px`）：

| 根字号 | 正文实际字号 | 相对 |
| --- | --- | --- |
| 16px | 16.00px | 100% |
| 20px | 18.97px | 119% |
| 24px | 22.77px | 142% |
| 32px | 30.36px | 190% |

同一组测量里，`width` 与 `padding` 全程保持 343px / 16px 不变——只有文字随缩放变化，布局尺寸不会跟着膨胀。

这组数字只覆盖默认配置的一个断面。项目应继续执行 WCAG 200% 缩放验收；编译公式不能替代真实可访问性测试。

### 静态部分锚在哪张画布上

`P × (1 - F)` 是一段固定长度，不随视口变化，所以它必须相对**某一个宽度**才有意义。默认就是 profile 自己的设计宽度：1440 稿上的 16px 就是 1440 处的 16px，说的是什么就是什么。

但组件库画布不是这样。Vant 画在 375 上，页面画在 750 上，两者描述的是**同一份设计的两套单位**——Vant 的 16px 和页面的 32px 是同一个尺寸。此时若各自锚在自己的画布上，两个 `rem` 部分会差整整一倍，在任何视口下都对不上：

```text
页面 32px on 750  → clamp(1.59867rem, calc(1.3rem  + 1.49333vw), 1.86rem)
Vant  16px on 375  → clamp(0.94867rem, calc(0.65rem + 1.49333vw), 1.098rem)   ← 修正前
```

流体项 `1.49333vw` 两边本来就相同（`P × F / D` 与画布同比例约掉了），差的全在静态项。实测 390px 视口下页面 26.62px、Vant 16.22px，Vant 的文字小了约 40%——而这是国内移动端相当常见的一组搭配。

所以静态部分锚定的是 `textAnchorWidth`，组件库画布一律继承所属 profile 的锚点。实现上等价于「先把长度换算成锚点画布的单位，再照常套公式」：

```text
P_anchor = P × A / D          A = textAnchorWidth
preferred = P_anchor × (1 - F) rem-part + P_anchor × F / A × 100vw
```

流体项完全不变（`P_anchor × F / A ≡ P × F / D`），只有静态项被归一化。非文字长度 `F = 1`，静态项恒为 0，产物逐字节不变——这也解释了为什么此前只有文字对不上，`padding` 一直是对的。

`textAnchorWidth` 也可以在 profile 上显式设置，用于手写画布之间存在同样换算关系的场合。

## 容器查询

Profile 的 `query.type` 为 `container` 时，`@adaptive` 输出 `@container`。通常同时把 `unit` 设为 `cqi`，使组件尺寸依赖自身容器而不是浏览器窗口。

容器必须由应用已有布局或 `root.container` 建立。不要让元素查询自己；为可复用组件选择稳定的祖先容器。

## 不转换范围

- `url()`、`local()`、`format()`；
- 引号字符串；
- 已含视口/容器单位的 `clamp()`、`min()`、`max()`——见下；
- `@font-face`、`@page`、`@property`、`@counter-style` 里的声明——它们描述的是资源或页面盒子，不是元素。打印边距换成 `vw` 不是同一个边距；
- 默认的 CSS 自定义属性（被组件库 `tokenPrefix` 认领的除外——认领本身即是开关）；
- 小于 `minPixelValue` 的值；
- 不超过 `hairline` 的绝对值；
- 过滤器或注释明确排除的内容。

## 幂等

`clamp()` / `min()` / `max()` 内部只要已经出现视口或容器单位，里面的 `px` 就原样保留。

这类表达式是**已经写好的有界流体值**——可能出自作者之手，也可能出自本插件上一趟。它的 `px` 是这个表达式自己的边界，不是从设计稿上量来的尺寸，再换算一次等于缩放两次。

直接结果是产物幂等：同一段 CSS 跑一遍和跑三遍结果完全相同。插件在 PostCSS 链里被挂了两次、或者组件库预编译过又被消费方编译一次，都不会产生嵌套 `clamp`。

范围刻意只限这三个函数。`calc(100vw - 32px)` 照常转换——那里的 `32px` 确实是设计稿尺寸，只是恰好挨着一个视口单位。

幂等不止于长度换算，还包括另外两件事：

- **忽略注释保留在产物里。** 被忽略的 `40px` 和没人管过的 `40px` 长得一模一样，注释一旦被吃掉，第二趟就会把它换算了。注释会被任何压缩器去掉，而作者的「别动这里」必须活过一趟以上。
- **根容器基础样式只注入一次。** 产物用 `/* postcss-adaptive-matrix foundation */` 与 `/* postcss-adaptive-matrix foundation end */` 把基础块括起来，第二趟只跳过这个区间——既不会把 `max-inline-size: 480px` 这类固定上限当成设计稿尺寸再缩放，预编译依赖后面拼接的应用 CSS 也仍会转换，同时不会叠出第二份。

一致性套件对**每一个**样例都断言了这一点：把产物再编译一遍必须原样返回。

## 嵌套

原生 CSS 嵌套里，`@adaptive` 与条件组规则（`@media`、`@supports`、`@layer`、`@container`、`@scope`、`@starting-style`）内部的声明属于外层元素，照常转换：

```css
.card {
  padding: 16px;          /* 默认画布 */

  @adaptive pc {
    padding: 32px;        /* pc 画布，并改写成 @media */
  }
}
```

插件没见过的 at-rule，内部的**规则**仍然会被处理，**直接声明**不会——未知语境下按元素样式处理是猜测。

## fixed 修正的边界

`fixedContainingBlock` 是唯一一处编译器改写定位的地方，它刻意做得很窄：

- 需要显式开启（`appPcPreset` 在建立居中列时默认开启，因为那正是问题出现的配置）；
- 只处理规则内**最终获胜的声明**是 `position: fixed` 的情况；同一规则内会遵守声明顺序与 `!important`。从其它规则继承的定位和跨规则胜者无法静态观察，按选择器组合去推测会制造隐藏的运行时耦合；
- 只处理行内轴；
- 只在值为 `0` 或 `100%` 这类无歧义形态上做替换，其余一律用 `calc()` 叠加，且幂等。

除此之外，编译器不猜测设计意图，不自动移动侧栏，也不注入 JavaScript。复杂布局应由 CSS Grid、Flexbox、容器查询和明确的端口规则表达。
