(I rewrote this answer rather significantly after discussions with OP and after receiving very significant coding help from @EgorSkriptunoff)
Here's a solution that doesn't pre-specify a list of all abbreviations for which thinspace should be inserted after interior periods (aka "full stops"). Instead, it sets up a pattern matching function to capture u.a.
, u.a.m.
, u.v.a.m.
, z.Zt.
, Bem.d.Red.
and many more such cases. (See the function insert_thinspaces
in the code below for the exact pattern matches that are performed.)
Observe also the use of unicode.utf8.gsub
instead of string.gsub
inside the Lua function insert_thinspaces
. This lets the code deal correctly with non-ASCII-encoded letters, such as ä
and Ä
, which may occur in abbreviations.
On the downside (potentially), this solution method doesn't capture abbreviations if they occur at the start of a sentence, e.g., Z.T.
or U.U.
; for what it's worth, your parallel answer currently doesn't catch such cases either, right?
The Lua function is assigned to the process_input_buffer
callback via a LaTeX macro called \ExpandAbbrOn
. If, for any reason, you need to suspend operation of the Lua function, simply execute the instruction \ExpandAbbrOff
.
The code checks if the string to be processed lies inside a verbatim-like environment such as verbatim
, Verbatim
, and lstlisting
; if that's the case, no processing is performed. And, with the latest iteration, the code now also ignores material that's in the arguments of inline-verbatim-like macros, such as \verb
, \Verb
, \lstinline
, and \url
. For sure, the contents of URL strings should never be processed by the Lua function, right?

