Take the 2-minute tour ×
TeX - LaTeX Stack Exchange is a question and answer site for users of TeX, LaTeX, ConTeXt, and related typesetting systems. It's 100% free, no registration required.

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?

share|improve this question

2 Answers 2

up vote 12 down vote accepted
+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
share|improve this answer
    
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 Sep 19 '12 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 Sep 19 '12 at 11:21

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

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.