我有这个用特定字符替换某些字符的例子,替换在整个输入流上工作正常,但在通过 tex 命令插入的文本上失败。
有没有办法让替换对通过 tex 命令插入的文本也有效?@DavidCarlisle 建议使用 pre-linebreak 回调,有人知道怎么做吗?
\documentclass{article}
\usepackage{luacode,luacolor}
\usepackage[bidi=basic]{babel}
\babelprovide[import,main]{arabic}
\babelfont{rm}{Amiri}
\begin{luacode}
M = {}
addtosubstitutions = function(input,output)
M[#M + 1] = {}
M[#M][1] = input
M[#M][2] = output
end
substitutechars = function(head)
for i = 1,#M do
head = string.gsub(head,M[i][1],M[i][2])
end
return head
end
\end{luacode}
\def\substitutechars{%
\directlua{luatexbase.add_to_callback("process_input_buffer",substitutechars,"substitutechars")}}
\def\unsubstitutechars{
\directlua{luatexbase.remove_from_callback("process_input_buffer","substitutechars")}}
\def\addtosubstitutions#1#2{%
\directlua{addtosubstitutions("#1","#2")}
}
\def\foo{نص طويل جدا من اليمين إلى اليسار}
\begin{document}
\addtosubstitutions{يمين}{\\textcolor{red}{يمين}}
\substitutechars
نص طويل جدا من اليمين إلى اليسار
\foo % Substitution fails here
\unsubstitutechars
\end{document}
答案1
这是一个有趣的挑战!
问题
目标是在以下情况下找到“查找字符串”并将其替换为“替换内容”:
- “查找字符串”是直接在文档中输入的整个 ASCII 单词
- “查找字符串”位于文档中直接输入的 ASCII 纯单词中间
- “查找字符串”是另一个命令扩展的结果
- “查找字符串”位于
\hbox
- “查找字符串”是 Unicode(非 ASCII)文本
- “查找字符串”位于用复杂(阿拉伯)文字书写的单词中间
- 有多对“查找字符串”和“替换内容”
- 我们应该能够在 1 个段落中多次替换“查找字符串”
substitutions
仅在环境中执行所有这些操作- 能够同时完成所有这些
解决方案
(需要不早于 2022 年 11 月的 LaTeX 内核)
\documentclass{article}
\usepackage[bidi=basic]{babel}
\babelprovide[import,main]{arabic}
\babelfont{rm}{Amiri}
\usepackage{luacode}
\usepackage{luacolor}
\makeatletter
\newbox\find@box
\newbox\replace@box
\begin{luacode*}
local function make_tex_function(name, func)
local index = luatexbase.new_luafunction(name)
lua.get_functions_table()[index] = func
token.set_lua(name, index, "global")
end
local replacements = {}
local function add_substitution(find, replace)
local find = node.copy_list(tex.getbox("find@box").head)
local replace = node.copy_list(tex.getbox("replace@box").head)
replacements[find] = replace
end
make_tex_function("add@substitution", add_substitution)
local function next_glyph(head)
for n in node.traverse_glyph(head) do
return n
end
end
-- local function debug_print(str, char)
-- print(str, char and char.char and unicode.utf8.char(char.char) or "", char)
-- end
local function debug_print() end
local function prev_glyph(head)
while head do
if head.id == node.id("glyph") then
return head
end
head = head.prev
end
end
local enabled = false
local function do_substitutions(head)
if not enabled then
return head
end
local function traverse(n, find, replace, status)
if status then
find = next_glyph(status[2].next)
local start = status[1]
if find then
if n and n.char == find.char then
debug_print("CONTINUE", n)
status = {start, find}
else
debug_print("FAIL", n)
status = nil
end
else
debug_print("END", n)
replace = node.copy_list(replace)
start.prev.next = replace
if n then
node.slide(replace).next = prev_glyph(n.prev).next
end
status = nil
end
elseif n then
if n.char == find.char then
debug_print("START", n)
status = {n, find}
else
debug_print("SKIP", n)
end
end
return status
end
for find, replace in pairs(replacements) do
debug_print("")
for n in node.traverse_glyph(find) do
debug_print("FIND", n)
end
local status, prev_n
for n in node.traverse_glyph(head) do
status = traverse(n, next_glyph(find), replace, status)
prev_n = n
end
traverse(prev_n.next, find, replace, status)
end
return head
end
luatexbase.add_to_callback(
"pre_linebreak_filter",
do_substitutions,
"substitutions"
)
luatexbase.add_to_callback(
"hpack_filter",
do_substitutions,
"substitutions"
)
luatexbase.declare_callback_rule(
"pre_linebreak_filter",
"substitutions", "before", "Babel.pre_otfload_v"
)
luatexbase.declare_callback_rule(
"hpack_filter",
"substitutions", "before", "Babel.pre_otfload_h"
)
make_tex_function("substitutions", function() enabled = true end)
make_tex_function("endsubstitutions", function() enabled = false end)
\end{luacode*}
\AddToHook{env/substitutions/before}{\par}
\AddToHook{env/substitutions/end}{\par}
\def\addtosubstitutions#1#2{%
\setbox\find@box=\hpack{#1}%
\setbox\replace@box=\hpack{#2}%
\add@substitution%
}
\makeatother
\addtosubstitutions{words}{\textcolor{red}{words}}
\addtosubstitutions{es}{ES}
\addtosubstitutions{يمين}{\textcolor{blue}{يمين}}
\def\testwords{test words test words}
\def\foo{نص طويل جدا من اليمين إلى اليسار}
\begin{document}
test words test words
\testwords
.. يمين xx
نص طويل جدا من اليمين إلى اليسار
\foo
\hbox{\testwords}
\hbox{\foo}
\bigskip
\begin{substitutions}
test words test words
\testwords
.. يمين xx
نص طويل جدا من اليمين إلى اليسار
\foo
\hbox{\testwords}
\hbox{\foo}
\end{substitutions}
\bigskip
test words test words
\testwords
.. يمين xx
نص طويل جدا من اليمين إلى اليسار
\foo
\hbox{\testwords}
\hbox{\foo}
\end{document}
解释
我们钩住pre_linebreak_filter
和hpack_filter
回调前Babel/HarfBuzz/luaotfload。在回调中,我们遍历当前段落/框中的每个字符。当我们找到“查找字符串”的开头时,我们会标记其位置。如果我们找到“查找字符串”中的所有字符,那么我们会在找到“查找字符串”的位置拼接“替换内容”。我们对每个“查找字符串”和段落/框重复此操作,然后将结果返回给 TeX。
答案2
\substitutechars
如果您在...之外定义字符串\unsubstitutechars
,则需要在其他地方进行替换。您可以调整函数substitutechars()
,以便它也可以直接输出(替换的)字符串。因此,例如,您可以执行以下操作:
\documentclass{article}
\usepackage{luacode,luacolor}
\usepackage[bidi=basic]{babel}
\babelprovide[import,main]{arabic}
\babelfont{rm}{Amiri}
\begin{luacode}
M = {}
addtosubstitutions = function(input,output)
M[#M + 1] = {}
M[#M][1] = input
M[#M][2] = output
end
substitutechars = function(head, print)
print = print or false
for i = 1,#M do
head = string.gsub(head,M[i][1],M[i][2])
end
if print then
tex.print(head)
end
return head
end
\end{luacode}
\def\substitutechars{%
\directlua{luatexbase.add_to_callback("process_input_buffer",substitutechars,"substitutechars")}}
\def\unsubstitutechars{
\directlua{luatexbase.remove_from_callback("process_input_buffer","substitutechars")}}
\def\addtosubstitutions#1#2{%
\directlua{addtosubstitutions("#1","#2")}
}
\def\presubstitutechars#1{%
\directlua{substitutechars("#1", true)}
}
\def\foo{\presubstitutechars{نص طويل جدا من اليمين إلى اليسار}}
\begin{document}
\addtosubstitutions{يمين}{\\textcolor{red}{يمين}}
\substitutechars
نص طويل جدا من اليمين إلى اليسار
\foo
\unsubstitutechars
\end{document}
不过,我不知道这是否可以成为您具体用例的解决方案。