fix: 修复行缘、切分还原与语言解析并同步文档

保留逐字 margin/jiya.bias 字体后备方案;补齐 demo 重排、量测边界说明及共享规则 1.0.1 反例向量。

验证:完整构建通过,Node 376 项、WKWebView 64 项及原生 demo 9 项通过;Firefox 仍暂缓。
This commit is contained in:
2026-10-03 21:15:54 +08:00
parent 024ad5da93
commit 3a4fa1c932
33 changed files with 983 additions and 140 deletions
+95
View File
@@ -0,0 +1,95 @@
import assert from "node:assert/strict";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
import { test } from "node:test";
import { fileURLToPath } from "node:url";
const python = "python3";
const available = spawnSync(python, [ "-c", "import fontTools" ]).status === 0;
const tool = fileURLToPath(new URL("../tools/measure-bias.py", import.meta.url));
const fixture = `
from fontTools.fontBuilder import FontBuilder
from fontTools.pens.ttGlyphPen import TTGlyphPen
from fontTools.feaLib.builder import addOpenTypeFeaturesFromString
import sys
fb = FontBuilder(1000, isTTF=True)
names = ['.notdef', 'comma', 'commaHant', 'fullstop']
fb.setupGlyphOrder(names)
fb.setupCharacterMap({0xff0c: 'comma', 0xff0e: 'fullstop'})
glyphs = {}
for name, left, right in [('.notdef', 0, 0), ('comma', 50, 250),
('commaHant', 450, 550), ('fullstop', 650, 850)]:
p = TTGlyphPen(None)
if right > left:
p.moveTo((left, 0)); p.lineTo((right, 0)); p.lineTo((right, 150))
p.lineTo((left, 150)); p.closePath()
glyphs[name] = p.glyph()
fb.setupGlyf(glyphs)
fb.setupHorizontalMetrics({'.notdef': (1000, 0), 'comma': (1000, 50),
'commaHant': (1000, 450), 'fullstop': (1000, 650)})
fb.setupHorizontalHeader(ascent=800, descent=-200)
fb.setupNameTable({'familyName': 'BiasFixture', 'styleName': 'Regular',
'uniqueFontIdentifier': 'BiasFixture', 'fullName': 'BiasFixture',
'psName': 'BiasFixture'})
fb.setupOS2(sTypoAscender=800, sTypoDescender=-200, usWinAscent=800, usWinDescent=200)
fb.setupPost(); fb.setupMaxp()
features = 'languagesystem hani ZHT; feature locl { script hani; language ZHT; sub comma by commaHant; } locl;'
if sys.argv[2] == 'halt':
features += ' feature halt { sub fullstop by comma; } halt;'
addOpenTypeFeaturesFromString(fb.font, features)
fb.save(sys.argv[1])
`;
function run(args)
{
const result = spawnSync(python, args, { encoding: "utf8" });
assert.equal(result.status, 0, result.stderr || String(result.error));
return result.stdout;
}
function withFont(feature, check)
{
const dir = mkdtempSync(join(tmpdir(), "juzhen-bias-test-"));
try
{
const font = join(dir, "fixture.ttf");
run([ "-c", fixture, font, feature ]);
check(font);
}
finally { rmSync(dir, { recursive: true, force: true }); }
}
function table(output)
{
return JSON.parse(output.slice(output.indexOf("\n{") + 1));
}
test("measure-bias:全角句點入表,lang 僅選鍵且輸出說明 cmap/locl 限制",
{ skip: available ? false : "需工具本身的 Python fonttools 依賴" }, () =>
{
withFont("none", (font) =>
{
for (const lang of [ "zh-Hans", "zh-Hant" ])
{
const output = run([ tool, font, lang ]);
assert.equal(table(output)[lang]["."], "right");
assert.equal(table(output)[lang][","], "left", "仍量預設 cmap,未套 ZHT locl 替代字形");
assert.match(output, /未做 shaping.*未應用 locl/);
assert.match(output, /語言參數僅選 JSON 分類鍵/);
assert.match(output, /瀏覽器實際字形逐字微調/);
}
});
});
test("measure-bias:halt feature 存在只報存在,不承諾逐字覆蓋",
{ skip: available ? false : "需工具本身的 Python fonttools 依賴" }, () =>
{
withFont("halt", (font) =>
{
const output = run([ tool, font ]);
assert.match(output, /halt:有.*不保證涵蓋所有標點/);
assert.equal(table(output)["zh-Hans"]["."], "right", "feature 偵測不改預設輪廓分類");
});
});