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 偵測不改預設輪廓分類"); }); });