☰
Maple Mono 字体特性自动化生成模块剖析:基于 AST 的 OpenType Feature 工程实践
2026/9/30 7:03:20 网站建设 项目流程
  • 开发工具

【免费下载链接】maple-font

Maple Mono: Open source monospace font with round corner, ligatures and Nerd-Font icons for IDE and terminal, fine-grained customization options. 带连字和控制台图标的圆角等宽字体,中英文宽度完美2:1,细粒度的自定义选项

项目地址:https://gitcode.com/GitHub_Trending/ma/maple-font
点击查看免费下载

Maple Mono 是一套圆角等宽字体,其特色之一是通过 OpenType 特性(字符变体 cv、文体集 ss、连字 calt 等)实现「细粒度自定义」。这些特性并非手工编写,而是由source/py/feature/模块以AST(抽象语法树)方式程序化定义并生成.fea文件。本文以该模块的官方文档(source/py/feature/README.md)为主线,结合仓库源码讲解其设计动机、核心工具类、自定义标签、无限连字开关与可变字体特性冻结策略,读完你既能独立复现uv run task.py fea的生成链路,也能在自己的字体工程中复用这套 AST 方案。

为什么需要程序化生成 Feature 文件

绝大多数开源字体项目是手工维护OpenType feature 文件的:为每一种连字、每一组字符变体手写sub ... by ...;规则,再反复调试规则顺序与上下文。虽然 fonttools 自带ast模块可以自动化操作 feature 文件,但其文档严重不足,实际落地成本很高。这正是本模块诞生的原因:在ast.py中重新实现了一套自洽的 AST 工具,用它生成.fea文件,并在构建流程中自动整合(见 source/py/feature/README.md 的 Why 一节)。

把字体特性当作「数据 + 代码」来管理带来几个直接收益:

  • 连字规则、上下文规则可以参数化、循环生成,例如tag_upper只需传入一个文本列表就能批量生成大写标签连字;
  • 同一套定义可以同时服务于 Regular 与 Italic、中文与西文等不同构建产物,避免规则漂移;
  • 生成过程是确定性的,每次构建产物一致,便于 CI 复现与 diff。

模块总览:feature/的组成与职责

source/py/feature/采用分层结构组织所有 OpenType 特性(见 目录清单 与 README Overview):

组件职责典型产物
ast.py核心 AST 工具:类、查找表、特性、规则生成与文本序列化Clazz/Lookup/Feature/subst_liga等
regular.pyRegular 字形的特性入口:字母类、cv 列表、ss 列表class_list_regular、cv_list_regular()、ss_list_regular()
italic.pyItalic 字形的特性入口class_list_italic、cv_list_italic()、ss_list_italic()
base/基础特性与通用类:数字、大小写、本地化形式等locl/case/number/ccmp特性
calt/默认连字(calt 特性)的分模块实现空白、冒号、箭头、标签、转义、管道等连字
cv/字符变体(cv01–cv99)每个cvNN.py定义一个CharacterVariant
ss/文体集(ss01–ss11)每个ssNN.py定义一个StylisticSet

入口模块 source/py/feature/init.py 对外暴露generate_fea_string,把上面所有组件编排成一段完整的 fea 文本;生成的文件最终落在 source/features/ 目录(如regular.fea、italic.fea、cn.fea、regular_cn.fea、italic_cn.fea)。

快速上手:一条命令生成全部 Feature 文件

特性在构建时会自动应用(task.py的字体构建流程会调用本模块),通常无需手工干预。当需要手动重新生成 fea 文件时,在仓库根目录执行:

uv run task.py fea

该命令等价于调用 task.py 中注册的fea子命令(可加--output指定输出目录,默认./source/features),实际逻辑在 source/py/task/fea.py:

  1. 依次调用generate_fea_string(...)生成regular.fea、italic.fea,调用generate_fea_string_cn_only()生成cn.fea,再生成中西文合并版regular_cn.fea、italic_cn.fea,每个文件头部写入Auto generated by python task.py fea标记;
  2. 用get_all_calt_text()、get_cv_desc()、get_cv_italic_desc()、get_cv_cn_desc()、get_ss_desc()同步更新 source/features/README.md 中<!-- CALT -->、<!-- CV -->、<!-- SS -->等区块;
  3. 用get_total_feat_dict()回写 source/schema.json 的feature_freeze属性描述与 config.json 的feature_freeze默认值;
  4. 根据所有含 lookup 的特性(get_freeze_moving_rules())自动重写 source/py/in_browser.py 中的MOVING_RULES列表,供浏览器端特性预览使用。

如果你只想在 Python 中拿到一段 fea 字符串,可以像 README 的 Generating Features 一节 那样直接调用入口函数:

