8

I have a simple macro which displays the letter “A” in the document:

\define\displayletter{A}

This is some text. \displayletter
This is some more text. \displayletter This is other text.
This is also some text. \displayletter

When compiled, this will print:

This is some text. A This is some more text. A This is other text. This is also some text. A

In some paragraphs, I need to have the last occurrence of \displayletter to instead display the letter “Z”.

There might be various solutions to this, but I think that a macro, such as this, placed around each paragraph can control whether or not the last letter appears as an “A” or “Z”:

\displayparagraph{0}{
    This is some text. \displayletter
    This is some more text. \displayletter This is other text.
    This is also some text. \displayletter
}

If #1 is equal to 0, then, every \displayletter in enclosed paragraph should be displayed as an “A”. This displays:

This is some text. A This is some more text. A This is other text. This is also some text. A

If #1 is greater than 0, then the last occurrence of \displayletter within the enclosed paragraph should be displayed as a “Z”, but all other previous occurrences of \displayletter within the paragraph remain as an “A”. This displays:

This is some text. A This is some more text. A This is other text. This is also some text. Z

How can I make the last occurrence of a macro within another macro display a different letter?

2 Answers 2

13
+150

Below is my approach to this but it modifies the user interface a bit so that it looks more Context-ish. (You can skip the implementation and start reading the code with the section “user interface”.)

EDIT: The code has been updated to reflect new functionality added with version 2012-09-21 of ConTeXt. No hacks necessary anymore ;-) Also, the rule final is now a synonym for last, following a suggestion by Aditya. If you are stuck with an older MkIV, the earlier version has been archived as a gist.

The substitution macros in action

There are two things going on.

  1. Paragraphs are counted and occurrences of the macro are stored.
  2. On macro invocation, a set of conditions is evaluated to determine the output.

You will need to define a condition set and hook it into a macro. The set is defined via \definesubstitutionset. It takes a key-value list that maps conditions to output:

\definesubstitutionset [mysub] [1=a,2=b,default=x,final=y]

The line above means that in a given paragraph, the first time the letter a will be printed, the second time b, the last time y and for all other occurrences x.

Now this substitution set must be assigned to a macro. Create a new one using \definesubstitute and use it as the parameter substitutionset:

\definesubstitute [displayfoo] [substitutionset=mysub]

(Also, try the style and color keys!) Now you can use the macro \displayfoo and the content it actually typesets will depend on the conditions specified.

% macros=mkvi
\unprotect

%% 1. Declare a counter and increment it with every paragraph start.
\newcount\parcount \parcount0
\appendtoks \advance\parcount1 \to \everypar

