更新换行算法和APP-PUSH
This commit is contained in:
138
spec_kit/Text Wrap/modules/width-measurement/plan.md
Normal file
138
spec_kit/Text Wrap/modules/width-measurement/plan.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# width-measurement(技术计划)
|
||||
|
||||
## 1. 计划目标
|
||||
|
||||
基于 `spec.md` 与 `设计说明文档/文档换行算法.md v1.2.1`,落地 Text Wrap 的“宽度测量与降级(approx mode)”能力,保证:
|
||||
|
||||
- APP 场景可进行**可靠的文本宽度测量**(成熟方案,结果可缓存)
|
||||
- WIDGET 场景允许不测量/测量失败时**确定性降级**(EN=wordCount,TC=charCount/graphemeCount)
|
||||
- 缓存 key、错误处理与降级路径固定,保证**同输入同输出**(确定性)
|
||||
- `fontSpec` 缺失字段直接报错(强制调用方补齐,避免隐式默认导致跨端漂移)
|
||||
- `availableWidth` 由“本机设备侧/上层”传入,本模块不内置 widgetProfiles 常量
|
||||
|
||||
## 2. 默认技术决策(本计划采用)
|
||||
|
||||
### 2.1 测量方案(成熟方法)
|
||||
|
||||
- **APP(RN/Expo)**:采用成熟的“离屏文本测量”库实现 `measureWidth(text, fontSpec)`
|
||||
- 建议选型:`react-native-text-size`(或团队已有的等价成熟方案)
|
||||
- 理由:可直接测量给定字体参数下的文本宽度,避免用 UI 渲染 onLayout 造成异步/不确定性
|
||||
|
||||
> 说明:本计划把“测量实现”作为本子模块交付的一部分,而不是仅定义接口;但仍保留注入点,便于替换实现或做平台差异适配。
|
||||
|
||||
### 2.2 缓存策略(由我统一定义)
|
||||
|
||||
采用“两级缓存 + 有上限”的策略:
|
||||
|
||||
1. **字符串测量缓存**:`(text, fontSpecKey, contextProfile) -> width`
|
||||
2. **切片测量缓存**:`(start, end, fontSpecKey, contextProfile) -> width`
|
||||
|
||||
并且:
|
||||
|
||||
- 缓存采用**模块级常驻缓存**(跨 `wrapText()` 多次调用复用),提升性能
|
||||
- 缓存容量必须有上限(建议 LRU 或“超限清空 + 打点”),避免内存无限增长
|
||||
|
||||
### 2.3 fontSpec 严格性(缺失就报错)
|
||||
|
||||
`fontSpec` 在 APP 测量路径下必须包含:
|
||||
|
||||
- `fontSize`
|
||||
- `fontFamily`
|
||||
- `fontWeight`
|
||||
|
||||
缺失任意字段时:
|
||||
|
||||
- **直接抛错**(错误信息必须为简体中文,并指出缺失字段与调用方应补齐的位置)
|
||||
- 不允许在测量层做“隐式默认值”(避免跨端不一致与线上难定位)
|
||||
|
||||
### 2.4 contextProfile 的口径
|
||||
|
||||
为保证缓存与确定性,本模块定义 `contextProfile`:
|
||||
|
||||
- APP:`APP|<platform>|<scale?>`(按需扩展,但必须固定字段顺序与拼接方式)
|
||||
- WIDGET:`WIDGET|<widgetSize?>`(如果上层传入 widgetSize,可写入;否则仅 WIDGET)
|
||||
|
||||
> 注意:你已确认 `availableWidth` 由设备侧传入,本模块不内置 widgetProfiles;但缓存仍需要一个 profile 字段区分 APP/WIDGET。
|
||||
|
||||
## 3. API 设计(实现阶段稳定契约)
|
||||
|
||||
### 3.1 对外接口(建议)
|
||||
|
||||
- `buildFontSpecKey(fontSpec) -> string`
|
||||
- `measureWidthCached({ text, context, fontSpec, contextProfile }) -> { width: number | null; meta: { isApprox: boolean; reason?: 'WIDTH_UNKNOWN' | 'MEASURE_FAILED' } }`
|
||||
- `measureSliceWidthCached({ tokens, start, end, joinTokens, ... }) -> { width: number | null; meta: ... }`
|
||||
|
||||
### 3.2 approx mode 口径(必须确定性)
|
||||
|
||||
当出现任一情况时进入 approx mode(返回 `width=null`,并由上层按 approx 口径计算 lineLen):
|
||||
|
||||
- `measureWidth` 不可用
|
||||
- `measureWidth` 抛错
|
||||
- `measureWidth` 返回 NaN/Infinity/负数
|
||||
- context=WIDGET 且上层选择不测量
|
||||
|
||||
approx 的“长度单位”口径固定为:
|
||||
|
||||
- EN:`wordCount`
|
||||
- TC:`graphemeCount`(优先;若上层仅有 charCount,则使用 charCount,但必须在实现中写死选择)
|
||||
|
||||
并必须打点 reason:
|
||||
|
||||
- `WIDTH_UNKNOWN`:没有测量能力/不启用测量
|
||||
- `MEASURE_FAILED`:测量抛错或返回非法值
|
||||
|
||||
## 4. 实现步骤(按落地顺序)
|
||||
|
||||
### 4.1 定义类型与错误
|
||||
|
||||
- 定义 `FontSpec`、`ContextProfile`、`MeasureResult` 类型
|
||||
- 定义 `MissingFontSpecError`(或统一错误码),错误信息简体中文
|
||||
|
||||
### 4.2 实现 fontSpecKey
|
||||
|
||||
实现 `fontSpecKey = fontFamily|fontWeight|fontSize`:
|
||||
|
||||
- 字段顺序固定
|
||||
- `fontSize` 转为字符串(禁止浮点格式漂移:建议 `String(fontSize)`,并要求输入是 number 且有限)
|
||||
|
||||
### 4.3 实现缓存容器
|
||||
|
||||
- 实现一个带上限的缓存(LRU 优先;若不引入依赖,先用 Map + 超限清空)
|
||||
- Key 生成必须确定性:
|
||||
- `textKey = <contextProfile>|<fontSpecKey>|<text>`
|
||||
- `sliceKey = <contextProfile>|<fontSpecKey>|<start>|<end>`
|
||||
|
||||
### 4.4 接入成熟测量库(APP)
|
||||
|
||||
- 封装 `measureWidthImpl(text, fontSpec) -> number`
|
||||
- 对返回值做校验(有限、非负)
|
||||
- 失败捕获并走 approx mode(并打 `MEASURE_FAILED`)
|
||||
|
||||
### 4.5 WIDGET 策略
|
||||
|
||||
- 默认允许调用方传入 `measureWidthImpl`(如果 Widget 侧实现了测量)
|
||||
- 若不传/不启用:直接 approx mode,并 reason=`WIDTH_UNKNOWN`
|
||||
|
||||
## 5. 测试计划(Vitest)
|
||||
|
||||
### 5.1 纯函数与缓存测试(必须)
|
||||
|
||||
- `fontSpecKey`:同输入同输出;缺字段抛错
|
||||
- 缓存命中:同 key 不重复调用底层 `measureWidthImpl`
|
||||
- 缓存隔离:不同 `contextProfile/fontSpecKey` 不互相污染
|
||||
|
||||
### 5.2 降级路径测试(必须)
|
||||
|
||||
- 缺少 `measureWidthImpl` -> approx mode + reason=`WIDTH_UNKNOWN`
|
||||
- `measureWidthImpl` 抛错/返回 NaN -> approx mode + reason=`MEASURE_FAILED`
|
||||
|
||||
> 说明:测量库本身的准确性不在单测中做像素级断言;单测只验证“缓存与降级语义确定性”。
|
||||
|
||||
## 6. 完成定义(DoD)
|
||||
|
||||
- APP/WIDGET 下 `width` 返回语义清晰:可测量返回 number,不可测量返回 null
|
||||
- `fontSpec` 缺字段必定报错(简体中文错误信息)
|
||||
- 缓存 key 与容量策略确定性,且有上限
|
||||
- approx mode 触发条件、reason 标记、EN/TC 近似口径写死
|
||||
- 单测覆盖缓存命中/隔离与降级路径
|
||||
|
||||
51
spec_kit/Text Wrap/modules/width-measurement/spec.md
Normal file
51
spec_kit/Text Wrap/modules/width-measurement/spec.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# width-measurement(子模块规范)
|
||||
|
||||
## 子模块名称
|
||||
|
||||
width-measurement(宽度测量与降级)
|
||||
|
||||
## 目标描述
|
||||
|
||||
提供统一的宽度测量接口与缓存策略,并定义“宽度不可用”时的确定性降级行为(approx mode),确保 APP 与 WIDGET 在测量能力差异下仍能:
|
||||
|
||||
- 保持搜索/评分流程可运行
|
||||
- 输出可解释(meta 打点)
|
||||
- 性能稳定(缓存与上限)
|
||||
|
||||
## 输入/输出定义
|
||||
|
||||
### 输入
|
||||
|
||||
- `context: 'APP' | 'WIDGET'`
|
||||
- `fontSpec?: { fontSize: number; fontWeight?: string; fontFamily?: string }`
|
||||
- `measureWidth?: (text: string, fontSpec) => number`(可选注入)
|
||||
- `text: string`
|
||||
- `slice?: { start: number; end: number }`(可选:段落切片测量)
|
||||
|
||||
### 输出
|
||||
|
||||
- `width: number | null`
|
||||
- 当不可用/失败时返回 `null`(触发 approx mode)
|
||||
- `meta?: { isApprox: boolean; reason?: 'WIDTH_UNKNOWN' | 'MEASURE_FAILED' }`
|
||||
|
||||
并提供缓存约定(逻辑输出):
|
||||
|
||||
- 字符串测量缓存 key:`(text, fontSpecKey, contextProfile)`
|
||||
- 切片测量缓存 key:`(start, end, fontSpecKey, contextProfile)`
|
||||
|
||||
## 验收标准(可验证)
|
||||
|
||||
- **测量一致**:在 APP 注入 `measureWidth` 时,同一输入重复测量命中缓存(不会重复计算)
|
||||
- **失败降级**:`measureWidth` 缺失/抛错/返回 NaN 时:
|
||||
- 输出 `width=null`
|
||||
- meta 标记 `isApprox=true` 且 reason 可追踪
|
||||
- **approx mode 口径一致**:
|
||||
- EN:宽度近似值使用 `wordCount`
|
||||
- TC:宽度近似值使用 `charCount`(或 grapheme 数)
|
||||
- **确定性**:相同输入在相同 contextProfile 下,测量与降级行为一致
|
||||
|
||||
## 依赖与关联
|
||||
|
||||
- **被依赖**:`search-engine-app`、`search-engine-widget`、`overflow-fallback`
|
||||
- **依赖**:`core-contract`(fontSpecKey 规范化、切片文本重组)
|
||||
|
||||
160
spec_kit/Text Wrap/modules/width-measurement/tasks.md
Normal file
160
spec_kit/Text Wrap/modules/width-measurement/tasks.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# width-measurement(任务清单)
|
||||
|
||||
> 对应计划:`spec_kit/Text Wrap/modules/width-measurement/plan.md`
|
||||
>
|
||||
> 状态含义:`[ ]` 未完成,`[x]` 已完成。
|
||||
> 执行完本清单后,需要在 `spec_kit/overview.md` 的 `Text Wrap` 条目下补充“已完成编码/任务执行完毕”的标记(见最后一节)。
|
||||
|
||||
---
|
||||
|
||||
## 0. 任务标记规则
|
||||
|
||||
- 用勾选框标记执行状态:
|
||||
- `[ ]` 未完成
|
||||
- `[x]` 已完成
|
||||
- 每个任务必须可独立验收(有明确产出与检查方式)。
|
||||
- 所有代码注释必须为简体中文,并把“缓存 key / 降级语义 / 报错口径”写死,避免后续模块漂移。
|
||||
|
||||
---
|
||||
|
||||
## 1. 前置检查(依赖与约束确认)
|
||||
|
||||
- [x] 1.1 确认 `availableWidth` 由设备侧传入(本模块不内置 widgetProfiles)
|
||||
- **验收**:在本模块实现中不引入任何固定宽度常量表;仅消费上层传入的宽度与 profile。
|
||||
|
||||
- [x] 1.2 确认 APP 测量方案采用成熟库(默认 `react-native-text-size`)
|
||||
- **验收**:`client/package.json` 中存在该依赖(或团队等价成熟方案),并且测量封装函数仅依赖该库/注入点。
|
||||
|
||||
---
|
||||
|
||||
## 2. 依赖与目录骨架(客户端侧实现)
|
||||
|
||||
- [x] 2.1 新建目录 `client/src/features/textWrap/measure/`
|
||||
- **产出**(建议文件):
|
||||
- `types.ts`(FontSpec/ContextProfile/MeasureResult)
|
||||
- `errors.ts`(缺字段报错)
|
||||
- `fontSpecKey.ts`
|
||||
- `cache.ts`(两级缓存 + 上限)
|
||||
- `measureWidthImpl.ts`(对接成熟测量库)
|
||||
- `measureWidthCached.ts`
|
||||
- `measureSliceWidthCached.ts`
|
||||
- `__tests__/widthMeasurement.test.ts`
|
||||
- `index.ts`(统一导出)
|
||||
- **验收**:目录存在,TS 可正常 import(不报路径错误)。
|
||||
|
||||
- [x] 2.2 新增依赖 `react-native-text-size`(如项目未安装)
|
||||
- **命令**(示例):
|
||||
- `cd client && npm install react-native-text-size`
|
||||
- **验收**:
|
||||
- 安装成功
|
||||
- `npm test` 不受影响(后续任务再补充本模块测试)
|
||||
|
||||
---
|
||||
|
||||
## 3. fontSpec 强校验(缺失直接报错)
|
||||
|
||||
- [x] 3.1 定义 `FontSpec` 类型(必须字段:`fontSize/fontFamily/fontWeight`)
|
||||
- **验收**:类型层面可表达“必填字段”,并在运行时也做校验。
|
||||
|
||||
- [x] 3.2 实现运行时校验与错误(简体中文)
|
||||
- **要求**:
|
||||
- 缺失任一字段直接抛错
|
||||
- 错误信息包含:缺失字段名 + 建议调用方补齐的位置(例如 `wrapText({ fontSpec: ... })`)
|
||||
- 禁止隐式默认值
|
||||
- **验收**:单测断言会抛错且错误信息包含缺失字段名。
|
||||
|
||||
---
|
||||
|
||||
## 4. fontSpecKey 与 contextProfile(确定性 key 体系)
|
||||
|
||||
- [x] 4.1 实现 `buildFontSpecKey(fontSpec)`
|
||||
- **规则**:`fontFamily|fontWeight|fontSize`(顺序固定)
|
||||
- **验收**:同输入同输出;fontSize 非有限数时报错。
|
||||
|
||||
- [x] 4.2 定义 `contextProfile` 拼接规则并实现 helper
|
||||
- **要求**:
|
||||
- APP:`APP|<platform>|<scale?>`(字段顺序固定)
|
||||
- WIDGET:`WIDGET|<widgetSize?>`(无 widgetSize 时为 `WIDGET`)
|
||||
- **验收**:输出字符串稳定;不同 profile 必须产生不同缓存 key。
|
||||
|
||||
---
|
||||
|
||||
## 5. 两级缓存(有上限 + 确定性)
|
||||
|
||||
- [x] 5.1 实现缓存容器(模块级常驻)
|
||||
- **要求**:
|
||||
- 字符串测量缓存(textKey)
|
||||
- 切片测量缓存(sliceKey)
|
||||
- 容量上限策略:LRU 优先;若不引入依赖,先 Map + 超限清空(并预留打点钩子)
|
||||
- **验收**:单测可验证缓存命中会减少底层测量调用次数。
|
||||
|
||||
- [x] 5.2 定义 key 生成规则并实现
|
||||
- **规则**:
|
||||
- `textKey = <contextProfile>|<fontSpecKey>|<text>`
|
||||
- `sliceKey = <contextProfile>|<fontSpecKey>|<start>|<end>`
|
||||
- **验收**:key 生成不依赖对象遍历顺序;同输入同 key。
|
||||
|
||||
---
|
||||
|
||||
## 6. 测量实现与降级(approx mode)
|
||||
|
||||
- [x] 6.1 实现 `measureWidthImpl(text, fontSpec)`(APP)
|
||||
- **要求**:
|
||||
- 依赖成熟库测量宽度
|
||||
- 返回值必须校验:有限且非负
|
||||
- **验收**:在单测中用 mock 替代真实库,验证封装逻辑与校验逻辑即可(不做像素级断言)。
|
||||
|
||||
- [x] 6.2 实现 `measureWidthCached(...)`
|
||||
- **行为**:
|
||||
- 正常测量:返回 `{ width:number, meta:{ isApprox:false } }`
|
||||
- 进入 approx:返回 `{ width:null, meta:{ isApprox:true, reason } }`
|
||||
- **approx 触发条件**(任一满足):
|
||||
- 未提供测量能力 / context=WIDGET 且上层不启用测量 -> `WIDTH_UNKNOWN`
|
||||
- 抛错/NaN/Infinity/负数 -> `MEASURE_FAILED`
|
||||
- **验收**:单测覆盖两类 reason。
|
||||
|
||||
- [x] 6.3 实现 `measureSliceWidthCached(...)`(切片测量)
|
||||
- **要求**:
|
||||
- 通过 `joinTokens(start,end)` 生成切片文本
|
||||
- 使用切片缓存避免 DP 反复测量
|
||||
- **验收**:单测验证同 sliceKey 不重复调用底层测量。
|
||||
|
||||
---
|
||||
|
||||
## 7. 单元测试(Vitest)
|
||||
|
||||
- [x] 7.1 新建 `__tests__/widthMeasurement.test.ts` 并覆盖以下用例
|
||||
- **fontSpec 报错**:缺字段必抛错(错误信息含字段名)
|
||||
- **缓存命中**:同 key 不重复调用底层测量 mock
|
||||
- **缓存隔离**:不同 `contextProfile/fontSpecKey` 不互相污染
|
||||
- **降级**:
|
||||
- 缺测量能力 -> `width=null` + `WIDTH_UNKNOWN`
|
||||
- 测量抛错/NaN -> `width=null` + `MEASURE_FAILED`
|
||||
- **验收**:`npm test` 稳定通过。
|
||||
|
||||
---
|
||||
|
||||
## 8. 最终自检清单(合入前)
|
||||
|
||||
- [x] 8.1 `npm test` 通过(包含本模块新增用例)
|
||||
- **验收**:不影响现有测试文件。
|
||||
|
||||
- [x] 8.2 `npx tsc --noEmit` 通过(或项目既有 TS 检查命令通过)
|
||||
- **验收**:无类型错误。
|
||||
|
||||
- [x] 8.3 注释与口径自检(简体中文)
|
||||
- **检查点**:
|
||||
- `fontSpec` 缺字段“必须报错”
|
||||
- key 生成规则与 contextProfile 口径
|
||||
- approx mode 的 reason 语义(WIDTH_UNKNOWN / MEASURE_FAILED)
|
||||
- **验收**:后续模块开发者只看代码也不会产生歧义。
|
||||
|
||||
---
|
||||
|
||||
## 9. 文档回写(任务清单执行完毕后必须做)
|
||||
|
||||
- [x] 9.1 在 `spec_kit/overview.md` 的 `Text Wrap` 条目下补充执行状态
|
||||
- **建议写法**:
|
||||
- 增加一行:`- **已完成编码(阶段性)**:width-measurement(宽度测量与降级)`
|
||||
- **验收**:overview 能反映该子模块已完成,便于全局追踪。
|
||||
|
||||
Reference in New Issue
Block a user