from source.py.feature import generate_fea_string # 生成包含中文特性的 Regular 字形 fea 文本 fea_string = generate_fea_string(is_italic=False, is_cn=True) print(fea_string)

注意该函数的签名是generate_fea_string(is_italic, is_cn, ...)(见 source/py/feature/init.py#L48-L131),调用时以源码签名为准。其完整参数与作用如下:

参数默认值作用
is_italic必填是否生成 Italic 字形特性(决定选用class_list_italic/cv_list_italic/ss_list_italic)
is_cn必填是否包含中文特性(追加cv_list_cn,即 cv96–cv99)
is_normalFalse是否生成「默认推荐」预设;为真时会把启用特性移入 calt 以便冻结
is_caltTrue是否启用 calt 连字;为False时直接清空 calt 内容实现无连字版
enable_infiniteTrue是否启用=-~等无限连字
enable_tagTrue是否启用TODO:等代码标签连字
variable_enabled_feature_listNone可变字体中需要「冻结启用」的特性 tag 列表,传入非空即进入可变模式
remove_italic_caltFalse是否移除 Italic 专用的 calt 连字

generate_fea_string还会校验类列表必须以@Var和@HexLetter结尾(否则抛出TypeError),保证连字上下文类始终可用(source/py/feature/init.py#L82-L83)。当 calt 内容为空时,会注入ast.EMPTY_FEAT_CONTENT(一个占位替换规则)以消除 fonttools 的空特性告警(source/py/feature/init.py#L120-L122)。

AST 核心工具:从 Python 对象到.fea文本

整套系统的基石是 source/py/feature/ast.py 中的几个数据结构。所有对象最终都会序列化为带缩进的Line,再由create()拼装成完整 fea 文本。

Clazz:字形类

Clazz表示一组字形(glyph class)。其state()会生成@Name = [...];声明,use()返回@Name引用(source/py/feature/ast.py#L17-L28):

from source.py.feature.ast import Clazz, subst cls_digit = Clazz("Digit", ["zero", "one", "two", "three"]) cls_digit.state() subst(cls_digit.use(), "a", "b", "c")

对应生成的 fea 文本(README 原始示例):

@Digit = [zero, one, two, three]; sub @Digit a' b by c;

类的嵌套是允许的:Clazz的元素可以是字符串,也可以是另一个Clazz,最终由__gly递归展开。仓库里大量复用这一能力,例如 source/py/feature/base/clazz.py 中cls_digit = Clazz("Digit", [cls_zero, cls_one, "two", ...]),而 source/py/feature/regular.py#L106 中的cls_var = Clazz("Var", ["_", "__", *cls_letters_list, cls_digit])直接把字母类嵌套进变量类——生成的@Var声明(见 source/features/regular.fea#L36)即为嵌套展开的结果。

Lookup:替换规则查找表

Lookup定义一个带名字的 lookup 块,内部包含若干规则行,可附带注释描述(source/py/feature/ast.py#L31-L54):

from source.py.feature.ast import Lookup, subst lookup_example = Lookup( name="example_lookup", desc="Example substitution", content=[ subst("a", "b", None, "c"), ], )

生成:

# Example substitution lookup example_lookup { sub a b' by c; } example_lookup;

注意subst(prefix, glyph, suffix, replace)的语义:无前后缀时不会在 glyph 上打'标记,有上下文时会自动生成b'这样的定位标记(见 source/py/feature/ast.py#L366-L384 的 docstring 示例)。

Feature:OpenType 特性容器

Feature把若干 Lookup/规则/类聚合进一个feature <tag> { ... }块(source/py/feature/ast.py#L57-L93):

from source.py.feature.ast import Feature feature_example = Feature( tag="calt", content=[lookup_example], )

生成:

feature calt { # Example substitution lookup example_lookup { sub a b' by c; } example_lookup; }

Feature构造时会递归展平内容并自动记录has_lookup,该标志在可变字体「冻结特性」时用于判断是否能整体搬移 lookup。

CharacterVariant与StylisticSet:带文档的特性

cv/与ss/下定义的特性都是FeatureWithDocs的子类:CharacterVariant(tag 形如cv01,id 合法范围 1–99)和StylisticSet(tag 形如ss01,id 合法范围 1–20),超出范围直接抛TypeError(source/py/feature/ast.py#L120-L188)。它们除了生成特性本身,还会生成cvParameters/featureNames块,把「特性说明文字」写进字体的 UI 标签,让用户在系统字体面板里看到可读的名称。例如cv01的定义(source/py/feature/cv/cv01.py#L67-L71):

cv01_feat = ast.CharacterVariant( id=1, desc="Normalize special symbols (`@ $ & % Q => ->`)", content=cv01_subst(), version="7.0", example="@$&", )

其内容cv01_subst()使用subst_map批量生成「字形 → 字形.后缀」的直接替换规则(source/py/feature/cv/cv01.py#L8-L64),这也是文档中「方法 2:直接字形替换」的典型实现。ss08则是「方法 1:连字规则移入 calt」的典型:用subst_liga为<<-、>>-、<<=、>>=、-<<、->>、=<<、=>>等双箭头连字生成带 ignore 规则和上下文替换的 lookup(见 source/py/feature/ss/ss08.py#L5-L113)。

create():最终序列化

create(content, indent=2)负责把任意嵌套的 Line/Clazz/Lookup/Feature 展平为最终文本(source/py/feature/ast.py#L333-L351):

from source.py.feature.ast import create fea_content = create([feature_example]) print(fea_content)

它会在特性/查找表/注释块之间自动插入空行、去重连续空行,并按indent * level控制缩进——README 中所有「Generated fea string」示例的输出格式,正是由它保证的。生成的完整样例可查看仓库内真实产物 source/features/regular.fea,其开头即为@Zero、@Digit、@Uppercase、@A–@Z、@Var、@HexLetter等类声明与languagesystem DFLT dflt;。

高频辅助函数

除了四大类,ast.py还提供一批高频工具(均带有 docstring 示例):

  • gly(g, suffix="", overwrite=False):把任意字符序列规范成字形名,长度 >1 的序列默认拼成x_y.liga连字字形名,例如gly("++")→plus_plus.liga;标点自动映射到 ADOBE 命名,如{→braceleft(source/py/feature/ast.py#L281-L302);
  • cls(...):生成内联字形类,例如cls(["a", "@", "++", cl])→[a at plus_plus.liga @cl](source/py/feature/ast.py#L313-L323);
  • subst_liga(source, target, lookup_name, desc, surround, ign_prefix, ign_suffix, extra_rules):一键生成带上下文与 ignore 规则的连字 lookup,内部用SPC占位字形分两步替换(先替换首字为SPC、再替换尾字为连字字形),支持surround上下文元组与ign_prefix/ign_suffix抑制规则(source/py/feature/ast.py#L414-L517);
  • ign(prefix, glyph, suffix):生成ignore sub ...;规则(source/py/feature/ast.py#L520-L533);
  • langsys/lang/script:生成languagesystem、language、script语句;
  • clone_empty/filter_empty:基于EMPTY_FEAT_SYMBOL = "$$$"占位符机制,把「仅在 Italic 有效」的特性在 Regular 构建中克隆成占位并过滤(source/py/feature/ast.py#L564-L591)。

实战一:自定义代码标签连字

字体内置了一批「带圆角底色边框」的标签连字,触发文本固定;你可以用subst_liga自由改写触发文本。README 原始示例:

subst_liga( source="TODO:", target="tag_todo.liga", lookup_name="todo_colon", )

这段代码对应 tag.py 中的tag_suffix_colon:它把TODO:替换为tag_todo.liga字形,lookup 名为todo_colon(冒号后缀形式)。source也支持列表形式(如["T", "O", ...]),target 与 lookup_name 缺省时分别回退为gly(source)与 target。

对于内置标签文本之外的额外标签,用tag_custom(README 原始示例):

tag_custom( [ (":attention:", "[attention]"), ("_noqa_", "(noqa)"), # ("_alter_", "<alter>"), ], bg_cls_dict, )

它会把:

:attention: _noqa_

转换为对应样式的标签字形。tag_custom的实现细节(source/py/feature/calt/tag.py#L133-L221)值得注意:

  • source与target的长度必须相等,否则抛ValueError;target 末字符必须是<>()[]之一(决定标签边框形状),中间只能是 ASCII 字母;
  • 字母字形统一用@X(大写)引用,非字母用ast.gly归一化;
  • 生成规则时从最后一个字形向前反向生成替换链,最终 lookup 名为custom_tag_<中间字符>;
  • 背景类由bg_cls_dict(字母 →BgX类)提供,例如Q与Q.cv01会构成BgQ = [Q.bg Q.bg.cv01](见 tag.py 的get_lookup)。

calt/tag.py还提供tag_upper(把[TODO]样式的方括号大写文本替换成tag_todo.liga,仅接受built_in_tag_text中的内置标签名,否则跳过并打印提示)与tag_any(todo))圆括号样式,含cls_var上下文抑制),built_in_tag_text内置了trace/debug/info/warn/error/fatal/todo/fixme/note/hack/mark/eror/warning等常见日志级别与标记词(source/py/feature/calt/tag.py#L4-L18)。

限制(README 原文):

  1. 自定义标签缺少间距优化;
  2. 字距(letter spacing)> 0 时标签可能被破坏;
  3. 标签会继承文本颜色。

实战二:关闭无限连字

=、-、~、#等符号默认支持「无限连字」(如==>、<--、~~~等按序列无限延长)。要整体关闭这一能力,按 README 的做法把 source/py/feature/calt/_infinite_utils.py 中开关置为False:

__USE_INFINITE = False

实际实现上,_infinite_utils.py用InfiniteHelper单例持有状态(set()/get(),默认开启,见 源码 L4-L27),并暴露ignore_when_enabled(...)与ignore_when_disabled(...)两个辅助方法,供其他模块条件性地加入或排除无限连字规则。最典型的消费方是cv01:当无限连字开启时,=>、->、<--、<=>等一长串无限连字序列会参与cv01归一化替换;关闭时则改为替换<=.sta、>=.end这类「序列起止字形」(见 source/py/feature/cv/cv01.py#L37-L60)。infinite_rules()则负责构造无限连字的核心替换组(start/mid/end 三段字形递进,见 源码 L30-L59)。

另外注意:generate_fea_string(..., enable_infinite=False)在调用链上会先执行infinite_helper.set(enable_infinite),因此通过参数关闭与直接改源码开关效果一致。

实战三:可变字体中的特性冻结策略

这是 README 中信息密度最高的一节,也是把特性工程和构建产物挂钩的关键。为什么需要「冻结」?因为在可变字体(variable font)格式下,某些特性无法像静态字体一样在运行时自由切换,只能把特性内容预先「烘焙」进特定变体。仓库采用两种策略:

  1. 把连字规则搬进 calt(如ss08这类含 lookup 的特性)——对应代码路径是generate_fea_string(..., variable_enabled_feature_list=[...]):凡 tag 命中列表且has_lookup为真的特性,其 lookup 会被整体抽取并追加到calt_feat.content,原特性内容清空(source/py/feature/init.py#L100-L118)。这一逻辑正对应文档所说的「common.py(页面展示层)实现方法 1」——前端预览页据此在浏览器端动态决定启用哪些特性;
  2. 直接字形替换(如cv01的Q → Q.cv01)——不搬 lookup,靠字形名后缀实现切换。

当前限制(README 原文):方法 2 尚不能在可变字体格式中完整支持。因此V7.0 中所有可变格式的变体除了 family name 之外完全一致,需要时通过构建命令的--apply-fea-file标志按需应用特性。可变字体的特性现在改为通过 Python 动态加载;文档同时提醒:**启用字体连字(ligatures)**才能让可变字体的全部特性生效(不推荐,会影响整体观感)。

配合这套机制,仓库还维护了一份「默认推荐启用」的特性清单normal_enabled_features,即cv01/cv02/cv33/cv34/cv35/cv36/cv61/cv62/ss05/ss06/ss07/ss08(source/py/feature/init.py#L24-L37),fea.py会把它同步进根目录 README 的<!-- NORMAL -->区块,作为构建可变字体时的默认开关。

从定义到落地:完整构建链路

最后把「定义」与「落地」串起来:generate_fea_string的输出顺序是「字形类声明 → languagesystem/语言 → base 特性 → cv/ss 特性」(source/py/feature/init.py#L124-L131)。其中 base 特性由 source/py/feature/base/init.py 组装:get_base_features把locl(本地化形式)、case(大小写)、number(数字特性)、calt聚合进aalt,并追加ccmp(合成字形)——最终形成以aalt为总入口、各特性可独立引用的结构。cn.fea则走generate_fea_string_cn_only(),只输出中文专用特性(locl+ccmp+ cv96–cv99,见 source/py/feature/init.py#L134-L140)。

至此,从task.py fea一条命令出发,你可以完成:ast.py对象定义 →generate_fea_string编排 →.fea文件落盘 → README / schema / config 数据同步 → 构建时被字体工具消费的完整闭环。这套「以 AST 为中心」的特性工程方案,既保证了 Maple Mono 数百条连字与变体规则的一致性,也让每一位字体开发者可以按本文的步骤独立复现和扩展。

  • 开发工具

【免费下载链接】maple-font

Maple Mono: Open source monospace font with round corner, ligatures and Nerd-Font icons for IDE and terminal, fine-grained customization options. 带连字和控制台图标的圆角等宽字体,中英文宽度完美2:1,细粒度的自定义选项

项目地址:https://gitcode.com/GitHub_Trending/ma/maple-font
点击查看免费下载

相关推荐

上一篇:radix-vue ContextMenuRadioGroup 详解:在右键菜单中实现互斥单选分组
下一篇:WorkshopDL免费下载器完整指南:742款游戏的Steam创意工坊模组一次拿全

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询