\startluacode
  local context       = context
  local jobdatasets   = job.datasets
  local tablefastcopy = table.fastcopy
  -- 2. Retrieve the state of the previous run. NEW: Datasets make this
  --    go smooth from the Lua end since version 2012-09-21.
  local paragraph_info = jobdatasets.getdata(
                          "substitute",
                          "paragraph_info")

  local occurrences, substitution_sets = { }, { }
  documentdata.occurrences = occurrences

  -- 3. (Ab)use the table to emulate a switch statement.
  local do_display_letter = function (n, old_n, branches)
    local s
    if not old_n then -- paragraph count changed
      s = branches.default
    elseif n == old_n then
      s = branches.last
    else
      s = branches[n] or branches.default
    end
    context(s)
  end

  -- 4. Bookkeeping: store away the number of times the macro is called
  --    per paragraph. Then, continue with the response function.
  local substitute = function (sub_id, set_id, parcount)
    local previous = paragraph_info and paragraph_info[sub_id]
    local current  = occurrences[sub_id]
    current[parcount] = current[parcount] or 0
    current[parcount] = current[parcount]  + 1
    do_display_letter(
      current[parcount],
      previous and previous[parcount],
      substitution_sets[set_id])
  end

  -- 5. Define a function that stores the counters of the current pass
  --    in the .tuc file. The setter requires both a namespace
  --    (“substitute”) and a tag (“paragraph_info”), so as to emulate
  --    the usage of \type{\definedataset}.
  local storeinfo = function ( )
    jobdatasets.setdata {
      name = "substitute",
      tag  = "paragraph_info",
      data = occurrences,
    }
  end

  -- 6. Code for key-value parser in Lua. Makes it easy to define
  --    substitutions using setups.
  local lpeg = require "lpeg"

  local C, Cc, Cf, Cg, Ct = lpeg.C, lpeg.Cc, lpeg.Cf, lpeg.Cg, lpeg.Ct

  local P, S, V, lpegmatch = lpeg.P, lpeg.S, lpeg.V, lpeg.match

  local p_args = P{
    "args",
    args           = Cf(Ct"" * (V"kv_pair" + V"emptyline")^0, rawset),
    kv_pair        = Cg(V"key"
                      * V"separator"
                      * (V"value" * V"final"
                      + V"empty"))
                  * V"rest_of_line"^-1
                  ,
    key            = V"whitespace"^0 * C(V"key_char"^1),
    key_char       = (1 - V"whitespace" - V"eol" - V"equals")^1,
    separator      = V"whitespace"^0 * V"equals" * V"whitespace"^0,
    empty          = V"whitespace"^0 * V"comma" * V"rest_of_line"^-1
                  * Cc(false)
                  ,
    value          = C((V"balanced" + (1 - V"final"))^1),
    final          = V"whitespace"^0 * V"comma" + V"rest_of_string",
    rest_of_string = V"whitespace"^0
                   * V"eol_comment"^-1
                   * V"eol"^0
                   * V"eof"
                   ,
    rest_of_line   = V"whitespace"^0 * V"eol_comment"^-1 * V"eol",
    eol_comment    = V"comment_string" * (1 - (V"eol" + V"eof"))^0,
    comment_string = V"lua_comment" + V"TeX_comment",
    TeX_comment    = V"percent",
    lua_comment    = V"double_dash",
    emptyline      = V"rest_of_line",

    balanced       = V"balanced_brk" + V"balanced_brc",
    balanced_brk   = V"lbrk"
                   * (V"balanced" + (1 - V"rbrk"))^0
                   * V"rbrk"
                   ,
    balanced_brc   = V"lbrc"
                   * (V"balanced" + (1 - V"rbrc"))^0
                   * V"rbrc"
                   ,
    -- Terminals
    eol            = P"\n\r" + P"\r\n" + P"\n" + P"\r",
    eof            = -P(1),
    whitespace     = S" \t\v",
    equals         = P"=",
    dot            = P".",
    comma          = P",",
    dash           = P"-",    double_dash  = V"dash" * V"dash",
    percent        = P"%",
    lbrk           = P"[",    rbrk         = P"]",
    lbrc           = P"{",    rbrc         = P"}",
  }

  local defaults = { -- EDIT: defaults are a tad more helpful
    last    = [[{\bold RULE “last” MISSING}]],
    default = [[{\bold RULE “default” MISSING}]],
  }

  -- 7. The substitution sets are stored in a Lua by their id. If it’s
  --    an integer, it will be stored on the array part. The special
  --    conditions “default” and “last” are stored on the hash.
  local define_substitutionset = function (set_id, parent, raw)
    local current = parent and tablefastcopy(substitution_sets[parent])
                            or { }
    local args = lpegmatch(p_args, raw)
    --- sanitize arguments to have numbers on the array part
    for k, v in next, args do
      k = tonumber(k) or k
      current[k] = v
    end
    --- make “.final” a synonym for “.last”
    current.last = current.last or current.final
    --- substitute defaults for missing mandatory rules
    for k, v in next, defaults do current[k] = current[k] or v end
    substitution_sets[set_id] = current
  end

  commands.conditional_substitution = substitute
  commands.store_substitution_info  = storeinfo
  commands.define_substitutionset   = define_substitutionset
\stopluacode

%% 8. Substitutions are assigned using \type{\definesubstitutionset},
%%    which supports inheritance.
\def\definesubstitutionset{%
  \dotripleempty\define_substitutionset_indeed%
}

