calibre 模板语言完全指南:从基础表达式到 Python 模板模式
导读
本文以 calibre 官方手册 manual/template_lang.rst 为骨架,系统讲解 calibre 模板语言(The calibre template language)的完整语法体系:从最基础的花括号字段替换,到条件前缀/后缀、格式化修饰,再到三种程序模式(Single Function Mode、Template Program Mode、General Program Mode)以及 Python Template Mode,并延伸到复合列、Plugboard、URL 构造与存储模板等实战场景。读完本文,你将能够熟练编写保存到磁盘/设备的文件命名模板、自定义复合列模板,以及复杂的条件逻辑程序,并通过 src/calibre/utils/formatter.py 等源码理解其底层求值机制。
一、模板语言是什么
模板语言是 calibre 特有的一种小型编程语言,贯穿 calibre 的诸多功能,典型用途包括:
- 指定把书籍从书库保存到磁盘或电子书阅读器时的文件夹结构与文件名;
- 为书库书列表定义图标与颜色规则;
- 定义由其他列数据计算而来的虚拟列(复合列);
- 高级书库搜索;
- 高级元数据查找与替换。
整个语言围绕一个核心概念——template(模板)展开:模板指明使用哪些图书元数据、对元数据做何种计算、以及如何格式化输出。
二、基础模板:花括号字段替换
一个基础模板由一个或多个 template expression(模板表达式)构成。模板表达式由普通文本和放在花括号 {} 中的字段名组成,求值时花括号内容会被替换为当前书籍对应的元数据。
例如 calibre 默认的“保存到设备”模板包含 4 个模板表达式:
{author_sort}/{title}/{title} - {authors}
对于 Isaac Asimov 的《The Foundation》,该模板会输出:
Asimov, Isaac/The Foundation/The Foundation - Isaac Asimov
斜杠不在花括号内,因此不是模板表达式,会原样保留在输出中。例如模板:
{author_sort} Some Important Text {title}/{title} - {authors}
输出为:
Asimov, Isaac Some Important Text The Foundation/The Foundation - Isaac Asimov
字段(column)的查找名
模板表达式可以访问 calibre 中的全部元数据,包括自定义列。引用方式是该列的 lookup name(查找名)。要查看某列的查找名,把鼠标悬停在书库书列表的列标题上即可。自定义列的查找名一律以 # 开头;对于系列(series)类型的列,还存在一个附加字段 #查找名_index,存放该书在该系列中的序号。例如自定义系列列 #myseries,对应有 #myseries_index;标准系列列的序号字段名为 series_index。
除了标准列字段,还可以使用:
{formats}——书籍在书库中可用的格式列表;{identifiers:select(isbn)}——书籍的 ISBN。
字段缺失时的处理
如果某本书没有定义某个字段的元数据,则该字段在模板中被替换为空字符串 ''。例如模板:
{author_sort}/{series}/{title} {series_index}
若 Asimov 的《Second Foundation》属于"Foundation"系列,输出:
Asimov, Isaac/Foundation/Second Foundation 3
若未填写系列,则输出:
Asimov, Isaac/Second Foundation
模板处理器会自动移除多个连续斜杠以及首尾空格。
三、高级格式化:条件文本与格式化指令
除了纯元数据替换,模板还可以按条件包含文本并控制输出格式。
条件包含文本:前缀/后缀语法
常见需求:只有当字段非空时才输出某些文本。典型场景是 series 与 series_index——要么什么都不输出,要么输出两个用连字符分隔的值。
沿用上面的 Foundation 例子,假设想让模板输出 Foundation - 3 - Second Foundation,直接写:
{series} - {series_index} - {title}
但若书籍没有系列,会输出 - - Second Foundation。解决办法是使用特殊语法:
{field:|prefix_text|suffix_text}
含义:若 field 的值为 XXXX,则结果为 prefix_textXXXXXsuffix_text;若字段为空,则前缀和后缀都被忽略,结果为空白。前缀和后缀中可以包含空格。
注意:不要在前缀或后缀中使用子模板(
{ ... })或函数(见下文)。
用该语法解决“无系列”问题:
{series}{series_index:| - | - }{title}
只有当书籍有系列序号(有系列才会有序号)时才输出连字符,结果形如 Foundation - 1 - Second Foundation。
使用规则:
- 使用前缀/后缀时,字段查找名后的冒号必须保留;
|必须成对出现(要么不用,要么两个都用),{field:| - }这种只用一个是不允许的;- 前缀或后缀可以为空,如
{series:|| - };{title:||}与{title}等价。
格式化指令
格式化指令同样放在冒号之后,语法继承自 Python 的格式字符串语法。常用示例:
| 模板 | 效果 |
|---|---|
{series_index:0>3s} |
三位数字,前导补零,如 003 |
{series_index:0<3s} |
三位数字,尾随补零 |
{series_index:0>5.2f} |
五字符宽度,两位整数带前导零、小数点、两位小数,如 01.00、02.50 |
{author_sort:.2} |
只取字段前两个字符 |
分数序号(如 1.1)场景下,可用 0>5.2f 把 1 与 2.5 显示成 01.00 与 02.50,使小数点在设备上按字典序排列时对齐。模板语言的格式化大量源自 Python,完整语法可参考 Python 官方 format string 文档(本文不引用外部链接,可查阅 pyproject.toml 确认项目基于 Python 3 的格式规范实现)。
四、用模板定义复合列(composite columns)
模板可用于显示 calibre 元数据中没有的信息,或以不同于默认格式的方式显示元数据。例如要显示 calibre 默认不展示的 ISBN 字段,可以新建一个类型为 Column built from other columns(由其他列构建的列,简称复合列)的自定义列,在模板框中输入:
{identifiers:select(isbn)}
该列将显示模板求值结果。又如显示两个自定义系列列的值并用逗号分隔:
{#series1:||,}{#series2}
复合列可以使用任何模板能力,包括格式化指令。
注意:复合列显示的数据不能直接编辑,只能编辑其源列。如果双击复合列进行编辑,calibre 打开的是模板编辑器而不是底层数据。
五、模板与 Plugboard
Plugboard 用于在“发送到设备”和“保存到磁盘”操作中改写写入书籍的元数据。通过 Plugboard 可以为以下字段指定模板以提供写入书籍的新值:authors、author_sort、language、publisher、tags、title、title_sort。这对希望设备上的书籍使用不同元数据以解决排序或显示问题的用户非常有用。
创建 Plugboard 时需要指定格式与设备。特殊的设备 save_to_disk 用于“保存格式到磁盘”(而非发送到设备)。选定格式与设备后,再选择要修改的元数据字段并为其提供模板。这些模板与其目标字段“连接”在一起,故名 plugboard。复合列当然也可以用在 Plugboard 模板中。
Plugboard 可以用 Single Function Mode、Template Program Mode、General Program Mode 或 Python Template Mode 编写,非常灵活。
Plugboard 的匹配搜索顺序
当 Plugboard 可能被触发时(内容服务器、保存到磁盘或发送到设备),calibre 会按给定格式与设备搜索已定义的 Plugboard。以 EPUB 书籍发送到 ANDROID 设备为例,搜索顺序是:
- 格式与设备完全匹配的 Plugboard,如
EPUB+ANDROID; - 格式完全匹配、设备为特殊值 any device(任意设备),如
EPUB+any device; - 格式为特殊值 any format(任意格式)、设备完全匹配,如
any format+ANDROID; any format+any device。
tags 与 authors 的特殊处理
tags 和 authors 字段可容纳多个值(一本书有很多标签和很多作者),因此有特殊处理:当模板结果为多值时,calibre 会将其拆分。
- 对 tags:凡是遇到逗号就切开。例如模板输出
Thriller, Horror,结果会变成两个标签Thriller和Horror;标签中间无法包含逗号。 - 对 authors:用
&(与号)而不是逗号切开。例如模板输出Blogs, Joe&Posts, Susan,会得到两个作者Blogs, Joe和Posts, Susan;若输出Blogs, Joe;Posts, Susan,则会得到一个名字奇怪的作者。
需要强调的是:Plugboard 影响的是写入书籍文件的元数据,并不影响“保存到磁盘/发送到设备”用于生成文件名的元数据;文件名由相应偏好设置窗口中的模板构造。文件保存/发送模板的核心配置可参考 src/calibre/library/save_to_disk.py 中的 config(),其中 template 与 send_template 分别控制保存到磁盘和发送到设备的文件名与文件夹结构。
六、在模板中使用函数:Single Function Mode(单函数模式)
假设某字段通常以标题大小写显示,而你希望显示为大写,可以使用模板函数。例如:
{title:uppercase()}——大写;{title:titlecase()}——标题大小写。
函数放在模板的 format 部分:位于 : 之后、第一个 | 之前(若没有前缀/后缀则为右花括号之前)。若同时使用格式与函数,函数位于第二个 : 之后。函数返回模板中指定列的值并做相应修改。
函数使用的语法有四种:
{lookup_name:function(arguments)}
{lookup_name:format:function(arguments)}
{lookup_name:function(arguments)|prefix|suffix}
{lookup_name:format:function(arguments)|prefix|suffix}
规则要点:
- 函数名后必须紧跟一对括号;需要额外参数时参数放在括号内,参数之间用逗号分隔;
- 作为文本的字面逗号(而非参数分隔符)必须以反斜杠转义:
\; - 最后一个(或唯一一个)参数中不能包含文本形式的右括号;
- 函数在格式指令与前缀/后缀之前求值。
重要提示:如果你有编程经验,请注意 Single Function Mode 的语法并不符合直觉——字符串不加引号,空格是有意义的;所有参数都被视为常量,不存在表达式。
Single Function Mode 调用函数时,第一个参数 value 会自动被模板中指定字段的内容替换。例如处理 {title:capitalize()} 时,title 字段的内容会自动作为参数 value 传入 capitalize 函数。在函数文档中,[something]* 表示 something 可重复零次或多次,[something]+ 表示至少重复一次。模板语言中的正则表达式匹配不区分大小写。
以 ifempty 函数为例:该函数需要两个参数 value 和 text_if_empty,但在 Single Function Mode 中省略 value,只传 text_if_empty:
{tags:ifempty(No tags on this book)}
书籍有标签则显示标签,无标签则显示 No tags on this book。
以下函数因其第一个参数是 value,可在 Single Function Mode 中使用:capitalize、ceiling、cmp、contains、date_arithmetic、encode_for_url、floor、format_date、format_duration、format_number、fractional_part、human_readable、ifempty、language_strings、list_contains、list_count、list_count_matching、list_item、list_sort、lookup、lowercase、mod、rating_to_stars、re、re_group、round、select、shorten、str_in_list、subitems、sublist、substr、swap_around_articles、swap_around_comma、switch、test、titlecase、transliterate、uppercase。
同一模板中同时使用函数与格式
假设有整数自定义列 #myint,希望显示为带前导零的 003。一种方式是使用格式 0>3s。但默认情况下,数字(整数或浮点数)为 0 时会被显示为空字符串,因此 0 值会产生空串而不是 000。若希望显示 000,需同时使用格式串与 ifempty 函数把空值改回 0:
{#myint:0>3s:ifempty(0)}
还可以叠加前缀/后缀,让数字显示为 [003] 或 [000]:
{#myint:0>3s:ifempty(0)|[|]}
七、General Program Mode(GPM,通用程序模式)
General Program Mode 用一个用模板语言编写的“程序”取代模板表达式。该语言的语法由如下文法定义:
program ::= 'program:' expression_list
expression_list ::= top_expression [ ';' top_expression ]*
top_expression ::= or_expression
or_expression ::= and_expression [ '||' and_expression ]*
and_expression ::= not_expression [ '&&' not_expression ]*
not_expression ::= [ '!' not_expression ]* | concatenate_expr
concatenate_expr::= compare_expr [ '&' compare_expr ]*
compare_expr ::= add_sub_expr [ compare_op add_sub_expr ]
compare_op ::= '==' | '!=' | '>=' | '>' | '<=' | '<' |
'in' | 'inlist' | 'inlist_field' |
'==#' | '!=#' | '>=#' | '>#' | '<=#' | '<#'
add_sub_expr ::= times_div_expr [ add_sub_op times_div_expr ]*
add_sub_op ::= '+' | '-'
times_div_expr ::= unary_op_expr [ times_div_op unary_op_expr ]*
times_div_op ::= '*' | '/'
unary_op_expr ::= [ add_sub_op unary_op_expr ]* | expression
expression ::= identifier | constant | function | assignment | field_reference |
if_expr | for_expr | break_expr | continue_expr | return_stmt
'(' expression_list ')' | function_def
field_reference ::= '$' [ '$' ] [ '#' ] identifier
identifier ::= id_start [ id_rest ]*
id_start ::= letter | underscore
id_rest ::= id_start | digit
constant ::= " string " | ' string ' | number
function ::= identifier '(' expression_list [ ',' expression_list ]* ')'
function_def ::= 'def' identifier '(' top_expression [ ',' top_expression ]* ')' ':'
expression_list 'fed'
assignment ::= identifier '=' top_expression
if_expr ::= 'if' condition 'then' expression_list
[ elif_expr ] [ 'else' expression_list ] 'fi'
condition ::= top_expression
elif_expr ::= 'elif' condition 'then' expression_list elif_expr | ''
for_expr ::= for_list | for_range
for_list ::= 'for' identifier 'in' list_expr
[ 'separator' separator_expr ] ':' expression_list 'rof'
for_range ::= 'for' identifier 'in' range_expr ':' expression_list 'rof'
range_expr ::= 'range' '(' [ start_expr ',' ] stop_expr
[ ',' step_expr [ ',' limit_expr ] ] ')'
with_expr ::= 'with' top_expression ':' expression_list 'htiw'
list_expr ::= top_expression
break_expr ::= 'break'
continue_expr ::= 'continue'
return_stmt ::= 'return' top_expression
separator_expr ::= top_expression
start_expr ::= top_expression
stop_expr ::= top_expression
step_expr ::= top_expression
limit_expr ::= top_expression
基本语义:
top_expression总是有值;expression_list的值是列表中最后一个top_expression的值。例如表达式列表1;2;'foobar';3的值是3;- 在逻辑上下文中,任何非空值都为
True,空值为False; - 字符串和数字可互换使用,
10与'10'是同一回事; - 注释是以
#开头的行(前面可有空格或制表符)。
运算符优先级
运算符按从高到低的优先级求值(最先求值优先):
- 函数调用、常量、括号表达式、语句表达式、赋值表达式、字段引用;
- 一元正号
+与负号-(从右往左求值)。一元运算符与其余算术运算符在结果小数部分为零时返回整数,例如3.0会变成3; - 乘
*与除/(可结合,从左往右求值;可用括号改变顺序); - 加
+与减-(可结合,从左往右求值); - 数值与字符串比较。比较成功返回
'1',否则返回空串''。比较运算符不可结合,a < b < c是语法错误; - 字符串连接
&。'aaa' & 'bbb'返回'aaabbb';可结合,从左往右求值; - 一元逻辑非
!。表达式为假(空串)时返回'1',否则返回''; - 逻辑与
&&。左右都为真返回'1',任一为假返回'';可结合,从左往右求值,并短路; - 逻辑或
||。任一为真返回'1',两者为假返回'';可结合,从左往右求值,短路;它是包含或,两边都为真时也返回'1'。
字段引用
field_reference 对 $ 或 $$ 后按查找名命名的元数据字段求值。$ 等价于 field() 函数,$$ 等价于 raw_field() 函数。示例:
$authors==>field('authors')$#genre==>field('#genre')$$pubdate==>raw_field('pubdate')$$#my_int==>raw_field('#my_int')
If 表达式
if 表达式先求值 condition;若为真(非空值),则求值 then 子句中的 expression_list;为假则求值 elif 或 else 子句(若存在)。elif 与 else 可选。关键字 if、then、elif、else、fi 是保留字,不能用作标识符。可在任意合理位置放置换行和空白。condition 是 top_expression 而非 expression_list,不允许分号。expression_list 是以分号分隔的 top_expression 序列。if 表达式返回被求值的 expression_list 中最后一个 top_expression 的结果,若没有求值任何表达式列表则返回空串。
示例:
program: if field('series') then 'yes' else 'no' fi
program:
if field('series') then
a = 'yes';
b = 'no'
else
a = 'no';
b = 'yes'
fi;
strcat(a, '-', b)
嵌套 if 示例:
program:
if field('series') then
if check_yes_no(field('#mybool'), '', '', '1') then
'yes'
else
'no'
fi
else
'no series'
fi
如上述,if 会产出值,因此以下写法完全等价:
program: if field('series') then 'foo' else 'bar' fi
program: if field('series') then a = 'foo' else a = 'bar' fi; a
program: a = if field('series') then 'foo' else 'bar' fi; a
下面的程序在书籍有系列时返回 series 列的值,否则返回 title 列的值:
program: field(if field('series') then 'series' else 'title' fi)
For 表达式
for 表达式遍历值列表,逐个处理。list_expression 必须求值为元数据字段查找名(如 tags、#genre)或值列表;range() 可生成数字列表。若结果是有效查找名,则取字段值并使用该字段类型指定的分隔符;若不是有效查找名则视为值列表,默认按逗号分隔,除非提供可选关键字 separator,此时列表值由 separator_expr 求值结果分隔。由 range() 生成的列表不能使用 separator。每个值赋给指定变量后求值 expression_list。可使用 break 跳出循环、continue 跳到下一轮迭代。
示例:移除 #genre 中每个值的第一个层级名,构建新名称列表:
program:
new_tags = '';
for i in '#genre':
j = re(i, '^.*?\.(.*)$', '\1');
new_tags = list_union(new_tags, j, ',')
rof;
new_tags
若原 Genre 为 History.Military, Science Fiction.Alternate History, ReadMe,模板返回 Military, Alternate History, ReadMe。可用在 Edit metadata in bulk -> Search & replace(批量编辑元数据 -> 查找与替换)中,把 Search for 设为 template,剥离层级的第一级并将结果赋给 Genre。注意模板最后一行 new_tags 在此处并非严格必需,因为 for 返回表达式列表中最后一个 top_expression 的值,而赋值语句的值是其表达式的值,因此 for 语句的值就是赋给 new_tags 的值。
With 表达式
with 表达式:
- 把当前书籍切换为由
top_expression求值产生的 calibre 书籍 id(整数)所对应的书; - 执行
expression_list; - 然后把当前书籍重置回原来的书。
with 返回被求值的 expression_list 中最后一个 top_expression 的结果,若无求值则返回空串。
示例:返回 GUI 中选中的每本书的书名列表:
program:
res = '';
ids = selected_books();
for id in ids:
with id:
res = (if res then res & ', ' fi) & $title
htiw
rof;
res
Return 语句
返回 expression 的值。若在函数中执行,则把值返回给调用者;若在最外层上下文(模板本身)执行,则把模板的值设为该表达式的值并退出模板。
函数定义(def / fed)
模板中重复出现的代码可以封装为局部函数。def 关键字开始定义,后跟函数名、参数列表、函数体,以 fed 关键字结束。
参数是位置参数:调用函数时实参从左到右与形参匹配。实参数多于形参数是错误。形参可以有默认值,如 a = 25;若调用时未提供该参数则使用默认值,否则该参数被设为空串。局部函数中可使用 return。函数必须先定义后使用。
示例:根据天数计算近似年、月、日。to_plural() 函数负责格式化计算结果(注意示例也使用了 & 运算符):
program:
days = 2112;
years = floor(days/360);
months = floor(mod(days, 360)/30);
days = days - ((years*360) + (months * 30));
def to_plural(v, str):
if v == 0 then return '' fi;
return v & ' ' & (if v == 1 then str else str & 's' fi) & ' '
fed;
to_plural(years, 'year') & to_plural(months, 'month') & to_plural(days,'day')
关系运算符
关系运算符比较成功返回 '1',否则返回空串 ''。分为字符串比较与数值比较两类。
字符串比较使用字典序做不区分大小写的字符串比较。支持的运算符有 ==、!=、<、<=、>、>=、in、inlist、inlist_field。对 in、inlist、inlist_field 而言,左侧表达式的结果被解释为正则表达式模式,左侧正则与右侧值匹配即为真;正则匹配不区分大小写。
inlist:左侧正则匹配右侧逗号分隔列表中的任意一项时为真;inlist_field:左侧正则匹配由右侧表达式命名的字段(列)中的任意一项时为真,使用该字段定义的分隔符。注意:inlist_field要求右侧表达式求值为字段名,而inlist要求右侧表达式求值为含逗号分隔列表的字符串。因此inlist_field明显快于inlist,因为它不需要字符串转换和列表构建。
数值比较运算符为 ==#、!=#、<#、<=#、>#、>=#。左右表达式必须求值为数值,有两个例外:字符串 "None"(未定义字段)和空串都按零求值。
示例:
program: field('series') == 'foo'——系列为foo时返回'1',否则返回'';program: 'f.o' in field('series')——系列匹配正则f.o(如foo、Off Onyx)时返回'1';program: 'science' inlist $#genre——genre 中任一值匹配正则science(如Science、History of Science、Science Fiction)时返回'1';program: '^science$' inlist $#genre——genre 中任一值精确匹配^science$时返回'1',History of Science与Science Fiction不匹配;program: 'asimov' inlist $authors——任一作者匹配asimov(如Asimov, Isaac或Isaac Asimov)时返回'1';program: 'asimov' inlist_field 'authors'——同上,但直接对authors字段操作;program: 'asimov$' inlist_field 'authors'——任一作者匹配asimov$(如Isaac Asimov)时返回'1';因正则锚点$,不匹配Asimov, Isaac;program: if field('series') != 'foo' then 'bar' else 'mumble' fi——系列非foo返回'bar',否则返回'mumble';program: if field('series') == 'foo' || field('series') == '1632' then 'yes' else 'no' fi——系列为foo或1632时返回'yes';program: if '^(foo|1632)$' in field('series') then 'yes' else 'no' fi——同上,用正则替代;program: if 11 > 2 then 'yes' else 'no' fi——返回'no',因为>做字典序比较;program: if 11 ># 2 then 'yes' else 'no' fi——返回'yes',因为>#做数值比较。
GPM 中的函数
GPM 中调用函数与 Single Function Mode 相反:必须显式给出第一个参数 value;所有参数都是表达式列表(见上面文法)。
八、Template Program Mode(TPM,模板程序模式)
Template Program Mode 是 General Program Mode 与 Single Function Mode 的混合体。与 Single Function Mode 相比,TPM 允许编写引用其他元数据字段、使用嵌套函数、修改变量、进行算术运算的模板表达式;与 General Program Mode 相比,TPM 模板包含在 { 与 } 之间,且不以 program: 开头。模板中的程序部分就是一个 GPM 表达式列表。
示例:希望有系列时显示系列,否则显示自定义字段 #genre。这在 Single Function Mode 中做不到(无法在模板表达式内引用另一元数据字段),TPM 则可以:
{series:'ifempty($, $#genre)'}
该示例说明:
- 表达式以
:'开头并以'}结束即使用 TPM;其他情况按 Single Function Mode 处理; - 若模板含前缀与后缀,表达式以
'|结束(|为前缀分隔符)。示例:{series:'ifempty($, $#genre)'|prefix | suffix}; - 函数必须给出全部参数,标准内置函数必须给出初始参数
value; - 变量
$可用作value参数,代表模板中字段(此处为series)的值; - 表达式内可忽略空白并使用任意空白;
- 常量字符串用匹配的引号括起,
'或"均可。
警告:在 TPM 的字符串字面量中使用
{与}会导致错误或意外结果,因为模板处理器会把它们当作模板表达式边界。某些(并非全部)情况下可把{替换为[[、}替换为]]。建议:若程序包含{与},请使用 General Program Mode。
九、Python Template Mode(PTM,Python 模板模式)
Python Template Mode 允许用原生 Python 与 calibre API 编写模板。其中数据库 API 最为常用。PTM 模板更快、能做更复杂的操作,但要求你会用 Python 写代码并了解 calibre API。
PTM 模板以如下形式开头:
python:
def evaluate(book, context):
# book is a calibre metadata object
# context is an instance of calibre.utils.formatter.PythonTemplateContext,
# which currently contains the following attributes:
# db: a calibre legacy database object.
# globals: the template global variable dictionary.
# arguments: is a list of arguments if the template is called by a GPM template, otherwise None.
# funcs: used to call Built-in/User functions and Stored GPM/Python templates.
# Example: context.funcs.list_re_group()
# your Python code goes here
return 'a string'
可右键(通常为右键菜单)把上述文本添加到模板中。注释不参与执行,可以删除;必须使用 Python 缩进。
context 对象支持 str(context)(返回 context 内容的字符串)与 context.attributes(返回 context 中属性名的列表)。这与 src/calibre/utils/formatter.py 中 PythonTemplateContext 类的实现一致:它内置 db、arguments、globals、funcs 等属性,attributes 属性返回已设置的属性名排序列表,__str__ 返回按属性名排序、以换行分隔的 属性:值 文本。
context.funcs 属性用于直接调用内置函数、用户模板函数以及已存储的 GPM/Python 模板。函数按名字检索;若名字与 Python 关键字冲突,在末尾加下划线。示例:
context.funcs.list_re_group()
context.funcs.assert_()
其底层实现是 src/calibre/utils/formatter.py 的 FormatterFuncsCaller 类:通过属性名访问 formatter.funcs 中的函数,名字以 _ 结尾时优先匹配去掉下划线的函数名(用于规避 Python 关键字),并对 None 参数做空串转换。
PTM 实战示例:列出系列的全部作者
下面是一个 PTM 模板,生成某系列所有作者的列表。它适合存放在“Column built from other columns, behaves like tags”(表现为标签的复合列)类型列中,在 Book details 中显示并勾选 on separate lines(在 Preferences->Look & feel->Book details 中)。该选项要求列表以逗号分隔,因此模板先把作者名中的逗号转换为分号,再构建逗号分隔的作者列表;作者经过排序,因此使用 author_sort:
python:
def evaluate(book, context):
if book.series is None:
return ''
db = context.db.new_api
ans = set()
# Get the list of books in the series
ids = db.search(f'series:"={book.series}"', '')
if ids:
# Get all the author_sort values for the books in the series
author_sorts = (v for v in db.all_field_for('author_sort', ids).values())
# Add the names to the result set, removing duplicates
for aus in author_sorts:
ans.update(v.strip() for v in aus.split('&'))
# Make a sorted comma-separated string from the result set
return ', '.join(v.replace(',', ';') for v in sorted(ans))
十、模板与 URL
模板可用于构造 URL,这里介绍两种场景:复合列 Book details 搜索 URL,以及 calibre URL scheme。
复合列 Book details 搜索 URL
创建自定义列时可以提供一个用模板生成的 URL,用于 Book details(书籍详情)。例如有 Translators 自定义列,可定义链接到译者站点的 URL。Book details 搜索 URL 可提供于 Text、Enumerated、Series 与 Column built from other column 类型的列。
点击带搜索模板的条目时,模板被求值。除了常规书籍元数据,还提供三个附加字段:
item_value:被点击条目的值;item_value_quoted:被点击条目的值,URL 编码,特殊字符转义为合法 URL 字符,空格替换为'+';item_value_no_plus:被点击条目的值,URL 编码,空格替换为%20而非加号。
构造 URL 有多种方式,以下以 Wikipedia 为例。最简单的是基础模板:
https://en.wikipedia.org/w/index.php?search={item_value_encoded}
需要更复杂处理时,可使用四个模板函数:make_url、make_url_extended、query_string、encode_for_url。
例如 Translators(#translators)列的名字格式为 Last name, First name,构造 URL 时需要转为 First name Last name,可用 make_url:
program: make_url('https://en.wikipedia.org/w/index.php', 'search', swap_around_comma($item_value))
若译者名为 Boy-Żeleński, Tadeusz,模板产生链接:
https://en.wikipedia.org/w/index.php?search=Tadeusz+Boy-%C5%BBele%C5%84ski
注意名字已调换顺序、空格变成了加号、姓氏中的非英文字符被 URL 编码。根据处理复杂度,make_url_extended、query_string、encode_for_url 也可能有用。
calibre URL scheme
calibre 支持多种 URL 用于在书库间导航。以下展示如何用模板构造部分 URL;可用 URL 的完整说明见 manual/url_scheme.rst。
切换到指定书库,语法:
calibre://switch-library/Library_Name
Library_Name 需替换为要打开的书库名称(窗口标题栏显示的名称,是简单名称而非文件路径,需按标题栏显示的原样书写、区分大小写)。下划线 _ 代表当前书库。名称含空格或特殊字符时必须用 to_hex 函数做十六进制编码:
program: strcat('calibre://switch-library/_hex_-', to_hex(current_library_name()))
生成 URL:
calibre://switch-library/_hex_-4c6962726172792e746573745f736d616c6c
也可把 current_library_name() 换成实际书库名:
program: strcat('calibre://switch-library/_hex_-', to_hex('Library.test_small'))
显示书籍的链接,在书库中选择一本书,语法:
calibre://show-book/Library_Name/book_id
book id 是书籍的数字 calibre id,模板中可用 $id 取得。书库名可能需要十六进制编码:
program: strcat('calibre://show-book/_hex_-', to_hex(current_library_name()), '/', $id)
生成:
calibre://show-book/_hex_-4c6962726172792e746573745f736d616c6c/1353
搜索书籍的链接,语法:
calibre://search/Library_Name?q=query
calibre://search/Library_Name?eq=hex_encoded_query
其中 query 是任意合法 calibre 搜索表达式。含空格或特殊字符的查询必须十六进制编码(通常都要)。例如搜索以 'AA' 开头的层级标签的表达式是 tags:"=.AA",构造该搜索 URL 的模板:
program: strcat('calibre://search/_hex_-', to_hex(current_library_name()), '?eq=', to_hex('tags:"=.AA"'))
生成:
calibre://search/_hex_-4c6962726172792e746573745f736d616c6c?eq=746167733a223d2e414122
同一 URL 用 make_url_extended 构造:
program: make_url_extended('calibre', '', 'search/_hex_-' & to_hex(current_library_name()),
'eq', to_hex('tags:"=.AA"'))
打开书籍详情窗口,语法:
calibre://book-details/Library_Name/book_id
示例模板:
program: strcat('calibre://book-details/_hex_-', to_hex(current_library_name()), '/', $id)
生成:
calibre://book-details/_hex_-4c6962726172792e746573745f736d616c6c/1353
打开作者/系列等关联的笔记,语法:
calibre://book-details/Library_Name/Field_Name/id_Item_Id
calibre://book-details/Library_Name/Field_Name/hex_Hex_Encoded_Item_Name
Field_Name 是字段的查找名;自定义列需把 # 替换为下划线 _。Item_Id 是字段值内部的数字 ID;没有返回 Item_Id 的模板函数,因此模板通常使用第二种形式 Hex_Encoded_Item_Name。打开字段 #authtest 中人物 Boy-Żeleński, Tadeusz 笔记的示例模板:
program: strcat('calibre://show-note/_hex_-', to_hex(current_library_name()),
'/_authtest/hex_', to_hex('Boy-Żeleński, Tadeusz'))
生成:
calibre://show-note/_hex_-4c6962726172792e746573745f736d616c6c/_authtest/hex_426f792dc5bb656c65c584736b692c205461646575737a
十一、存储模板(Stored templates)
General Program Mode 与 Python Template Mode 都支持保存模板并从另一个模板中调用它,就像调用存储的函数一样。通过 Preferences->Advanced->Template functions 保存模板(该对话框中有更多说明)。调用模板的方式与调用函数相同,可按需传递位置参数,参数可以是任意表达式。假设存储模板名为 foo:
foo()——不传参数调用;foo(a, b)——传入变量a与b的值;foo(if field('series') then field('series_index') else 0 fi)——有系列则传series_index,否则传0。
在 GPM 中,用 arguments 函数取回调用时传入的参数。它同时声明并初始化局部变量(相当于形参)。变量是位置参数,按调用时的位置取值;若调用未提供对应参数,arguments 给该变量赋默认值;无默认值时变量设为空串。例如下面的 arguments 声明了两个变量 key、alternate:
arguments(key, alternate='series')
示例(仍假设存储模板名为 foo):
foo('#myseries')——key赋值为'myseries',alternate使用默认值'series';foo('series', '#genre')——key赋值为'series',alternate赋值为'#genre';foo()——key赋值为空串,alternate使用默认值'series'。
在 PTM 中,参数通过 arguments 参数传入,它是一个字符串列表。无法指定默认值,必须自行检查 arguments 列表长度以确保参数数量符合预期。
测试存储模板的便捷方式是 Template tester(模板测试器)对话框。为方便访问,可在 Preferences->Advanced->Keyboard shortcuts->Template tester 中给它设置键盘快捷键;给 Stored templates 对话框设置快捷键则有助于在测试器与存储模板源码编辑之间快速切换。
十二、向模板提供附加信息(globals)
开发者可以向模板处理器传递附加信息,例如应用特有的书籍元数据或当前处理任务的说明。模板在求值期间可以访问这些信息。
开发者视角:如何传递附加信息
附加信息是一个 Python 字典,包含 variable_name: variable_value 键值对,其中值必须是字符串。模板可以访问该字典,创建名为 variable_name、值为 variable_value 的模板局部变量。用户不能更改名字,因此最好使用不易与其他模板局部变量冲突的名字,例如以下划线开头。
该字典通过命名参数 global_vars=your_dict 传给模板处理器(formatter)。完整方法签名(见 src/calibre/utils/formatter.py 中的 safe_format):
def safe_format(self, fmt, kwargs, error_value, book,
column_name=None, template_cache=None,
strip_results=True, template_functions=None,
global_vars={})
从源码看,safe_format 会保存/恢复处理器状态,设置 strip_results、column_name、template_cache、kwargs、book、global_vars 等实例属性,初始化 PythonTemplateContext 与函数表(可用 template_functions 覆盖默认的内置函数表),最终调用 evaluate() 求值;异常被统一捕获并返回 error_value 加错误消息,保证“绝不抛异常”。
模板编写者视角:如何访问附加信息
在模板中用 globals 函数访问附加信息(globals 字典):
globals(id[=expression] [, id[=expression]]*)
其中 id 是任意合法变量名。该函数检查开发者提供的附加信息中是否包含该名字:包含则把对应值赋给同名模板局部变量;不包含但提供了 expression,则求值表达式并把结果赋给局部变量;既不提供值也不提供表达式,则把空串 '' 赋给局部变量。
模板也可以用 set_globals 函数在 globals 字典中设置值:
set_globals(id[=expression] [, id[=expression]]*)
该函数设置 globals 字典键值对 id:value,其中 value 是模板局部变量 id 的值;若该局部变量不存在,则 value 为 expression 求值的结果。
十三、三种模式的区别速查
三种程序模式——Single Function Mode(SFM)、Template Program Mode(TPM)、General Program Mode(GPM)——工作方式不同。SFM 定位为“简单”,因此隐藏了大量编程语言细节。主要差异:
-
SFM 中列的值总是作为“隐形”的第一参数传给模板中的函数;
-
SFM 不区分变量与字符串,所有值都是字符串;
-
下面的 SFM 模板返回系列名或字符串 "no series":
{series:ifempty(no series)}等价 TPM 模板:
{series:'ifempty($, 'no series')'}等价 GPM 模板:
program: ifempty(field('series'), 'no series')ifempty的第一个参数是series字段的值,第二个参数是字符串no series;SFM 中第一个参数(字段值)被自动传入(隐形参数); -
若干模板函数(如
booksize()、current_library_name())不接受参数。由于“隐形参数”的存在,这些函数不能在 SFM 中使用; -
SFM 不支持嵌套函数(一个函数调用另一个函数来计算参数)。例如下面这个本想返回 series 值前 5 个大写字符的模板在 SFM 中不工作:
{series:uppercase(substr(0,5))} -
TPM 与 GPM 支持嵌套函数。上述模板在 TPM 中为:
{series:'uppercase(substr($, 0,5))'}在 GPM 中为:
program: uppercase(substr(field('series'), 0,5)) -
如前所述,TPM 字符串字面量中的
{与}会造成解析混乱;某些情况可把{换成[[、}换成]];一般建议:程序含{与}时使用 General Program Mode。
十四、用户自定义 Python 模板函数
可以给模板处理器添加自己的 Python 函数,这些函数可用于三种模板编程模式中的任意一种。添加方式:Preferences -> Advanced -> Template functions,对话框中有说明。也可用 Python Templates 达到类似目的。由于调用用户自定义函数比调用 Python 模板更快,在复杂度相近时,用户自定义函数可能更高效。
十五、不同使用场景下的特殊说明
GUI 中(Columns made from other columns 与 Template searches):
- GPM 模板照常工作;
- Python 模板拥有对 calibre 数据库的完整访问权限。
图标规则(icon rules)中:
- 图标规则模板没有书籍数据,因此基于字段的函数(如
format_date_field、list_count_field、check_yes_no)无法工作。
内容服务器(Content server)中:
- 模板可访问新 API,但不能访问旧 API(LibraryDatabase);
- 因此以下 formatter 函数在 GPM 模板(复合列、图标规则等)中不保证可用,若使用内容服务器应避免:
connected_device_name、connected_device_uuid、current_virtual_library_name、is_marked、virtual_libraries。
十六、保存/发送模板的特殊说明
当模板用于 Save to disk(保存到磁盘)或 Send to device(发送到设备)时,会应用特殊处理:字段值会被清洗,把对文件系统特殊的字符(包括斜杠)替换为下划线。这意味着字段文本不能用于创建文件夹。但前缀/后缀字符串中的斜杠不会被修改,因此这些字符串中的斜杠会导致创建文件夹。正因如此,你可以构造可变深度的文件夹结构。
例如希望得到 series/series_index - title 的文件夹结构,且无系列时标题放在顶层。模板:
{series:||/}{series_index:|| - }{title}
斜杠与连字符仅在系列非空时出现。
lookup 函数允许更花哨的处理。例如:有系列时用 series/series index - title.fmt 结构;无系列时用 genre/author_sort/title.fmt 结构;无 genre 时用 Unknown。要实现两条完全不同的路径,取决于 series 的值:
- 创建复合字段(查找名
#aa),内容为{series}/{series_index} - {title}。系列非空时产生series/series_index - title; - 创建复合字段(查找名
#bb),内容为{#genre:ifempty(Unknown)}/{author_sort}/{title}。产生genre/author_sort/title,空 genre 替换为Unknown; - 把保存模板设为
{series:lookup(.,#aa,#bb)}。该模板在系列非空时选择复合字段#aa,系列为空时选择#bb,从而实现两条不同的保存路径。
对应地,src/calibre/library/save_to_disk.py 的 config() 中维护了 template(保存到磁盘的文件名/文件夹结构模板)与 send_template(发送到设备的模板)配置项,并有 preprocess_template() 把 // 折叠为 /、把 {author} 归一化为 {authors},可见底层对模板清洗与预处理的支持。
十七、实用技巧
- 使用 Template Tester 测试模板;把它加入书库中书籍的上下文菜单并/或设置键盘快捷键;
- 模板可以引用其他模板:引用用目标模板构建的复合列,或使用存储模板(Stored Templates);
- 在 Plugboard 中,用特殊模板
{}把字段设为空(或等价于空的值)。该模板永远求值为空串; - 上文“显示零值数字”的技巧同样适用于标准字段
series_index。
十八、模板函数参考
模板语言内置函数的完整列表、参数与行为说明见模板语言文档的“Template function reference”(Template 函数参考)章节(由原文档 toctree 引入的 generated/en/template_ref 目录生成),其中记录了每个函数所需的参数及返回值语义,例如 ifempty、make_url、to_hex、format_date 等均在其中。函数实现方面,可在 src/calibre/utils/formatter_functions.py 中查看内置函数的具体 Python 实现,理解其参数约定与返回类型,从而在 Single Function Mode / TPM / GPM / PTM 中正确使用。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.22 K638- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python400
cherry-studio🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端TypeScript2 K146
hello-agents📚 《从零开始构建智能体》——从零开始的智能体原理与实践教程Python48467
new-apiAI模型聚合管理中转分发系统,一个应用管理您的所有AI模型,支持将多种大模型转为统一格式调用,支持OpenAI、Claude、Gemini等格式,可供个人或者企业内部管理与分发渠道使用。🍥 A Unified AI Model Management & Distribution System. Aggregate all your LLMs into one app and access them via an OpenAI-compatible API, with native support for Claude (Messages) and Gemini formats.Go20843
JeecgBoot🔥企业级低代码平台集成了AI应用平台,帮助企业快速实现低代码开发和构建AI应用!前后端分离架构 SpringBoot,SpringCloud、Mybatis,Ant Design4、 Vue3.0、TS+vite!强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE,显著的提高效率,又不失灵活~Java34451