Here is a routine
\SurroundDDigits{⟨formatting command which takes one undelimited argument⟩}{⟨tokens⟩}
which in a tail-recursive loop examines ⟨tokens⟩ without expanding them. Tail-recursion/expansion is driven by \romannumeral until all tokens are examined. When all tokens are examined, then a TeX-⟨number⟩-quantity of value 0 trailed by the tokens forming by the result is delivered, yielding that TeX, due to encountering that ⟨number⟩-quantity, stops gathering a ⟨number⟩ for \romannumeral and for the gathered ⟨number⟩-quantity of value 0 delivers no token in return as 0 is not a positive number.
Sequences of pattern
D11 + non-empty sequence of tokens that are elements of {012, 112, 212, 312, 412, 512, 612, 712, 812, 912}
in the result are nested between curly braces ({1 and }2) whereof the left curly brace is preceded by ⟨formatting command which takes one undelimited argument⟩.
As a side-effect in the result
- any explicit character-token of category code 1(begin-group) is replaced by an explicit { of category-code 1 and
- any explicit character-token of category code 2(end-group) is replaced by an explicit } of category-code 2.
Due to \romannumeral-expansion the result is delivered by triggering two expansion-steps/after two "hits" with \expandafter.
I did my best to ensure that expansion will not be disturbed within alignments/tables etc, but no warranties.
For the sake of having fun I did not use any ε-TeX- or LuaTeX- or pdfTeX-extensions or the like and I avoided any \if...\else...\fi- and any \csname..\endcsname-expression.
Thus (unbalanced parts of) such expressions occurring within ⟨tokens⟩ does not disturb expansion.
Exclamation-mark as argument-delimiter is used a lot in one place.
This might probably bite you in case the uppercase code or lowercase code of the exclamation-mark is changed and the course of expansion-action is intercepted right at the corresponding stage for applying \uppercase/\lowercase. Due to expansion being driven via \romannumeral until obtaining the result this is extremely unlikely to happen if possible at all.
Usage is at your own risk.
\errorcontextlines=10000
\makeatletter
%%=============================================================================
%% PARAPHERNALIA:
%% \UD@firstoftwo, \UD@secondoftwo, \UD@PassFirstToSecond, \UD@Exchange,
%% \UD@removespace, \UD@stopromannumeral, \UD@CheckWhetherNull,
%% \UD@CheckWhetherBrace, \UD@CheckWhetherLeadingExplicitSpace,
%% \UD@ExtractFirstArg
%%=============================================================================
\newcommand\UD@firstoftwo[2]{#1}%
\newcommand\UD@secondoftwo[2]{#2}%
\newcommand\UD@PassFirstToSecond[2]{#2{#1}}%
\newcommand\UD@Exchange[2]{#2#1}%
\@ifdefinable\UD@removespace{\UD@Exchange{ }{\def\UD@removespace}{}}%
\@ifdefinable\UD@stopromannumeral{\chardef\UD@stopromannumeral=`\^^00}%
%%-----------------------------------------------------------------------------
%% Check whether argument is empty:
%%.............................................................................
%% \UD@CheckWhetherNull{<Argument which is to be checked>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked is empty>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked is not empty>}%
%%
%% The gist of this macro comes from Robert R. Schneck's \ifempty-macro:
%% <https://groups.google.com/forum/#!original/comp.text.tex/kuOEIQIrElc/lUg37FmhA74J>
\newcommand\UD@CheckWhetherNull[1]{%
\romannumeral\expandafter\UD@secondoftwo\string{\expandafter
\UD@secondoftwo\expandafter{\expandafter{\string#1}\expandafter
\UD@secondoftwo\string}\expandafter\UD@firstoftwo\expandafter{\expandafter
\UD@secondoftwo\string}\expandafter\UD@stopromannumeral\UD@secondoftwo}{%
\expandafter\UD@stopromannumeral\UD@firstoftwo}%
}%
%%-----------------------------------------------------------------------------
%% Check whether argument is blank (empty or only spaces):
%%-----------------------------------------------------------------------------
%% -- Take advantage of the fact that TeX discards space tokens when
%% "fetching" _un_delimited arguments: --
%% \UD@CheckWhetherBlank{<Argument which is to be checked>}%
%% {<Tokens to be delivered in case that
%% argument which is to be checked is blank>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked is not blank>}%
\newcommand\UD@CheckWhetherBlank[1]{%
\romannumeral\expandafter\expandafter\expandafter\UD@secondoftwo
\expandafter\UD@CheckWhetherNull\expandafter{\UD@firstoftwo#1{}{}}%
}%
%%-----------------------------------------------------------------------------
%% Check whether argument's first token is a catcode-1-character
%%.............................................................................
%% \CheckWhetherBrace{<Argument which is to be checked>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked has a leading
%% explicit catcode-1-character-token>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked does not have a
%% leading explicit catcode-1-character-token>}%
\newcommand\UD@CheckWhetherBrace[1]{%
\romannumeral\expandafter\UD@secondoftwo\expandafter{\expandafter{%
\string#1.}\expandafter\UD@firstoftwo\expandafter{\expandafter
\UD@secondoftwo\string}\expandafter\UD@stopromannumeral\UD@firstoftwo}{%
\expandafter\UD@stopromannumeral\UD@secondoftwo}%
}%
%%-----------------------------------------------------------------------------
%% Check whether brace-balanced argument starts with a space-token
%%.............................................................................
%% \UD@CheckWhetherLeadingExplicitSpace{<Argument which is to be checked>}%
%% {<Tokens to be delivered in case <argument
%% which is to be checked> does have a
%% leading explicit space-token>}%
%% {<Tokens to be delivered in case <argument
%% which is to be checked> does not have a
%% a leading explicit space-token>}%
\newcommand\UD@CheckWhetherLeadingExplicitSpace[1]{%
\romannumeral\UD@CheckWhetherNull{#1}%
{\expandafter\UD@stopromannumeral\UD@secondoftwo}%
{%
% Let's nest things into \UD@firstoftwo{...}{} to make sure they are nested in braces
% and thus do not disturb when the test is carried out within \halign/\valign:
\expandafter\UD@firstoftwo\expandafter{%
\expandafter\expandafter\expandafter\UD@stopromannumeral
\romannumeral\expandafter\UD@secondoftwo
\string{\UD@CheckWhetherLeadingExplicitSpaceB.#1 }{}%
}{}%
}%
}%
\@ifdefinable\UD@CheckWhetherLeadingExplicitSpaceB{%
\long\def\UD@CheckWhetherLeadingExplicitSpaceB#1 {%
\expandafter\UD@CheckWhetherNull\expandafter{\UD@firstoftwo{}#1}%
{\UD@Exchange{\UD@firstoftwo}}{\UD@Exchange{\UD@secondoftwo}}%
{\expandafter\expandafter\expandafter\UD@stopromannumeral
\expandafter\expandafter\expandafter}%
\expandafter\UD@secondoftwo\expandafter{\string}%
}%
}%
%%-----------------------------------------------------------------------------
%% Extract first inner undelimited argument:
%%.............................................................................
%% \UD@ExtractFirstArg{ABCDE} yields {A}
%%
%% \UD@ExtractFirstArg{{AB}CDE} yields {{AB}}
%%
%% Due to \romannumeral-expansion the result is delivered after two
%% expansion-steps/after "hitting" \UD@ExtractFirstArg with \expandafter
%% twice.
%%
%% \UD@ExtractFirstArg's argument must not be blank.
%% This case can be cranked out via \UD@CheckWhetherBlank before calling
%% \UD@ExtractFirstArg.
%%
%% Use frozen-\relax as delimiter for speeding things up.
%% I chose frozen-\relax because David Carlisle pointed out in
%% <https://tex.stackexchange.com/a/578877>
%% that frozen-\relax cannot be (re)defined in terms of \outer and cannot be
%% affected by \uppercase/\lowercase.
%%
%% \UD@ExtractFirstArg's argument may contain frozen-\relax:
%% The only effect is that internally more iterations are needed for
%% obtaining the result.
%%.............................................................................
\@ifdefinable\UD@RemoveTillFrozenrelax{%
\expandafter\expandafter\expandafter\UD@Exchange
\expandafter\expandafter\expandafter{%
\expandafter\expandafter\ifnum0=0\fi}%
{\long\def\UD@RemoveTillFrozenrelax#1#2}{{#1}}%
}%
\expandafter\UD@PassFirstToSecond\expandafter{%
\romannumeral\expandafter
\UD@PassFirstToSecond\expandafter{\romannumeral
\expandafter\expandafter\expandafter\UD@Exchange
\expandafter\expandafter\expandafter{%
\expandafter\expandafter\ifnum0=0\fi}{\UD@stopromannumeral#1}%
}{%
\UD@stopromannumeral\romannumeral\UD@ExtractFirstArgLoop
}%
}{%
\newcommand\UD@ExtractFirstArg[1]%
}%
\newcommand\UD@ExtractFirstArgLoop[1]{%
\expandafter\UD@CheckWhetherNull\expandafter{\UD@firstoftwo{}#1}%
{\UD@stopromannumeral#1}%
{\expandafter\UD@ExtractFirstArgLoop\expandafter{\UD@RemoveTillFrozenrelax#1}}%
}%
%====================================================================
\@ifdefinable\UD@gobbletoexclam{\long\def\UD@gobbletoexclam#1!{}}%
\@ifdefinable\UD@ChooseDDigit{%
\long\def\UD@ChooseDDigit#1!1!2!3!4!5!6!7!8!9!0!D!#2#3!!!!{#2}%
}%
\newcommand\UD@CollectDDigit[5]{%
% #1 token to examine
% #2 pending D
% #3 digits gathered as probable argument of formatting-command
% #4 formatting-command, takes 1 undelimited argument
% #5 tokens forming the result gathered so far
\expandafter\UD@CheckWhetherNull\expandafter{\UD@gobbletoexclam#1!}%
{%
\UD@ChooseDDigit
!#1!2!3!4!5!6!7!8!9!0!D!{\UD@firstoftwo}% digit 1
!1!#1!3!4!5!6!7!8!9!0!D!{\UD@firstoftwo}% digit 2
!1!2!#1!4!5!6!7!8!9!0!D!{\UD@firstoftwo}% digit 3
!1!2!3!#1!5!6!7!8!9!0!D!{\UD@firstoftwo}% digit 4
!1!2!3!4!#1!6!7!8!9!0!D!{\UD@firstoftwo}% digit 5
!1!2!3!4!5!#1!7!8!9!0!D!{\UD@firstoftwo}% digit 6
!1!2!3!4!5!6!#1!8!9!0!D!{\UD@firstoftwo}% digit 7
!1!2!3!4!5!6!7!#1!9!0!D!{\UD@firstoftwo}% digit 8
!1!2!3!4!5!6!7!8!#1!0!D!{\UD@firstoftwo}% digit 9
!1!2!3!4!5!6!7!8!9!#1!D!{\UD@firstoftwo}% digit 0
!1!2!3!4!5!6!7!8!9!0!#1!{\expandafter\UD@firstoftwo\UD@secondoftwo}% character D
!1!2!3!4!5!6!7!8!9!0!D!{\expandafter\UD@secondoftwo\UD@secondoftwo}!!!!%neither D nor digit
}{\expandafter\UD@secondoftwo\UD@secondoftwo}%%neither D nor digit
{% digit
\UD@CheckWhetherNull{#2}{%
\UD@stopromannumeral\UD@DDigitReplaceloop{}{}{#4}{#5#1}%
}{%
\UD@stopromannumeral\UD@DDigitReplaceloop{#2}{#3#1}{#4}{#5}%
}%
}{%
{% character D
\UD@CheckWhetherNull{#2}{%
\UD@PassFirstToSecond{#5}%
}{%
\UD@CheckWhetherNull{#3}{%
\UD@PassFirstToSecond{#5#2}%
}{%
\UD@PassFirstToSecond{#5#4{#2#3}}%
}%
}%
{\UD@stopromannumeral\UD@DDigitReplaceloop{#1}{}{#4}}%
}{% neither D nor digit
\UD@CheckWhetherNull{#2}{%
\UD@PassFirstToSecond{#5#1}%
}{%
\UD@CheckWhetherNull{#3}{%
\UD@PassFirstToSecond{#5#2#1}%
}{%
\UD@PassFirstToSecond{#5#4{#2#3}#1}%
}%
}%
{\UD@stopromannumeral\UD@DDigitReplaceloop{}{}{#4}}%
}%
}%
}%
\newcommand\UD@DDigitReplaceloop[5]{%
% #1 pending D
% #2 digits gathered as probable argument of formatting-command
% #3 formatting-command, takes 1 undelimited argument
% #4 tokens forming the result gathered so far
% #5 remaining token list to process
\UD@CheckWhetherNull{#5}{%
\UD@CheckWhetherNull{#1}{\UD@stopromannumeral#4#2}{%
\UD@CheckWhetherNull{#2}{\UD@stopromannumeral#4#1}{\UD@stopromannumeral#4#3{#1#2}}%
}%
}{%
\UD@CheckWhetherBrace{#5}{%
\expandafter\UD@PassFirstToSecond\expandafter{\UD@firstoftwo{}#5}{%
\expandafter\UD@PassFirstToSecond\expandafter{%
\romannumeral
\UD@CheckWhetherNull{#1}{%
\UD@PassFirstToSecond{\UD@stopromannumeral#4#2}%
}{%
\UD@CheckWhetherNull{#2}{%
\UD@PassFirstToSecond{\UD@stopromannumeral#4#1}%
}{%
\UD@PassFirstToSecond{\UD@stopromannumeral#4#3{#1#2}}%
}%
}%
{%
\expandafter\UD@PassFirstToSecond
\expandafter{%
\romannumeral
\expandafter\expandafter\expandafter\UD@PassFirstToSecond
\UD@ExtractFirstArg{#5}{\UD@DDigitReplaceloop{}{}{#3}{}}%
}%
}%
}{%
\UD@DDigitReplaceloop{}{}{#3}%
}%
}%
}{%
\UD@CheckWhetherLeadingExplicitSpace{#5}{%
\expandafter\UD@PassFirstToSecond\expandafter{\UD@removespace#5}{%
\UD@CheckWhetherNull{#1}{%
\UD@PassFirstToSecond{#4 }%
}{%
\UD@CheckWhetherNull{#2}{%
\UD@PassFirstToSecond{#4#1 }%
}{%
\UD@PassFirstToSecond{#4#3{#1#2} }%
}%
}%
{\UD@DDigitReplaceloop{}{}{#3}}%
}%
}{%
\expandafter\UD@PassFirstToSecond\expandafter{\UD@firstoftwo{}#5}{%
\expandafter\UD@firstoftwo\expandafter{%
\romannumeral
\expandafter\expandafter\expandafter
\UD@CollectDDigit\UD@ExtractFirstArg{#5}{#1}{#2}{#3}{#4}%
}{}%
}%
}%
}%
}%
}%
\newcommand\SurroundDDigits[2]{%
\romannumeral\UD@DDigitReplaceloop{}{}{#1}{}{#2}%
}%
\makeatother
\documentclass{article}
\begin{document}
\begin{verbatim}
\expandafter\expandafter\expandafter\def
\expandafter\expandafter\expandafter\test
\expandafter\expandafter\expandafter{%
\SurroundDDigits{\textbf}{%
Axfd D12FF 23{AxfdD12FF2 3{Axf dD12FF23{}f\relax
D114 6ffasdfa}f\relax D1146ff asdfa}f\relax
D1146ffa sdfa%
}%
}%
{\ttfamily \meaning\test}
\end{verbatim}
\noindent yields:
\vskip\partopsep\vskip\topsep
\expandafter\expandafter\expandafter\def
\expandafter\expandafter\expandafter\test
\expandafter\expandafter\expandafter{%
\SurroundDDigits{\textbf}{%
Axfd D12FF 23{AxfdD12FF2 3{Axf dD12FF23{}f\relax D114 6ffasdfa}f\relax D1146ff asdfa}f\relax D1146ffa sdfa%
}%
}%
{\ttfamily \meaning\test}
\vskip\partopsep\vskip\topsep
\noindent\hrule
\vskip\partopsep\vskip\topsep
\begin{verbatim}
\newcommand\MyFontCommand[1]{{\footnotesize\emph{\textbf{#1}}}}%
\SurroundDDigits{\MyFontCommand}{%
Axfd D12FF 23{AxfdD12FF2 3{Axf dD12FF23{}f\relax
D114 6ffasdfa}f\relax D1146ff asdfa}f\relax
D1146ffa sdfa%
}%
\end{verbatim}
\noindent yields:
\vskip\partopsep\vskip\topsep
\newcommand\MyFontCommand[1]{{\footnotesize\emph{\textbf{#1}}}}%
\SurroundDDigits{\MyFontCommand}{%
Axfd D12FF 23{AxfdD12FF2 3{Axf dD12FF23{}f\relax
D114 6ffasdfa}f\relax D1146ff asdfa}f\relax
D1146ffa sdfa%
}%
\end{document}

\D{1234}and\D{abcd}? Wouldn't you just use\Dwith numbers necessarily? Regardless, if you're using LuaLaTeX, you don't even have to use\D{<num>}as you should be able to intercept the input sequence and replace any occurrence ofD<num>withD<num>(in bold). – Werner Jul 28 '21 at 20:58LaTeX3allows working with regex. Here it would be an easy way to achieve your goal. Take a look at this answer. – projetmbc Jul 29 '21 at 23:30Dof catcode 11(letter) in cases where the formatting is to be applied to theDof catcode 11 and these following tokens? What aboutD\seven0while\def\seven{7}is in effect? What aboutD\number`\A--\\A` is an alphabetic constant and therefore a valid TeX-