\def\define_substitutionset_indeed[#id][#second][#third]{%
  \ifthirdargument% inherit
    \ctxcommand{define_substitutionset(
                  \!!bs#id\!!es,
                  \!!bs#second\!!es,
                  \!!bs\detokenize{#third}\!!es)}%
  \else% new definition
    \ctxcommand{define_substitutionset(
                  \!!bs#id\!!es,
                  false,
                  \!!bs\detokenize{#second}\!!es)}%
  \fi%
}

%% 9. For the actual macro generator we rely on the generic namespace
%%    functionality.

\installnamespace {substitute}
\installdefinehandler        \????substitute {substitute}
                             \????substitute
\installsetuphandler         \????substitute {substitute}
\installparameterhandler     \????substitute {substitute}
\installstyleandcolorhandler \????substitute {substitute}

\appendtoks
  \ctxlua{documentdata.occurrences["\currentsubstitute"] = { }}
  \setuevalue{\currentsubstitute}%
             {\substitute_direct[\currentsubstitute]}%
\to \everydefinesubstitute

\unexpanded\def\substitute_direct[#id]{%
  \edef\currentsubstitute{#id}%
  \dosingleempty\substitute_direct_indeed%
}

\def\substitute_direct_indeed[#setups]{%
  \bgroup%
    \iffirstargument\setupcurrentsubstitute[#setups]\fi
    \usesubstitutestyleandcolor\c!style\c!color%% I♥ConTeXt
    \ctxcommand{conditional_substitution(
                  \!!bs\currentsubstitute\!!es,
                  \!!bs\substituteparameter{substitutionset}\!!es,
                  \the\parcount)}%
  \egroup%
}

%% 10. Store the counters in the .tuc file on exit.
\appendtoks \ctxcommand{store_substitution_info()} \to \everybye

\protect


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%                          user interface                           %%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% Usage:
%%  First, define one or more substitution sets. These map occurrences
%%         to output. Integers as key are interpreted as “use this for
%%         the $n$-th occurrence. Two keys are special: “last” for the
%%         last occurrence in a paragraph. “default” for any other
%%         occurrence that is not explicitly defined.
%%         {\em EDIT}: “final” has been made a synonym for “last”.
%%
%%
%%  Second, define a substitution macro. These behave just as normal
%%          ConTeXt macro generators including inheritance. They also
%%          have corresponding \setup<id>. Just because it’s no effort
%%          to implement, the “substitute” macros also respect “style”
%%          and “color” parameters.

\definesubstitutionset [1] [default=A,last=A]
\definesubstitutionset [2] [1] [last=Z]

\definesubstitute [displayletter] [substitutionset=1,style=bold]

\definesubstitutionset [yetanotherset] [
  1=Initial occurrence.,
  3=Kilroy was here.,
  last=Last occurrence.,
  default=Ordinary occurrence.,
]

\definesubstitute [displaywhatever] [
  substitutionset=yetanotherset,
  style=smallcaps,
]

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%                              testing                              %%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\starttext

  foo \displayletter      bar \displayletter      baz \displayletter    
  foo \displayletter[substitutionset=2]
  bar \displayletter[substitutionset=2]
  baz \displayletter[substitutionset=2]

  foo \displaywhatever      bar \displaywhatever      baz \displaywhatever    
\stoptext \endinput
2
  • It is better to use last as a key rather than final because last is already used as a key by other commands (and therefore you get automatic multilingual interface if you use \c!last, etc.
    – Aditya
    Commented Sep 19, 2012 at 6:01
  • 2
    +1 for putting so much effort into this and for providing an excellent answer one can learn a lot from.
    – Marco
    Commented Sep 19, 2012 at 11:21
5

This would be my approach to solve the problem. I was inspired by the comprehensive answer of @phg to try it in a more direct way. Because I don't know much about ConTeXt I give a LuaLatex example. But the main idea is implemented in a Lua function so I think it doesn't matter.

\documentclass{article}
\usepackage{filecontents}

\begin{filecontents*}{luaFunctions.lua}
function ReplaceLetter(text)
     counter = 0 

     -- count the occurrences of '[displayletter]'
     _, count = string.gsub(text, "%[displayletter%]", "")

     -- define the replacements
    replacements={[1]="\\textbf{A}",[2]="\\textbf{B}",[count]="\\textbf{Z}",["default"]="\\textbf{D}"}

    -- replace all '[displayletter]'    
    out = string.gsub(text, "%[displayletter%]",
        function(word)
            counter = counter + 1 
            return replacements[counter] or replacements["default"]
        end)

    tex.print(out)
end    
\end{filecontents*}

\directlua{dofile("luaFunctions.lua")}
\def\displayparagraph#1{\directlua{ReplaceLetter("#1")}}

\begin{document}
\displayparagraph{%
Lorem ipsum dolor sit amet, [displayletter] consectetur adipisicing elit, 
sed do eiusmod tempor incididunt ut labore et dolore magna 
aliqua. Ut enim [displayletter] ad minim veniam, quis nostrud exercitation 
ullamco laboris nisi ut aliquip ex ea commodo consequat. 
Duis [displayletter] aute irure dolor in reprehenderit in voluptate velit 
esse cillum dolore eu fugiat nulla pariatur. [displayletter] Excepteur 
sint occaecat cupidatat [displayletter] non proident, sunt in culpa qui 
officia deserunt [displayletter] mollit anim id est laborum.}
\end{document}

enter image description here

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .