使用 lua 回调替换 tex 命令中插入的一些阿拉伯字符

使用 lua 回调替换 tex 命令中插入的一些阿拉伯字符

我有这个用特定字符替换某些字符的例子,替换在整个输入流上工作正常,但在通过 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

这是一个有趣的挑战!

问题

目标是在以下情况下找到“查找字符串”并将其替换为“替换内容”:

  1. “查找字符串”是直接在文档中输入的整个 ASCII 单词
  2. “查找字符串”位于文档中直接输入的 ASCII 纯单词中间
  3. “查找字符串”是另一个命令扩展的结果
  4. “查找字符串”位于\hbox
  5. “查找字符串”是 Unicode(非 ASCII)文本
  6. “查找字符串”位于用复杂(阿拉伯)文字书写的单词中间
  7. 有多对“查找字符串”和“替换内容”
  8. 我们应该能够在 1 个段落中多次替换“查找字符串”
  9. substitutions仅在环境中执行所有这些操作
  10. 能够同时完成所有这些

解决方案

(需要不早于 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_filterhpack_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}

在此处输入图片描述

不过,我不知道这是否可以成为您具体用例的解决方案。

相关内容