![组合字符的示例](https://linux22.com/image/451026/%E7%BB%84%E5%90%88%E5%AD%97%E7%AC%A6%E7%9A%84%E7%A4%BA%E4%BE%8B.png)
当我在包含一个或多个“外来”字符的字符串上使用宏时\StrLen
,返回的值与字符串中实际的字符数不对应。
我怀疑这是因为其中一些字符使用超过 1 个字节进行编码。话虽如此,是否有技巧可以获取字符串的“实际”长度?
\StrLen{aàáâãäåāăąæ} % returns 57 for 11 characters
答案1
在 UTF-8 中,非 ASCII 字符占用几个字节,并且xstring
可能\StrLen
正在计算这些字节...然而,它似乎完全不同,正如@SandyG著名的。
以下示例显示如何计算 Unicode代码点在给定字符串中使用expl3
函数,以及如何迭代它们。它应该适用于任何引擎(足够新的引擎expl3
)。
\documentclass{article}
% Font setup: only needed for typesetting the sample string
\usepackage{iftex}
\ifpdftex
\usepackage{lmodern}
\usepackage[T1]{fontenc}
\fi
\ExplSyntaxOn
\NewDocumentCommand \setToNbCodePoints { O{default} m m }
{
\str_set_convert:Nnnn \l_tmpa_clist {#3} {#1} { clist }
\cs_set:Npx #2 { \clist_count:N \l_tmpa_clist }
}
\NewDocumentCommand \iterateOverCodePoints { O{default} m m }
{
\str_set_convert:Nnnn \l_tmpa_clist {#2} {#1} { clist }
\clist_map_inline:Nn \l_tmpa_clist {#3}
}
\NewDocumentCommand \printCodePoints { O{default} m }
{
\str_set_convert:Nnnn \l_tmpa_clist {#2} {#1} { clist }
\clist_use:Nnnn \l_tmpa_clist { ~and~ } { ,~ } { ,~and~ }
}
\ExplSyntaxOff
\begin{document}
\setToNbCodePoints{\result}{aàáâãäåāăąæ}%
There are \result~code points in “aàáâãäåāăąæ”. These are:
\iterateOverCodePoints{aàáâãäåāăąæ}{#1, } and that's it.
Put in another way: \printCodePoints{aàáâãäåāăąæ}.
\end{document}
注:约瑟夫·赖特暗示,在许多情况下,每个“字符”只有一个代码点,但这并不总是正确的。例如,一个é
可以用两个 Unicode 代码点(一个代表 ,一个代表重音符号)来书写e
:这就是所谓的组合字符(并导致某些软件出现问题)。
“性格”这个概念一般没有明确的定义:我建议阅读字形、字素和其他 Unicode 种类在无处不在的 UTF-8。
组合字符的示例
结合n
U+0308 组合分音符,得到n̈
(使用 LuaTeX 或 XeTeX 编译):
\setToNbCodePoints{\result}{n̈}%
Example with two combining characters: there are \result~code points in “n̈”:
\printCodePoints{n̈}.
你会看到的:
注意:在 pdfTeX 下运行的代码也可以计算这些代码点,但不能对其进行处理排版这个特殊的组合:
Unicode 字符 ̈ (U+0308) 未设置为用于 LaTeX。
甚至é
建造自e
和U+0301 结合尖锐重音,在编码上是可以表示的T1
,所以在排版的时候会造成这种错误。
根据代码点列表排版
反方向前进排版使用前面的内容从找到的代码点构建字形似乎更困难。以下代码已使用pdfTeX
、XeTeX
和进行了测试LuaTeX
。
困难在于\str_set_convert:Nnnn \whatever { ⟨comma-list of code points⟩ } { clist } { default }
中存储了 catcode 为 12 的非空格字符标记\whatever
,通常不能用 8 位引擎来排版非 ASCII 字符。对于 pdfTeX,我们需要非 ASCII 字符的活动字符(或者从代码点到 的合适映射\textcommandsomething
,但我手头没有)。
注意:这不适用于 ASCII 范围(0-127)内的字符,其中字体编码与 ASCII 不同(至少对于|
、<
、>
和"
的OT1
编码是这种情况;T1
在这方面更好)。
\documentclass{article}
\usepackage{xcolor}
% Font setup: only needed for typesetting the sample string
\usepackage{iftex}
\ifpdftex
\usepackage{lmodern}
\usepackage[T1]{fontenc}
\fi
\ExplSyntaxOn
\cs_new_protected:Npn \jlb_iterate_over_code_points:nnn #1#2#3
{
\str_set_convert:Nnnn \l_tmpa_clist {#2} {#1} { clist }
\clist_map_inline:Nn \l_tmpa_clist {#3}
}
\tl_new:N \l__jlb_prepare_for_typesetting_tl
\cs_generate_variant:Nn \tl_analysis_map_inline:nn { V }
% Store in tl var #1 whatever is needed to typeset the Unicode code point #2
% (given as an integer denotation—e.g., a decimal representation)
\cs_new_protected:Npn \jlb_prepare_for_typesetting:Nn #1#2
{
\str_set_convert:Nnnn \l__jlb_prepare_for_typesetting_tl {#2} { clist }
{ default }
\bool_lazy_and:nnTF { \int_compare_p:nNn {#2} > { 127 } }
{ \sys_if_engine_pdftex_p: }
{
\tl_clear:N #1
\tl_analysis_map_inline:Vn \l__jlb_prepare_for_typesetting_tl
{
% \char_generate:nn does its job in two expansion steps
\exp_args:NNNo \exp_args:NNo
\tl_put_right:Nn #1 { \char_generate:nn {##2} { 13 } }
}
}
{
% Char token (32, 12) isn't a space token → let's special-case spaces
\int_compare:nNnTF {#2} = { 32 }
{ \tl_set:Nn #1 { \scan_stop: \c_space_token } }
{ \tl_set_eq:NN #1 \l__jlb_prepare_for_typesetting_tl }
}
}
\tl_new:N \l__jlb_alternate_one_char_tl
\int_new:N \l__jlb_alternate_counter_int
\NewDocumentCommand \alternate { m m m }
{
\int_zero:N \l__jlb_alternate_counter_int
\jlb_iterate_over_code_points:nnn { default } {#3}
{
% Make \__jlb_alternate_mapping_func:n alternate between #1 and #2
\int_if_even:nTF { \l__jlb_alternate_counter_int }
{ \cs_set_eq:NN \__jlb_alternate_mapping_func:n #1 }
{ \cs_set_eq:NN \__jlb_alternate_mapping_func:n #2 }
% Store suitable tokens in \l__jlb_alternate_one_char_tl to allow
% typesetting the character whose Unicode code point is ##1.
\jlb_prepare_for_typesetting:Nn \l__jlb_alternate_one_char_tl {##1}
% Use the result with \__jlb_alternate_mapping_func:n
\__jlb_alternate_mapping_func:n { \l__jlb_alternate_one_char_tl }
\int_incr:N \l__jlb_alternate_counter_int
}
}
\ExplSyntaxOff
\newcommand*{\typesetInBlue}[1]{\textcolor{blue}{#1}}
\newcommand*{\typesetInRed}[1]{\textcolor{red}{#1}}
\begin{document}
\alternate{\typesetInBlue}{\typesetInRed}{aàáâãäåāăąæ}
\end{document}
答案2
当在低级别处理(如您所说)“异国情调”(但仍然是单字形和 utf8 编码,对吧?)字符时,最好不要使用 pdf(La)TeX;而是使用原生支持 utf8 的 TeX 引擎。我知道两个这样的引擎:XeTeX 和 LuaTeX。
如果您选择使用 LuaTeX,您甚至可以根据需要使用 Lua 强大的字符串函数库来扩展包提供的宏xstring
。
附言:这种方法(即使用 LualaTeX 或 XeLaTeX 下的宏包xstring
,包括\StrLen
)假定可见的“字符”被编码为“单个”字形。Unicode 和 UTF8 也有一些称为“组合”字符的东西,例如n̈
。当应用于“组合字符”时, 的输出\StrLen
将为 2 —— 甚至更多,如果是多重组合字符的话。
% !TEX TS-program = lualatex
\documentclass{article}
\usepackage{xstring} % for '\StrLen' macro
% create an alternative to '\StrLen':
\newcommand*\StrLenAlt[1]{\directlua{tex.sprint(unicode.utf8.len("#1"))}}
\begin{document}
---\StrLen{aàáâãäåāăąæ}---\StrLenAlt{aàáâãäåāăąæ}---
\end{document}
答案3
正如其他答案所写,Unicode 中的字符计数有些定义不明确。虽然其他答案展示了如何计算 Unicode 代码点,但这可能会导致相当意外的结果。例如,ä
可能是一个或两个代码点,具体取决于使用的是组合字符还是a
带有组合分音符的字符。虽然有时可以通过将所有文档规范化为 NFC 来解决这个问题,从而避免使用这种组合字符,但当使用的字符不存在预组合形式时,这种方法就会失败。
对于 pdfTeX 来说这并不重要,因为无论如何都不支持组合字符,但在 LuaTeX 中应该支持这一点。
计算 Unicode 字符的一种更方便用户的方法是计算 Unicode“字素簇”。这些是 Unicode 代码点簇,大致对应于通常被认为是字符的内容。我的lua-uni-algos
软件包包含 Lua 实用程序,用于查找某些文本中的字素簇边界,但不提供基于 TeX 的界面。
但它可以用来编写这样的接口(显然需要 LuaTeX):
\documentclass{article}
\usepackage{iftex}
\RequireLuaTeX
\directlua{
% You can ignore this block, it's just a technical helper to allow Lua based "macro expansion"
local match = 0x1A00000
local end_match = 0x1C00000
local arg1_tok = token.new(1, token.command_id'car_ret')
local function insert_arg(toks, arg1)
if not toks[1] == match or not toks[2] == end_match then
error"Misuse"
end
local result = {}
local j = 1
local arg_length = \csstring\#arg1
for i = 3, \csstring\#toks do
local t = toks[i]
if t == arg1_tok then
table.move(arg1, 1, arg_length, j, result)
j = j + arg_length
else
result[j] = t
j = j + 1
end
end
return result
end
% Load the grapheme handler from lua-uni-algos
local read_codepoint = require'lua-uni-graphemes'.read_codepoint
% Which category codes should be allowed? We treat everything except
% spaces, letters and "other" chars as an error.
local letter_like = {
[token.command_id'spacer'] = true,
[token.command_id'letter'] = true,
[token.command_id'other_char'] = true,
}
% Some boilerplate to define \MapUnicodeGraphemes as a command from Lua
local func = luatexbase.new_luafunction'MapUnicodeGraphemes'
token.set_lua('MapUnicodeGraphemes', func)
lua.get_functions_table()[func] = function()
% Here we really start
% ====================
% First read the text we want to map
local text = token.scan_toks(false, true)
% Then scan what each grapheme cluster should be mapped to.
% Since this is scanned as a macro (like in \def), we have to insert #1 first to
% indicate that #1 is allowed inside.
token.put_next(token.create(\number`\#), token.create(\number`\1))
local mapping = token.scan_toks(true, false)
% Some variables:
% - `state` is a blackbox needed by lua-uni-graphemes
% - `new_cluster` will indicate if the current codepoint starts a new cluster
% - `start_of_cluster` is the index where the last cluster started
local state, new_cluster, start_of_cluster
% Iter
for i, tok in ipairs(text) do
if letter_like[tok.command] then
% If this is actually a letter, pass it to lua_uni_graphemes
new_cluster, state = read_codepoint(tok.index, state)
if new_cluster then
% We have a new cluster. If we had a previous cluster, then that cluster finished with the previous character. Map it according to mapping and send it to TeX.
if start_of_cluster then
tex.sprint(insert_arg(mapping, table.move(text, start_of_cluster, i - 1, 1, {})))
end
% Then record the beginning of this cluster
start_of_cluster = i
end
else
% If some other token appears raise and error
tex.error('Ignoring unexpected token in argument')
end
end
% Finally make sure that the final cluster also gets mapped and send to TeX
if start_of_cluster then
tex.sprint(insert_arg(mapping, table.move(text, start_of_cluster, \csstring\#text, 1, {})))
end
end
}
\NewExpandableDocumentCommand \countGraphemes { m }
{%
% With the \MapUnicodeGraphemes helper we can map every grapheme cluster to +1, such that we can form an expression indicating the number of clusters
\number\numexpr0\MapUnicodeGraphemes {#1}{+1}\relax
}
\begin{document}
There are \countGraphemes{aàáâãäåāăąæa̧̯̅̈̏} grapheme clusters in “aàáâãäåāăąæa̧̯̅̈̏”. These are:
\MapUnicodeGraphemes{aàáâãäåāăąæa̧̯̅̈̏}{#1, } and that's it.
\end{document}
答案4
这不是重音字符(“异国字符”)的普遍问题,只有一些字符,尤其是ā
和其他“上划线”字符,例如ē
、ī
等,以及“凹线”字符,例如ǎ
。注意:
\documentclass{article}
\usepackage{xstring}
\usepackage[T1]{fontenc}
\begin{document}
\StrLen{aàáâãäåăąæ}
\end{document}
正确返回10
,而\StrLen{ā}
返回47
。