% !TeX program = lualatex
\documentclass{article}
\usepackage[ngerman]{babel}
\usepackage{fancyvrb} % for "Verbatim" env.
\usepackage[obeyspaces]{url} % for "\url" macro
\usepackage{listings} % for "\lstinline" macro
%% Lua-side code:
\usepackage{luacode} % for 'luacode*' environment
\begin{luacode*}
-- Names of verbatim-like environments
local verbatim_env = { "[vV]erbatim" , "lstlisting" }
-- By default, we're *not* in a verbatim-like env.:
local in_verbatim_env = false
-- Specify number of parameters for every macro; use neg. numbers
-- for macros that support matching pair of curly braces {}
local all_macros = {
verb = 1,
Verb = 1,
lstinline = -1,
url = -1
}
-- List all poss. delimiters
local all_delimiters = [[!"#$%&'*+,-./:;<=>?^_`|~()[]{}0123456789]]
-- Quick check if "s" contains an inline-verbatim-like macro:
function quick_check ( s )
if s:find("\\[vV]erb") or s:find("\\url") or s:find("\\lstinline") then
return true
else
return false
end
end
-- Function to process the part of string "s" that
-- does *not* contain inline-verbatim-like macros
local function insert_thinspaces ( s )
s = unicode.utf8.gsub ( s ,
"(%l%.)(%a%l?%l?%.)(%a%l?%l?%.)(%a%l?%l?%.)",
"%1\\,%2\\,%3\\,%4" ) -- e.g. "u.v.a.m.", "w.z.b.w."
s = unicode.utf8.gsub ( s ,
"(%l%.)(%a%l?%l?%.)(%a%l?%l?%.)",
"%1\\,%2\\,%3" ) -- e.g., "a.d.Gr."
s = unicode.utf8.gsub ( s ,
"(%u%l%l?%.)(%a%l?%l?%.)(%a%l?%l?%.)",
"%1\\,%2\\,%3" ) -- e.g., "Anm.d.Red."
s = unicode.utf8.gsub ( s ,
"(%l%.)(%a%l?%l?%.)",
"%1\\,%2" ) -- e.g., "z.T.", "z.Zt.", "v.Chr."
return s
end
-- Finally, the main Lua function:
function expand_abbr ( s )
-- Check if we're entering or exiting a verbatim-like env.;
-- if so, reset the 'in_verbatim_env' "flag" and break.
for i,p in ipairs ( verbatim_env ) do
if s:find( "\\begin{" .. p .. "}" ) then
in_verbatim_env = true
break
elseif s:find( "\\end{" .. p .. "}" ) then
in_verbatim_env = false
break
end
end
-- Potentially modify "s" only if *not* in a verbatim-like env.:
if not in_verbatim_env then
-- Quick check if "s" contains one or more inlike-verbatim-like macros:
if quick_check ( s ) then
-- See https://stackoverflow.com/a/45688711/1014365 for the source
-- of the following code. Many many thanks, @EgorSkriptunoff!!
s = s:gsub("\\([%a@]+)",
function(macro_name)
if all_macros[macro_name] then
return
"\1\\"..macro_name
..(all_macros[macro_name] < 0 and "\2" or "\3")
:rep(math.abs(all_macros[macro_name]) + 1)
end
end
)
repeat
local old_length = #s
repeat
local old_length = #s
s = s:gsub("\2(\2+)(%b{})", "%2%1")
until old_length == #s
s = s:gsub("[\2\3]([\2\3]+)((["..all_delimiters:gsub("%p", "%%%0").."])(.-)%3)", "%2%1")
until old_length == #s
s = ("\2"..s.."\1"):gsub("[\2\3]+([^\2\3]-)\1", insert_thinspaces):gsub("[\1\2\3]", "")
else
-- Since no inline-verbatim-like macro found in "s", invoke
-- the Lua function 'insert_thinspaces' directly.
s = insert_thinspaces ( s )
end
end
return(s)
end
\end{luacode*}
%% LaTeX-side code: Macros to assign 'expand_abbr'
%% to LuaTeX's 'process_input_buffer' callback.
\newcommand\ExpandAbbrOn{\directlua{%
luatexbase.add_to_callback("process_input_buffer",
expand_abbr, "expand_abbreviations")}}
\newcommand\ExpandAbbrOff{\directlua{%
luatexbase.remove_from_callback("process_input_buffer",
"expand_abbreviations")}}
\AtBeginDocument{\ExpandAbbrOn} % enabled by default
%% Just for this example:
\setlength\parindent{0pt}
\obeylines
\begin{document}
Dies ist u.U. ein Test.
\begin{Verbatim}
Dies ist u.U. ein Test.
\end{Verbatim}
z.B. u.a. u.Ä. u.ä. u.U. a.a.O. d.h. i.e. v.a.
i.e.S. z.T. m.E. i.d.F. z.Z. u.v.m. z.Zt.
u.v.a.m. b.z.b.w. v.Chr. a.d.Gr. Anm.d.Red.
\begin{verbatim}
z.B. u.a. u.Ä. u.ä. u.U. a.a.O. d.h. i.e. v.a.
i.e.S. z.T. m.E. i.d.F. z.Z. u.v.m. z.Zt.
u.v.a.m. b.z.b.w. v.Chr. a.d.Gr. Anm.d.Red.
\end{verbatim}
U.S.A. U.K. % should *not* be processed
\lstinline|u.a. u.Ä. u.ä.|; \Verb$u.a. u.Ä. u.ä.$
% nested verbatim-like macros
\Verb+u.U.\lstinline|u.U.|u.U.+ \lstinline+u.U.\[email protected][email protected].+
% 2 URL strings
u.U. \url{u.U.aaaa.z.T.bbb_u.v.a.m.com} u.U.
u.U. \url?u.U.aaaa.z.T.bbb_u.v.a.m.com? u.U.
\end{document}
<single letter>.<single letter>.
? Please advise.etc.pp.
(but I haven't seen that abbreviation in any text in my entire life). In general they might exist but are not that common.\zB
(or in my case,\eg
, or\ie
) to give the desired spacing.\documentclass{article} \newcommand\zB{z.\,B.} \begin{document} Dies ist \zB{} ein Test. \end{document}
That way, if I ever change my mind on the notion of "proper" spacing, a one line fix fixes the whole document.