From 1598205ade1602e324b7fd8e5410d1389eae7416 Mon Sep 17 00:00:00 2001 From: Norbert Preining Date: Thu, 12 Oct 2023 03:04:15 +0000 Subject: CTAN sync 202310120304 --- macros/generic/memoize/memoize.edtx | 3597 +++++++++++++++++++++++++++++++++++ 1 file changed, 3597 insertions(+) create mode 100644 macros/generic/memoize/memoize.edtx (limited to 'macros/generic/memoize/memoize.edtx') diff --git a/macros/generic/memoize/memoize.edtx b/macros/generic/memoize/memoize.edtx new file mode 100644 index 0000000000..10794c7a3b --- /dev/null +++ b/macros/generic/memoize/memoize.edtx @@ -0,0 +1,3597 @@ +% \iffalse +% +% memoize.edtx (this is not a .dtx file; to produce a .dtx, process it with edtx2dtx) +% +%% This file is a part of Memoize, a TeX package for externalization of +%% graphics and memoization of compilation results in general, available at +%% https://ctan.org/pkg/memoize and https://github.com/sasozivanovic/memoize. +%% +%% Copyright (c) 2020- Saso Zivanovic +%% (Sa\v{s}o \v{Z}ivanovi\'{c}) +%% +%% This work may be distributed and/or modified under the conditions of the +%% LaTeX Project Public License, either version 1.3c of this license or (at +%% your option) any later version. The latest version of this license is in +%% https://www.latex-project.org/lppl.txt and version 1.3c or later is part of +%% all distributions of LaTeX version 2008 or later. +%% +%% This work has the LPPL maintenance status `maintained'. +%% The Current Maintainer of this work is Saso Zivanovic. +%% +%% The files belonging to this work and covered by LPPL are listed in +%% (/doc/generic/memoize/)FILES. +% +% \fi +% +% \begin{macrocode} +% +% \relax +% +% This file contains the documented source code of +% package \href{https://ctan.org/pkg/memoize}{Memoize} and, somewhat +% unconventionally, its two independently distributed auxiliary +% packages \href{https://ctan.org/pkg/advice}{Advice} +% and \href{https://ctan.org/pkg/collargs}{CollArgs}. +% +% The source code of the \hologo{TeX} parts of the package resides in +% |memoize.edtx|, |advice.edtx| and |collargs.edtx|. These files are written +% in \href{https://ctan.org/pkg/easydtx}{EasyDTX}, a format of my own invention +% which is almost like the DTX format but eliminates the need for all those +% pesky |macrocode| environments: Any line introduced by a single comment +% counts as documentation, and to top it off, documentation lines may be +% indented. An |.edtx| file is converted to a |.dtx| by a little Perl script +% called |edtx2dtx|; there is also a rudimentary Emacs mode, implemented in +% |easydoctex-mode.el|, which takes care of fontification, indentation, and +% forward and inverse search. +% +% The |.edtx| files contain the code for all three formats supported by the +% three packages --- \hologo{LaTeX} (guard |latex|), \hologo{plainTeX} (guard +% |plain|) and \hologo{ConTeXt} (guard |context|) --- but upon reading the +% code, it will quickly become clear that Memoize was first developed for +% \hologo{LaTeX}. In \S\ref{sec:code:identification}, we manually define +% whatever \hologo{LaTeX} tools are ``missing'' in \hologo{plainTeX} and +% \hologo{ConTeXt}. Even worse, \hologo{ConTeXt} code is often just the same +% as \hologo{plainTeX} code, even in cases where I'm sure \hologo{ConTeXt} +% offers the relevant tools. This nicely proves that I have no clue about +% \hologo{ConTeXt}. If you are willing to \hologo{ConTeXt}-ualize my code --- +% please do so, your help is welcome! +% +% The runtimes of Memoize (and also Advice) comprise of more than just the main +% runtime for each format. Memoize ships with two additional stub packages, +% |nomemoize| and |memoizable|, and a \hologo{TeX}-based extraction script +% |memoize-extract-one|; Advice optionally offers a \TikZ; support defined in +% |advice-tikz.code.tex|. For the relation between guards and runtimes, +% consult the core of the |.ins| files below. +% +% \tcbset{ins listing/.style={listing only, fonttitle=\ttfamily\footnotesize, +% leftupper=-1.5mm, lefttitle=2mm, right=0mm, top=2mm, bottom=2.65mm, +% listing options app={basicstyle=\ttfamily\scriptsize}}} +% +% \begin{tcbraster}[raster columns=100] +% \tcbinputlisting{raster multicolumn=55, ins listing, top=1mm, bottom=1mm, title=memoize.ins,listing file=../memoize.ins, linerange={27-38}, leftupper=1mm} +% \begin{tcboxedraster}[raster columns=1]{blankest, raster multicolumn=45} +% \tcbinputlisting{ins listing, title=advice.ins, listing file=../advice.ins, linerange=28-31} +% \tcbinputlisting{ins listing, title=collar\raisebox{0pt}[\height][0pt]{g}s.ins, listing file=../collargs.ins, linerange=29-31} +% \end{tcboxedraster} +% \end{tcbraster} +% +% Memoize also contains two scripts, |memoize-extract| and |memoize-clean|. +% Both come in two functionally equivalent implementations: Perl (|.pl|) and a +% Python (|.py|). The code is listed in \S\ref{sec:code:scripts}, I believe it +% is self-explanatory enough to lack more than a occasional comment. +% +% +% \thispagestyle{empty} +% \clearpage +% \tableofcontents +% \clearpage +% +% \newgeometry{left=4cm,right=1cm,top=1cm,bottom=1cm, +% marginparwidth=3cm,marginparsep=0pt,nohead,includefoot} +% \settowidth\marginparsep{\ } +% +% \section{First things first} +% \label{sec:code:identification} +% +% \paragraph{Identification} of |memoize|, |memoizable| and |nomemoize|. +%<*mmz> +%\ProvidesPackage{memoize}[2023/10/10 v1.0.0 Fast and flexible externalization] +%%D \module[ +%%D file=t-memoize.tex, +%%D version=1.0.0, +%%D title=Memoize, +%%D subtitle=Fast and flexible externalization, +%%D author=Saso Zivanovic, +%%D date=2023-10-10, +%%D copyright=Saso Zivanovic, +%%D license=LPPL, +%%D ] +%\writestatus{loading}{ConTeXt User Module / memoize} +%\unprotect +%\startmodule[memoize] +%% Package memoize 2023/10/10 v1.0.0 +% +%<*mmzable> +%\ProvidesPackage{memoizable}[2023/10/10 v1.0.0 A programmer's stub for Memoize] +%%D \module[ +%%D file=t-memoizable.tex, +%%D version=1.0.0, +%%D title=Memoizable, +%%D subtitle=A programmer's stub for Memoize, +%%D author=Saso Zivanovic, +%%D date=2023-10-10, +%%D copyright=Saso Zivanovic, +%%D license=LPPL, +%%D ] +%\writestatus{loading}{ConTeXt User Module / memoizable} +%\unprotect +%\startmodule[memoizable] +%% Package memoizable 2023/10/10 v1.0.0 +% +%<*nommz> +%\ProvidesPackage{nomemoize}[2023/10/10 v1.0.0 A no-op stub for Memoize] +%%D \module[ +%%D file=t-nomemoize.tex, +%%D version=1.0.0, +%%D title=Memoize, +%%D subtitle=A no-op stub for Memoize, +%%D author=Saso Zivanovic, +%%D date=2023-10-10, +%%D copyright=Saso Zivanovic, +%%D license=LPPL, +%%D ] +%\writestatus{loading}{ConTeXt User Module / nomemoize} +%\unprotect +%\startmodule[nomemoize] +%% Package nomemoize 2023/10/10 v1.0.0 +% +% +% \paragraph{Required packages} and \hologo{LaTeX}ization of \hologo{plainTeX} +% and \hologo{ConTeXt}. +%<*(mmz,mmzable,nommz)&(plain,context)> +\input miniltx +% +% Some stuff which is ``missing'' in |miniltx|, copied here from |latex.ltx|. +%<*mmz&(plain,context)> +\def\PackageWarning#1#2{{% + \newlinechar`\^^J\def\MessageBreak{^^J\space\space#1: }% + \message{#1: #2}}} +% +% Same as the official definition, but without |\outer|. Needed for record +% file declarations. +%<*mmz&plain> +\def\newtoks{\alloc@5\toks\toksdef\@cclvi} +\def\newwrite{\alloc@7\write\chardef\sixt@@n} +% +% |Nomemoize| has to load |pgfopts| as well, and process package options (right +% away, why not), otherwise \hologo{LaTeX} will complain. +%<*latex> +%\RequirePackage{pgfopts} % pgfkeys-based package options +%\pgfkeys{/memoize/package options/.unknown/.code={}} +%\ProcessPgfPackageOptions{/memoize/package options} +% +% I can't really write any code without |etoolbox| \dots +%<*mmz> +%\RequirePackage{etoolbox} +%\input etoolbox-generic +% Setup the |memoize| namespace in \hologo{LuaTeX}. +\ifdefined\luatexversion + \directlua{memoize = {}} +\fi +% |pdftexcmds.sty| eases access to some PDF primitives, but I cannot manage to +% load it in \hologo{ConTeXt}, even if it's supposed to be a generic +% package. So let's load |pdftexcmds.lua| and copy--paste what we need from +% |pdftexcmds.sty|. +%\RequirePackage{pdftexcmds} +%\input pdftexcmds.sty +%<*context> +\directlua{% + require("pdftexcmds") + tex.enableprimitives('pdf@', {'draftmode'}) +} +\long\def\pdf@mdfivesum#1{% + \directlua{% + oberdiek.pdftexcmds.mdfivesum("\luaescapestring{#1}", "byte")% + }% +}% +\def\pdf@system#1{% + \directlua{% + oberdiek.pdftexcmds.system("\luaescapestring{#1}")% + }% +} +\let\pdf@primitive\primitive +% Lua function |oberdiek.pdftexcmds.filesize| requires the |kpse| library, +% which is not loaded in \hologo{ConTeXt}, +% see \url(https://){github.com/latex3/lua-uni-algos/issues/3}, so we define our +% own filesize function. +\directlua{% + function memoize.filesize(filename) + local filehandle = io.open(filename, "r") + % We can't easily use |~=|, as |~| is an active character, so the |else| + % workaround. + if filehandle == nil then + else + tex.write(filehandle:seek("end")) + io.close(filehandle) + end + end +}% +\def\pdf@filesize#1{% + \directlua{memoize.filesize("\luaescapestring{#1}")}% +} +% +% Take care of some further differences between the engines. +\ifdef\pdftexversion{% +}{% + \def\pdfhorigin{1true in}% + \def\pdfvorigin{1true in}% + \ifdef\XeTeXversion{% + \let\quitvmode\leavevmode + }{% + \ifdef\luatexversion{% + \let\pdfpagewidth\pagewidth + \let\pdfpageheight\pageheight + \def\pdfmajorversion{\pdfvariable majorversion}% + \def\pdfminorversion{\pdfvariable minorversion}% + }{% + \PackageError{memoize}{Support for this TeX engine is not implemented}{}% + }% + }% +} +% +% In \hologo{ConTeXt}, |\unexpanded| means |\protected|, and the usual +% |\unexpanded| is available as |\normalunexpanded|. Option one: use dtx +% guards to produce the correct control sequence. I tried this option. I find +% it ugly, and I keep forgetting to guard. Option two: |\let| an internal +% control sequence, like |\mmz@unexpanded|, to the correct thing, and use that +% all the time. I never tried this, but I find it ugly, too, and I guess I +% would forget to use the new control sequence, anyway. Option three: use +% |\unexpanded| in the |.dtx|, and |sed| through the generated \hologo{ConTeXt} +% files to replace all its occurrences by |\normalunexpanded|. Oh yeah! +% +% +% \paragraph.{Shipout} +% We will next load our own auxiliary package, CollArgs, but before we do +% that, we need to grab |\shipout| in \hologo{plainTeX}. The problem is, +% Memoize needs to hack into the shipout routine, but it has best chances +% of working as intended if it redefines the \emph{primitive} |\shipout|. +% However, CollArgs loads |pgfkeys|, which in turn (and perhaps with no for +% reason) loads |atbegshi|, which redefines |\shipout|. For details, see +% section~\ref{sec:code:extern}. Below, we first check that the current +% meaning of |\shipout| is primitive, and then redefine it. +% +%<*plain> +\def\mmz@regular@shipout{% + \global\advance\mmzRegularPages1\relax + \mmz@primitive@shipout +} +\edef\mmz@temp{\string\shipout}% +\edef\mmz@tempa{\meaning\shipout}% +\ifx\mmz@temp\mmz@tempa + \let\mmz@primitive@shipout\shipout + \let\shipout\mmz@regular@shipout +\else + \PackageError{memoize}{Cannot grab \string\shipout, it is already redefined}{}% +\fi +% +% Our auxiliary package (\MS\ref{sec:ref:collargs}, \S\ref{sec:code:collargs}). +% We also need it in |nomemoize|, to collect manual environments. +%\RequirePackage{advice} +%\input advice +%\input t-advice +% +% +% \paragraph.{Loading order} |memoize| and |nomemoize| are mutually exclusive, +% and |memoizable| must be loaded before either of them. |\mmz@loadstatus|: 1 +% = memoize, 2 = memoizable, 3 = nomemoize. +%<*(plain,context)&(mmz,nommz,mmzable)> +\def\ifmmz@loadstatus#1{% + \ifnum#1=0\csname mmz@loadstatus\endcsname\relax + \expandafter\@firstoftwo + \else + \expandafter\@secondoftwo + \fi +} +% +%<*mmz> +%\@ifpackageloaded{nomemoize}% +%\ifmmz@loadstatus{3}% +{% + \PackageError{memoize}{Cannot load the package, as "nomemoize" is already + loaded. Memoization will NOT be in effect}{Packages "memoize" and + "nomemoize" are mutually exclusive, please load either one or the other.}% + %\pgfkeys{/memoize/package options/.unknown/.code={}} + %\ProcessPgfPackageOptions{/memoize/package options} + \endinput +}{}% +%\@ifpackageloaded{memoizable}% +%\ifmmz@loadstatus{2}% +{% + \PackageError{memoize}{Cannot load the package, as "memoizable" is already + loaded}{Package "memoizable" is loaded by packages which support + memoization. Memoize must be loaded before all such packages. The + compilation log can help you figure out which package loaded "memoizable"; + please move + %"\string\usepackage{memoize}" + %"\string\input memoize" + %"\string\usemodule[memoize]" + before the + %"\string\usepackage" + %"\string\input" + %"\string\usemodule" + of that package.}% + %\pgfkeys{/memoize/package options/.unknown/.code={}} + %\ProcessPgfPackageOptions{/memoize/package options} + \endinput +}{}% +%\ifmmz@loadstatus{1}{\endinput}{}% +%\def\mmz@loadstatus{1}% +% +%<*mmzable> +%\@ifpackageloaded{memoize}% +%\ifmmz@loadstatus{1}% +{\endinput}{} +%\@ifpackageloaded{nomemoize}% +%\ifmmz@loadstatus{3}% +{\endinput}{}% +%\ifmmz@loadstatus{2}{\endinput}{}% +%\def\mmz@loadstatus{2}% +% +%<*nommz> +%\@ifpackageloaded{memoize} +%\ifmmz@loadstatus{1}% +{% + \PackageError{nomemoize}{Cannot load the package, as "memoize" is already + loaded; memoization will remain in effect}{Packages "memoize" and + "nomemoize" are mutually exclusive, please load either one or the other.}% + \endinput }{}% +%\@ifpackageloaded{memoizable}% +%\ifmmz@loadstatus{2}% +{% + \PackageError{nomemoize}{Cannot load the package, as "memoizable" is already + loaded}{Package "memoizable" is loaded by packages which support + memoization. (No)Memoize must be loaded before all such packages. The + compilation log can help you figure out which package loaded + "memoizable"; please move + %"\string\usepackage{nomemoize}" + %"\string\input memoize" + %"\string\usemodule[memoize]" + before the + %"\string\usepackage" + %"\string\input" + %"\string\usemodule" + of that package.}% + \endinput +}{}% +%\ifmmz@loadstatus{3}{\endinput}{}% +%\def\mmz@loadstatus{3}% +% +% +%<*mmz> +% +% \begin{macro}{\filetotoks} +% Read \hologo{TeX} file |#2| into token register |#1| (under the current +% category code regime); |\toksapp| is defined in CollArgs. +\def\filetotoks#1#2{% + \immediate\openin0{#2}% + #1={}% + \loop + \unless\ifeof0 + \read0 to \totoks@temp + % We need the |\expandafter|s for our |\toksapp| macro. + \expandafter\toksapp\expandafter#1\expandafter{\totoks@temp}% + \repeat + \immediate\closein0 +} +% \end{macro} +% +% \paragraph{Other} little things. +\newif\ifmmz@temp +\newtoks\mmz@temptoks +\newbox\mmz@box +\newwrite\mmz@out +% +% \section{The basic configuration} +% \label{sec:code:configuration} +% +% \begin{macro}{\mmzset} +% The user primarily interacts with |Memoize| through the |pgfkeys|-based +% configuration macro |\mmzset|, which executes keys in path |/mmz|. In +% |nomemoize| and |memoizable|, is exists as a no-op. +\def\mmzset#1{\pgfqkeys{/mmz}{#1}\ignorespaces} +% +%\def\mmzset#1{\ignorespaces} +% \end{macro} +% +% \begin{macro}{\mmzset} +% Any |/mmz| keys used outside of |\mmzset| must be declared by this macro +% for |nomemoize| package to work. +%\def\nommzkeys#1{} +%<*nommz,mmzable> +\def\nommzkeys{\pgfqkeys{/mmz}} +\pgfqkeys{/mmz}{.unknown/.code={\pgfkeysdef{\pgfkeyscurrentkey}{}}} +% +% \end{macro} +% +% \begin{key}{enable, disable} +% \begin{macro}{\ifmemoize} +% These keys set \hologo{TeX}-style conditional \cs{ifmemoize}, used as the +% central on/off switch for the functionality of the package --- it is +% inspected in |\Memoize| and by run conditions of automemoization handlers. +% +% If used in the preamble, the effect of these keys is delayed until the +% beginning of the document. The delay is implemented through a special +% style, |begindocument|, which is executed at |begindocument| hook in +% \hologo{LaTeX}; in other formats, the user must invoke it manually +% (\MS\ref{sec:ref:loading}). +% +% |Nomemoize| does not need the keys themselves, but it does need the +% underlying conditional --- which will be always false. +%\newif\ifmemoize +%<*mmz> +\mmzset{% + enable/.style={begindocument/.append code=\memoizetrue}, + disable/.style={begindocument/.append code=\memoizefalse}, + begindocument/.append style={ + enable/.code=\memoizetrue, + disable/.code=\memoizefalse, + }, + % Memoize is enabled at the beginning of the document, unless explicitly + % disabled by the user in the preamble. + enable, +} +% \end{macro} +% \end{key} +% +% \begin{key}{normal,readonly,recompile} +% When Memoize is enabled, it can be in one of three modes +% (\MS\ref{sec:tut:working-on-a-picture}): normal, readonly, and recompile. +% The numeric constants are defined below. The mode is stored in +% |\mmz@mode|, and only matters in |\Memoize| (and +% |\mmz@process@ccmemo|).\footnote{In fact, this code treats anything but 1 +% and 2 as normal.} +\def\mmz@mode@normal{0} +\def\mmz@mode@readonly{1} +\def\mmz@mode@recompile{2} +\let\mmz@mode\mmz@mode@normal +\mmzset{% + normal/.code={\let\mmz@mode\mmz@mode@normal}, + readonly/.code={\let\mmz@mode\mmz@mode@readonly}, + recompile/.code={\let\mmz@mode\mmz@mode@recompile}, +} +% \end{key} +% +% \begin{key}{path, path/relative, path/dir, path/prefix} +% Key |path| executes the given keylist in path |/mmz/path|, to determine the +% full \emph{path prefix} to memo and extern files +% (\MS\ref{sec:tut:memodir},\ref{sec:memos}): |relative|, true by default, +% determines whether the location of these files is relative to the current +% directory; |dir| sets their directory; and |prefix| sets the first, fixed +% part of their basename; the second part containing the MD5 sum(s) is not +% under user control, and neither is the suffix. These subkeys will be +% initialized a bit later, via |no memo dir|. +\mmzset{% + path/.code={\pgfqkeys{/mmz/path}{#1}}, + path/.cd, + relative/.is if=mmz@relativepath, + dir/.store in=\mmz@dir, + dir/.value required, + prefix/.store in=\mmz@prefix, + prefix/.value required, +} +\newif\ifmmz@relativepath +% Key |path| concludes by performing two post-path-setting actions: creating +% the given directory if |mkdir| is in effect, and noting the new path prefix +% in record files (by eventually executing |record/prefix|, which typically +% puts a |\mmzPrefix| line in the |.mmz| file). These actions are the reason +% for having the path setting keys grouped under |path| --- we don't want them +% to be triggered by changes of the individual components of the path. +% Similarly, we don't want them triggered by multiple invocations of |path| in +% the preamble; only the final setting matters, so |path| is only equipped with +% the action-triggering code at the beginning of the document. +\mmzset{% + begindocument/.append style={ + path/.append code=\mmz@maybe@mkmemodir\mmz@record@prefix, + }, + % Consequently, the post-path-setting actions must be triggered manually at + % the beginning of the document. Below, we trigger directory creation; + % |record/prefix| will be called from |record/begin|, which is executed at + % the beginning of the document, so it shouldn't be mentioned here. + begindocument/.append code=\mmz@maybe@mkmemodir, +} +% Define the paths to the memo directory and the prefix. +\def\mmz@dir@path{\ifmmz@relativepath.\fi/\mmz@dir} +\def\mmz@prefix@path{\mmz@dir@path/\mmz@prefix} +% \end{key} +% +% \begin{key}{mkdir, mkdir command} +% Should we create the memo/extern directory if it doesn't exist? And which +% command should we use to create it? Of course, shell escape must be +% properly configured for this to work (\MS\ref{sec:shell-escape}). +% ^^A todo: What about quoting the paths containing spaces on Windows? Will this work? +\mmzset{ + mkdir/.is if=mmz@mkdir, + mkdir command/.code={\def\mmz@mkdir@command##1{#1}}, + mkdir command={mkdir "#1"}, +} +% The underlying conditional \cs{ifmmz@mkdir} is only ever used in +% |\mmz@maybe@mkmemodir| below, which is itself only executed at the end of +% |path| and in |begindocument|. +\newif\ifmmz@mkdir +\def\mmz@maybe@mkmemodir{% + \ifmmz@mkdir + \pdf@system{\mmz@mkdir@command{\mmzOutputDirectory\mmz@dir@path}}% + \fi +} +% \end{key} +% +% \begin{key}{memo dir, no memo dir} +% Shortcuts for two common settings of |path| keys. The default |no memo +% dir| will place the memos and externs in the current directory, prefixed +% with |#1.|, where |#1| defaults to (unquoted) |\jobname|. Key |memo dir| +% places the memos and externs in a dedicated directory, |#1.memo.dir|; the +% filenames themselves have no prefix. Furthermore, |memo dir| triggers the +% creation of the directory. +\mmzset{% + memo dir/.style={ + mkdir, + path={ + relative, + dir={#1.memo.dir}, + prefix={}, + }, + }, + memo dir/.default=\mmzUnquote\jobname, + no memo dir/.style={ + mkdir=false, + path={ + relative, + dir={}, + prefix={#1.}, + }, + }, + no memo dir/.default=\mmzUnquote\jobname, + no memo dir, +} +% \end{key} +% +% \begin{macro}{\mmzUnquote} +% If the expanded argument is surrounded by double quotes, remove them. This +% relies on |#1| containing no quotes other than the potential surrounding +% quotes, which should be the case when applying the macro to |\jobname|. An +% empty |#1| is dealt with correctly, even if |\jobname| can hardly ever be +% empty (needs |openout_any=a|). +% +% We use this macro when we are passing a filename constructed from +% |\jobname| to external programs. +\def\mmzUnquote#1{\expanded{\noexpand\mmz@unquote#1}\mmz@unquote@end} +\def\mmz@unquote#1{% + \ifx\mmz@unquote@end#1% + \else + \ifx"#1% + \expandafter\expandafter\expandafter\mmz@unquote@quotes + \else + \expandafter\expandafter\expandafter\mmz@unquote@noquotes + \expandafter\expandafter\expandafter#1% + \fi + \fi +} +\def\mmz@unquote@quotes#1"\mmz@unquote@end{#1} +\def\mmz@unquote@noquotes#1\mmz@unquote@end{#1} +% \end{macro} +% +% \begin{key}{ignore spaces} +% The underlying conditional will be inspected by automemoization handlers, +% to maybe put |\ignorespaces| after the invocation of the handler. +\newif\ifmmz@ignorespaces +\mmzset{ + ignore spaces/.is if=mmz@ignorespaces, +} +% \end{key} +% +% \begin{key}{verbatim, verb, no verbatim} +% These keys are tricky. For one, there's |verbatim|, which sets all +% characters' category codes to other, and there's |verb|, which leaves +% braces untouched (well, honestly, it redefines them). But Memoize itself +% doesn't really care about this detail --- it only uses the underlying +% conditional \cs{ifmmz@verbatim}. It is CollArgs which cares about the +% difference between the ``long'' and the ``short'' verbatim, so we need to +% tell it about it. That's why the verbatim options ``append themselves'' to +% |\mmzRawCollectorOptions|, which is later passed on to +% |\CollectArgumentsRaw| as a part of its optional argument. +\newif\ifmmz@verbatim +\def\mmzRawCollectorOptions{} +\mmzset{ + verbatim/.code={% + \def\mmzRawCollectorOptions{\collargsVerbatim}% + \mmz@verbatimtrue + }, + verb/.code={% + \def\mmzRawCollectorOptions{\collargsVerb}% + \mmz@verbatimtrue + }, + no verbatim/.code={% + \def\mmzRawCollectorOptions{\collargsNoVerbatim}% + \mmz@verbatimfalse + }, +} +% \end{key} +% \section{Memoization} +% \label{sec:code:memoization} +% +% \subsection{Manual memoization} +% \label{sec:code:memoization:manual} +% +% \begin{macro}{\mmz} +% The core of this macro will be a simple invocation of |\Memoize|, but to +% get there, we have to collect the optional argument carefully, because we +% might have to collect the memoized code verbatim. +\protected\def\mmz{\futurelet\mmz@temp\mmz@i} +\def\mmz@i{% + % Anyone who wants to call |\Memoize| must open a group, because |\Memoize| + % will close a group. + \begingroup + % As the optional argument occurs after a control sequence (|\mmz|), any + % spaces were consumed and we can immediately test for the opening bracket. + \ifx\mmz@temp[%] + \def\mmz@verbatim@fix{}% + \expandafter\mmz@ii + \else + % If there was no optional argument, the opening brace (or the unlikely + % single token) of our mandatory argument is already tokenized. If we are + % requested to memoize in a verbatim mode, this non-verbatim tokenization + % was wrong, so we will use option |\collargsFixFromNoVerbatim| to ask + % CollArgs to fix the situation. (|\mmz@verbatim@fix| will only be used in + % the verbatim mode.) + \def\mmz@verbatim@fix{\noexpand\collargsFixFromNoVerbatim}% + % No optional argument, so we can skip |\mmz@ii|. + \expandafter\mmz@iii + \fi +} +\def\mmz@ii[#1]{% + % Apply the options given in the optional argument. + \mmzset{#1}% + \mmz@iii +} +\def\mmz@iii{% + % In the non-verbatim mode, we avoid collecting the single mandatory argument + % using |\CollectArguments|. + \ifmmz@verbatim + \expandafter\mmz@do@verbatim + \else + \expandafter\mmz@do + \fi +} +% This macro grabs the mandatory argument of |\mmz| and calls |\Memoize|. +\long\def\mmz@do#1{% + \Memoize{#1}{#1}% +}% +% +% The following macro uses |\CollectArgumentsRaw| of package CollArgs +% (\S\ref{sec:code:collargs}) to grab the argument verbatim; the appropriate +% verbatim mode triggering raw option was put in |\mmzRawCollectorOptions| by +% key |verb(atim)|. The macro also |\mmz@verbatim@fix| contains the potential +% request for a category code fix (\S\ref{sec:code:collargs:fix}). +\def\mmz@do@verbatim#1{% + \expanded{% + \noexpand\CollectArgumentsRaw{% + \noexpand\collargsCaller{\noexpand\mmz}% + \expandonce\mmzRawCollectorOptions + \mmz@verbatim@fix + }% + }{+m}\mmz@do +} +% \end{macro} +% +% \begin{environment}{memoize} +% The definition of the manual memoization environment proceeds along the +% same lines as the definition of |\mmz|, except that we also have to +% implement space-trimming, and that we will collect the environment using +% |\CollectArguments| in both the verbatim and the non-verbatim and mode. +% +% We define the \hologo{LaTeX}, \hologo{plainTeX} and \hologo{ConTeXt} +% environments in parallel. The definition of the \hologo{plainTeX} and +% \hologo{ConTeXt} version is complicated by the fact that space-trimming is +% affected by the presence vs.\ absence of the optional argument (for +% purposes of space-trimming, it counts as present even if it is empty). +%<*latex> +% We define the \hologo{LaTeX} environment using |\newenvironment|, which +% kindly grabs any spaces in front of the optional argument, if it exists --- +% and if doesn't, we want to trim spaces at the beginning of the environment +% body anyway. +\newenvironment{memoize}[1][\mmz@noarg]{% + % We close the environment right away. We'll collect the environment body, + % complete with the end-tag, so we have to reintroduce the end-tag somewhere. + % Another place would be after the invocation of |\Memoize|, but that would + % put memoization into a double group and |\mmzAfterMemoization| would not work. + \end{memoize}% + % % We open the group which will be closed by |\Memoize|. + \begingroup + % As with |\mmz| above, if there was no optional argument, we have to ask + % Collargs for a fix. The difference is that, as we have collected the + % optional argument via |\newcommand|, we have to test for its presence in a + % roundabout way. + \def\mmz@temp{#1}% + \ifx\mmz@temp\mmz@noarg + \def\mmz@verbatim@fix{\noexpand\collargsFixFromNoVerbatim}% + \else + \def\mmz@verbatim@fix{}% + \mmzset{#1}% + \fi + \mmz@env@iii +}{} +\def\mmz@noarg{\mmz@noarg} +% +%\def\memoize{% +%\def\startmemoize{% +%<*plain,context> + \begingroup + % In \hologo{plainTeX} and \hologo{ConTeXt}, we don't have to worry about any + % spaces in front of the optional argument, as the environments are opened by + % a control sequence. + \futurelet\mmz@temp\mmz@env@i +} +\def\mmz@env@i{% + \ifx\mmz@temp[%] + \def\mmz@verbatim@fix{}% + \expandafter\mmz@env@ii + \else + \def\mmz@verbatim@fix{\noexpand\collargsFixFromNoVerbatim}% + \expandafter\mmz@env@iii + \fi +} +\def\mmz@env@ii[#1]{% + \mmzset{#1}% + \mmz@env@iii +} +% +\def\mmz@env@iii{% + \long\edef\mmz@do##1{% + % |\unskip| will ``trim'' spaces at the end of the environment body. + \noexpand\Memoize{##1}{##1\unskip}% + }% + \expanded{% + \noexpand\CollectArgumentsRaw{% + % |\CollectArgumentsRaw| will adapt the caller to the format automatically. + \noexpand\collargsCaller{memoize}% + % |verb(atim)| is in here if it was requested. + \expandonce\mmzRawCollectorOptions + % The category code fix, if needed. + \ifmmz@verbatim\mmz@verbatim@fix\fi + }% + % Spaces at the beginning of the environment body are trimmed by setting + % the first argument to |!t| and disappearing it with + % |\collargsAppendPostwrap{}|; note that this removes any number of space + % tokens. |\CollectArgumentsRaw| automatically adapts the argument type + % |b| to the format. + % + }{&&{\collargsAppendPostwrap{}}!t{ }+b{memoize}}{\mmz@do}% +}% +% +% \end{environment} +% +% \begin{macro}{\nommz} +% We throw away the optional argument if present, and replace the opening +% brace with begin-group plus |\memoizefalse|. This way, the ``argument'' of +% |\nommz| will be processed in a group (with Memoize disabled) and even the +% verbatim code will work because the ``argument'' will not have been +% tokenized. +% +% As a user command, |\nommz| has to make it into package |nomemoize| as +% well, and we'll |\let| |\mmz| equal it there; it is not needed in +% |mmzable|. +%<*mmz,nommz> +\protected\def\nommz#1#{% + \afterassignment\nommz@i + \let\mmz@temp +} +\def\nommz@i{% + \bgroup + \memoizefalse +} +%\let\mmz\nommz +% \end{macro} +% +% \begin{environment}{nomemoize} +% We throw away the optional argument and take care of the spaces at the +% beginning and at the end of the body. +%<*latex> +\newenvironment{nomemoize}[1][]{% + \memoizefalse + \ignorespaces +}{% + \unskip +} +% +%<*plain,context> +%\def\nomemoize{% +%\def\startnomemoize{% +% Start a group to delimit |\memoizefalse|. + \begingroup + \memoizefalse + \futurelet\mmz@temp\nommz@env@i +} +\def\nommz@env@i{% + \ifx\mmz@temp[%] + \expandafter\nommz@env@ii + % No optional argument, no problems with spaces. + \fi +} +\def\nommz@env@ii[#1]{% + \ignorespaces +} +%\def\endnomemoize{% +%\def\stopnomemoize{% + \endgroup + \unskip +} +% +%<*nommz> +%\let\memoize\nomemoize +%\let\endmemoize\endnomemoize +%\let\startmemoize\startnomemoize +%\let\stopmemoize\stopnomemoize +% +% \end{environment} +% +% \subsection{The memoization process} +% \label{sec:code:memoization-process} +% +% \begin{macro}{\ifmemoizing} +% This conditional is set to true when we start memoization (but not when +% we start regular compilation or utilization); it should never be set +% anywhere else. It is checked by |\Memoize| to prevent nested +% memoizations, deployed in advice run conditions set by |run only if +% memoizing|, etc. +\newif\ifmemoizing +% \end{macro} +% +% \begin{macro}{\ifinmemoize} +% This conditional is set to true when we start either memoization or +% regular compilation (but not when we start utilization); it should never +% be set anywhere else. It is deployed in the default advice run conditions, +% making sure that automemoized commands are not handled even when we're +% regularly compiling some code submitted to memoization. +\newif\ifinmemoize +% \end{macro} +% +% \begin{macro}{\mmz@maybe@scantokens} +% An auxiliary macro which rescans the given +% code using |\scantokens| if the verbatim mode is active. We also need it +% in NoMemoize, to properly grab verbatim manually memoized code. +% +%<*mmz> +\def\mmz@maybe@scantokens{% + \ifmmz@verbatim + \expandafter\mmz@scantokens + \else + \expandafter\@firstofone + \fi +} +% Without |\newlinechar=13|, |\scantokens| would see receive the entire +% argument as one long line --- but it would not \emph{see} the entire +% argument, but only up to the first newline character, effectively losing most +% of the tokens. (We need to manually save and restore |\newlinechar| because +% we don't want to execute the memoized code in yet another group.) +\long\def\mmz@scantokens#1{% + \expanded{% + \newlinechar=13 + \unexpanded{\scantokens{#1\endinput}}% + \newlinechar=\the\newlinechar + }% +} +% \end{macro} +% +% \begin{macro}{\Memoize} +% Memoization is invoked by executing |\Memoize|. This macro is a decision +% hub. It test for the existence of the memos and externs associated with +% the memoized code, and takes the appropriate action (memoization: +% |\mmz@memoize|; regular compilation: |\mmz@compile|, utilization: +% |\mmz@process@cmemo| plus |\mmz@process@ccmemo| plus further complications) +% depending on the memoization mode (normal, readonly, recompile). Note that +% one should open a \hologo{TeX} group prior to executing |\Memoize|, because +% |\Memoize| will close a group (\MS\ref{sec:Memoize}). +% +% |\Memoize| takes two arguments, which contain two potentially different +% versions of the code submitted to memoization: |#1| contains the code which +% \meta{code MD5 sum} is computed off of, while |#2| contains the code which +% is actually executed during memoization and regular compilation. The +% arguments will contain the same code in the case of manual memoization, but +% they will differ in the case of automemoization, where the executable code +% will typically prefixed by |\AdviceOriginal|. As the two +% codes will be used not only by |\Memoize| but also by macros called from +% |\Memoize|, |\Memoize| stores them into dedicated toks registers, declared +% below. +\newtoks\mmz@mdfive@source +\newtoks\mmz@exec@source +% Finally, the definition of the macro. In package NoMemoize, we should simply +% execute the code in the second argument. But in Memoize, we have work to do. +\let\Memoize\@secondoftwo +\long\def\Memoize#1#2{% + % We store the first argument into token register |\mmz@mdfive@source| + % because we might have to include it in tracing info (when |trace| is in + % effect), or paste it into the c-memo (depending on |include source in + % cmemo|). + \mmz@mdfive@source{#1}% + % We store the executable code in |\mmz@exec@source|. In the verbatim mode, + % the code will have to be rescanned. This is implemented by + % |\mmz@maybe@scantokens|, and we wrap the code into this macro right away, + % once and for all. Even more, we pre-expand |\mmz@maybe@scantokens| (three + % times), effectively applying the current \cs{ifmmz@verbatim} and + % eliminating the need to save and restore this conditional in + % |\mmz@compile|, which (regularly) compiles the code \emph{after} closing + % the |\Memoize| group --- after this pre-expansion, |\mmz@exec@source| will + % contain either |\mmz@scantokens{...}| or |\@firstofone{...}|. + \expandafter\expandafter\expandafter\expandafter + \expandafter\expandafter\expandafter + \mmz@exec@source + \expandafter\expandafter\expandafter\expandafter + \expandafter\expandafter\expandafter + {% + \mmz@maybe@scantokens{#2}% + }% + \mmz@trace@Memoize + % In most branches below, we end up with regular compilation, so let this be + % the default action. + \let\mmz@action\mmz@compile + % If Memoize is disabled, or if memoization is currently taking place, we + % will perform a regular compilation. + \ifmemoizing + \else + \ifmemoize + % Compute \meta{code md5sum} off of the first argument, and globally + % store it into |\mmz@code@mdfivesum| --- globally, because we need it in + % utilization to include externs, but the |\Memoize| group is closed (by + % |\mmzMemo|) while inputting the cc-memo. + \xdef\mmz@code@mdfivesum{\pdf@mdfivesum{\the\mmz@mdfive@source}}% + \mmz@trace@code@mdfive + % Recompile mode forces memoization. + \ifnum\mmz@mode=\mmz@mode@recompile\relax + \ifnum\pdf@draftmode=0 + \let\mmz@action\mmz@memoize + \fi + \else + % In the normal and the readonly mode, we try to utilize the memos. The + % c-memo comes first. If the c-memo does not exist (or if something is + % wrong with it), |\mmz@process@cmemo| (defined in + % \S\ref{sec:code:c-memo}) will set |\ifmmz@abort| to true. It might + % also set |\ifmmzUnmemoizable| which means we should compile normally + % regardless of the mode. + \mmz@process@cmemo + \ifmmzUnmemoizable + \mmz@trace@cmemo@unmemoizable + \else + \ifmmz@abort + % If there is no c-memo, or it is invalid, we memoize, unless the + % read-only mode is in effect. + \mmz@trace@process@cmemo@fail + \ifnum\mmz@mode=\mmz@mode@readonly\relax + \else + \ifnum\pdf@draftmode=0 + \let\mmz@action\mmz@memoize + \fi + \fi + \else + \mmz@trace@process@cmemo@ok + % If the c-memo was fine, the formal action decided upon is to try + % utilizing the cc-memo. If it exists and everything is fine with + % it, |\mmz@process@ccmemo| (defined in + % section~\ref{sec:code:cc-memo}) will utilize it, i.e.\ the core + % of the cc-memo (the part following |\mmzMemo|) will be executed + % (typically including the single extern). Otherwise, + % |\mmz@process@ccmemo| will trigger either memoization (in the + % normal mode) or regular compilation (in the readonly mode). This + % final decision is left to |\mmz@process@ccmemo| because if we + % made it here, the code would get complicated, as the cc-memo must + % be processed outside the |\Memoize| group and all the + % conditionals in this macro. + \let\mmz@action\mmz@process@ccmemo + \fi + \fi + \fi + \fi + \fi + \mmz@action +} +% \end{macro} +% +% \begin{macro}{\mmz@compile} +% This macro performs regular compilation --- this is signalled to the +% memoized code and the memoization driver by setting \cs{ifinmemoize} to +% true for the duration of the compilation; \cs{ifmemoizing} is not touched. +% The group opened prior to the invocation of |\Memoize| is closed before +% executing the code in |\mmz@exec@source|, so that compiling the code has +% the same local effect as if was not submitted to memoization; it is closing +% this group early which complicates the restoration of \cs{ifinmemoize} at +% the end of compilation. Note that |\mmz@exec@source| is already set to +% properly deal with the current verbatim mode, so any further inspection of +% \cs{ifmmz@verbatim} is unnecessary; the same goes for +% \cs{ifmmz@ignorespaces}, which was (or at least should be) taken care of by +% whoever called |\Memoize|. +\def\mmz@compile{% + \mmz@trace@compile + \expanded{% + \endgroup + \noexpand\inmemoizetrue + \the\mmz@exec@source + \ifinmemoize\noexpand\inmemoizetrue\else\noexpand\inmemoizefalse\fi + }% +} +% \end{macro} +% \begin{luafun}{abortOnError} +% \begin{macro}{\mmz@lua@atbeginmemoization,\mmz@lua@atendmemoization} +% In \hologo{LuaTeX}, we can whether an error occurred during memoization, +% and abort if it did. (We're going through |memoize.abort|, because +% |tex.print| does not seem to work during error handling.) We omit all +% this in \hologo{ConTeXt}, as it appears to stop on any error? +%<*!context> +\ifdefined\luatexversion + \directlua{% + luatexbase.add_to_callback( + "show_error_message", + function() + memoize.abort = true + texio.write_nl(status.lasterrorstring) + end, + "Abort memoization on error" + ) + }% + \def\mmz@lua@atbeginmemoization{% + \directlua{memoize.abort = false}% + }% + \def\mmz@lua@atendmemoization{% + \directlua{% + if memoize.abort then + tex.print("\noexpand\\mmzAbort") + end + }% + }% +\else +% + \let\mmz@lua@atbeginmemoization\relax + \let\mmz@lua@atendmemoization\relax +%\fi +% \end{macro} +% \end{luafun} +% \begin{macro}{\mmz@memoize} +% This macro performs memoization --- this is signalled to the memoized code +% and the memoization driver by setting both \cs{ifinmemoize} and +% \cs{ifinmemoizing} to true. +\def\mmz@memoize{% + \mmz@trace@memoize + \memoizingtrue + \inmemoizetrue + % Initialize the various macros and registers used in memoization (to be + % described below, or later). Note that most of these are global, as they + % might be adjusted arbitrarily deep within the memoized code. + \edef\memoizinggrouplevel{\the\currentgrouplevel}% + \global\mmz@abortfalse + \global\mmzUnmemoizablefalse + \global\mmz@seq 0 + \global\setbox\mmz@tbe@box\vbox{}% + \global\mmz@ccmemo@resources{}% + \global\mmzCMemo{}% + \global\mmzCCMemo{}% + \global\mmzContextExtra{}% + \gdef\mmzAtEndMemoizationExtra{}% + \gdef\mmzAfterMemoizationExtra{}% + \mmz@lua@atbeginmemoization + % Execute the pre-memoization hook, the memoized code (wrapped in the + % driver), and the post-memoization hook. + \mmzAtBeginMemoization + \mmzDriver{\the\mmz@exec@source}% + \mmzAtEndMemoization + \mmzAtEndMemoizationExtra + \mmz@lua@atendmemoization + \ifmmzUnmemoizable + % To permanently prevent memoization, we have to write down the c-memo + % (containing |\mmzUnmemoizabletrue|). We don't need the extra context in + % this case. + \global\mmzContextExtra{}% + \gtoksapp\mmzCMemo{\global\mmzUnmemoizabletrue}% + \mmz@write@cmemo + \mmz@trace@endmemoize@unmemoizable + \PackageWarning{memoize}{Marking this code as unmemoizable}% + \else + \ifmmz@abort + % If memoization was aborted, we create an empty c-memo, to make sure that + % no leftover c-memo tricks Memoize into thinking that the code was + % successfully memoized. + \mmz@trace@endmemoize@aborted + \PackageWarning{memoize}{Memoization was aborted}% + \mmz@write@empty@cmemo + \else + % If memoization was not aborted, we compute the \meta{context md5sum}, + % open and write out the memos, and shipout the externs (as pages into the + % document). + \mmz@compute@context@mdfivesum + \mmz@write@cmemo + \mmz@write@ccmemo + \mmz@shipout@externs + \mmz@trace@endmemoize@ok + \fi + \fi + % After closing the group, we execute the final, after-memoization hook (we + % pre-expand the regular macro; the extra macro was assigned to globally). In + % the after-memoization code, |\mmzIncludeExtern| points to a macro which can + % include the extern from |\mmz@tbe@box|, which makes it possible to typeset + % the extern by dropping the contents of |\mmzCCMemo| into this hook --- but + % note that this will only work if |\ifmmzkeepexterns| was in effect at the + % end of memoization. + \expandafter\endgroup + \expandafter\let + \expandafter\mmzIncludeExtern\expandafter\mmz@include@extern@from@tbe@box + \mmzAfterMemoization + \mmzAfterMemoizationExtra +} +% \end{macro} +% +% \begin{macro}{\memoizinggrouplevel} +% This macro stores the group level at the beginning of memoization. It is +% deployed by |\IfMemoizing|, normally used by integrated drivers. +\def\memoizinggrouplevel{-1}% +% \end{macro} +% +% \begin{macro}{\mmzAbort} +% Memoized code may execute this macro to abort memoization. +\def\mmzAbort{\global\mmz@aborttrue} +% \end{macro} +% +% \begin{macro}{\ifmmz@abort} +% This conditional serves as a signal that something went wrong during +% memoization (where it is set to true by |\mmzAbort|), or c(c)-memo +% processing. The assignment to this conditional should always be global +% (because it may be set during memoization). +\newif\ifmmz@abort +% \end{macro} +% +% \begin{macro}{\mmzUnmemoizable} +% Memoized code may execute |\mmzUnmemoizable| to abort memoization and mark +% (in the c-memo) that memoization should never be attempted again. The +% c-memo is composed by |\mmz@memoize|. +\def\mmzUnmemoizable{\global\mmzUnmemoizabletrue} +% \end{macro} +% +% \begin{macro}{\ifmmzUnmemoizable} +% This conditional serves as a signal that the code should never be +% memoized. It can be set (a) during memoization (that's why it should be +% assigned globally), after which it is inspected by |\mmz@memoize|, and +% (b) from the c-memo, in which case it is inspected by |\Memoize|. +\newif\ifmmzUnmemoizable +% \end{macro} +% +% \begin{macro}{\mmzAtBeginMemoization,\mmzAtEndMemoization,\mmzAfterMemoization} +% \begin{key}{at begin memoization, at end memoization, after memoization} +% The memoization hooks and their keys. The hook macros may be set either +% before or during memoization. In the former case, one should modify the +% primary macro (|\mmzAtBeginMemoization|, |\mmzAtEndMemoization|, +% |\mmzAfterMemoization|), and the assignment should be local. In the +% latter case, one should modify the extra macro +% (|\mmzAtEndMemoizationExtra|, |\mmzAfterMemoizationExtra|; there is no +% |\mmzAtBeginMemoizationExtra|), and the assignment should be global. The +% keys automatically adapt to the situation, by appending either to the +% primary or the the extra macro; if |at begin memoization| is used during +% memoization, the given code is executed immediately. We will use this +% ``extra'' approach and the auto-adapting keys for other options, like +% |context|, as well. +\def\mmzAtBeginMemoization{} +\def\mmzAtEndMemoization{} +\def\mmzAfterMemoization{} +\mmzset{ + at begin memoization/.code={% + \ifmemoizing + \expandafter\@firstofone + \else + \expandafter\appto\expandafter\mmzAtBeginMemoization + \fi + {#1}% + }, + at end memoization/.code={% + \ifmemoizing + \expandafter\gappto\expandafter\mmzAtEndMemoizationExtra + \else + \expandafter\appto\expandafter\mmzAtEndMemoization + \fi + {#1}% + }, + after memoization/.code={% + \ifmemoizing + \expandafter\gappto\expandafter\mmzAfterMemoizationExtra + \else + \expandafter\appto\expandafter\mmzAfterMemoization + \fi + {#1}% + }, +} +% \end{key} +% \end{macro} +% +% \begin{key}{driver} +% This key sets the (formal) memoization driver. The function of the driver +% is to produce the memos and externs while executing the submitted code. +\mmzset{ + driver/.store in=\mmzDriver, + driver=\mmzSingleExternDriver, +} +% \end{key} +% +% \begin{macro}{\ifmmzkeepexterns} +% This conditional causes Memoize not to empty out |\mmz@tbe@box|, holding +% the externs collected during memoization, while shipping them out. +\newif\ifmmzkeepexterns +% \end{macro} +% +% \begin{macro}{\mmzSingleExternDriver} +% The default memoization driver externalizes the submitted code. It always +% produces exactly one extern, and including the extern will be the only +% effect of inputting the cc-memo (unless the memoized code contained some +% commands, like |\label|, which added extra instructions to the cc-memo.) +% The macro (i) adds |\quitvmode| to the cc-memo, if we're capturing into a +% horizontal box, and it puts it to the very front, so that it comes before +% any |\label| and |\index| replications, guaranteeing (hopefully) that they +% refer to the correct page; (ii) takes the code and typesets it in a box +% (|\mmz@box|); (iii) submits the box for externalization; (iv) adds the +% extern-inclusion code to the cc-memo, and (v) puts the box into the +% document (again prefixing it with |\quitvmode| if necessary). (The listing +% region markers help us present this code in the manual.) +% \begin{listingregion}{single-extern-driver.tex} +\long\def\mmzSingleExternDriver#1{% + \xtoksapp\mmzCCMemo{\mmz@maybe@quitvmode}% + \setbox\mmz@box\mmz@capture{#1}% + \mmzExternalizeBox\mmz@box\mmz@temptoks + \xtoksapp\mmzCCMemo{\the\mmz@temptoks}% + \mmz@maybe@quitvmode\box\mmz@box +} +% \end{listingregion} +% \end{macro} +% +% \begin{key}{capture} +% The default memoization driver uses |\mmz@capture| and +% |\mmz@maybe@quitvmode|, which are set by this key. |\mmz@maybe@quitvmode| +% will be expanded, but for \hologo{XeTeX}, we have defined |\quitvmode| as a +% synonym for |\leavevmode|, which is a macro rather than a primitive, so we +% have to prevent its expansion in that case. It is easiest to just add +% |\noexpand|, regardless of the engine used. +\mmzset{ + capture/.is choice, + capture/hbox/.code={% + \let\mmz@capture\hbox + \def\mmz@maybe@quitvmode{\noexpand\quitvmode}% + }, + capture/vbox/.code={% + \let\mmz@capture\vbox + \def\mmz@maybe@quitvmode{}% + }, + capture=hbox, +} +% \end{key} +% The memoized code may be memoization-aware; in such a case, we say that the +% driver is \emph{integrated} into the code. Code containing an integrated +% driver must take care to execute it only when memoizing, and not during a +% regular compilation. The following key and macro can help here, see +% \MS\ref{sec:memoization-complex-single-driver} for details. +% +% \begin{mmzautokey}{integrated driver} +% This is an advice key, residing in |/mmz/auto|. Given \meta{suffix} as the +% only argument, it declares conditional \cs{ifmemoizing}\meta{suffix}, and +% sets the driver for the automemoized command to a macro which sets this +% conditional to true. The declared conditional is \emph{internal} and +% should not be used directly, but only via |\IfMemoizing| --- because it +% will not be declared when package NoMemoize or only Memoizable is loaded. +\mmzset{ + auto/integrated driver/.style={ + after setup={\expandafter\newif\csname ifmmz@memoizing#1\endcsname}, + driver/.expand once={% + \csname mmz@memoizing#1true\endcsname + % Without this, we would introduce an extra group around the memoized + % code. + \@firstofone + }% + }, +} +% \end{mmzautokey} +% +% \begin{macro}{\IfMemoizing} +% Without the optional argument, the condition is satisfied when the internal +% conditional \cs{ifmemoizing}\meta{suffix}, declared by |integrated driver|, +% is true. With the optional argument \meta{offset}, the current group level +% must additionally match the memoizing group level, modulo \meta{offset} --- +% this makes sure that the conditional comes out as false in a regular +% compilation embedded in a memoization. +\newcommand\IfMemoizing[2][\mmz@Ifmemoizing@nogrouplevel]{%>\fi +\csname ifmmz@memoizing#2\endcsname%>\if + % One |\relax| is for the |\numexpr|, another for |\ifnum|. Complications + % arise when |#1| is the optional argument default (defined below). In + % that case, the content of |\mmz@Ifmemoizing@nogrouplevel| closes off the + % |\ifnum| conditional (with both the true and the false branch empty), and + % opens up a new one, |\iftrue|. Effectively, we're not testing for the + % group level match. + \ifnum\currentgrouplevel=\the\numexpr\memoizinggrouplevel+#1\relax\relax + \expandafter\expandafter\expandafter\@firstoftwo + \else + \expandafter\expandafter\expandafter\@secondoftwo + \fi +\else + \expandafter\@secondoftwo +\fi +} +\def\mmz@Ifmemoizing@nogrouplevel{0\relax\relax\fi\iftrue} +% \end{macro} +% +% \paragraph.{Tracing} We populate the hooks which send the tracing info to the +% terminal. +\def\mmz@trace#1{\immediate\write16{[tracing memoize] #1}} +\def\mmz@trace@context{\mmz@trace{\space\space + Context: "\expandonce{\mmz@context@key}" --> \mmz@context@mdfivesum}} +\def\mmz@trace@Memoize@on{% + \mmz@trace{% + Entering \noexpand\Memoize (% + \ifmemoize enabled\else disabled\fi, + \ifnum\mmz@mode=\mmz@mode@recompile recompile\fi + \ifnum\mmz@mode=\mmz@mode@readonly readonly\fi + \ifnum\mmz@mode=\mmz@mode@normal normal\fi + \space mode) on line \the\inputlineno + }% + \mmz@trace{\space\space Code: \the\mmz@mdfive@source}% +} +\def\mmz@trace@code@mdfive@on{\mmz@trace{\space\space + Code md5sum: \mmz@code@mdfivesum}} +\def\mmz@trace@compile@on{\mmz@trace{\space\space Compiling}} +\def\mmz@trace@memoize@on{\mmz@trace{\space\space Memoizing}} +\def\mmz@trace@endmemoize@ok@on{\mmz@trace{\space\space + Memoization completed}}% +\def\mmz@trace@endmemoize@aborted@on{\mmz@trace{\space\space + Memoization was aborted}} +\def\mmz@trace@endmemoize@unmemoizable@on{\mmz@trace{\space\space + Marking this code as unmemoizable}} +% No need for |\mmz@trace@endmemoize@fail|, as abortion results in a package +% warning anyway. +\def\mmz@trace@process@cmemo@on{\mmz@trace{\space\space + Attempting to utilize c-memo \mmz@cmemo@path}} +\def\mmz@trace@process@no@cmemo@on{\mmz@trace{\space\space + C-memo does not exist}} +\def\mmz@trace@process@cmemo@ok@on{\mmz@trace{\space\space + C-memo was processed successfully}\mmz@trace@context} +\def\mmz@trace@process@cmemo@fail@on{\mmz@trace{\space\space + C-memo input failed}} +\def\mmz@trace@cmemo@unmemoizable@on{\mmz@trace{\space\space + This code was marked as unmemoizable}} +\def\mmz@trace@process@ccmemo@on{\mmz@trace{\space\space + Attempting to utilize cc-memo \mmz@ccmemo@path\space + (\ifmmz@direct@ccmemo@input\else in\fi direct input)}} +\def\mmz@trace@resource@on#1{\mmz@trace{\space\space + Extern file does not exist: #1}} +\def\mmz@trace@process@ccmemo@ok@on{% + \mmz@trace{\space\space Utilization successful}} +\def\mmz@trace@process@no@ccmemo@on{% + \mmz@trace{\space\space CC-memo does not exist}} +\def\mmz@trace@process@ccmemo@fail@on{% + \mmz@trace{\space\space Cc-memo input failed}} +% +% \begin{key}{tracing} +% \begin{macro}{\mmzTracingOn,\mmzTracingOff} +% The user interface for switching the tracing on and off; initially, it is +% off. Note that there is no underlying conditional. The off version +% simply |\let|s all the tracing hooks to |\relax|, so that the overhead of +% having the tracing functionality available is negligible. +\mmzset{% + trace/.is choice, + trace/.default=true, + trace/true/.code=\mmzTracingOn, + trace/false/.code=\mmzTracingOff, +} +\def\mmzTracingOn{% + \let\mmz@trace@Memoize\mmz@trace@Memoize@on + \let\mmz@trace@code@mdfive\mmz@trace@code@mdfive@on + \let\mmz@trace@compile\mmz@trace@compile@on + \let\mmz@trace@memoize\mmz@trace@memoize@on + \let\mmz@trace@process@cmemo\mmz@trace@process@cmemo@on + \let\mmz@trace@endmemoize@ok\mmz@trace@endmemoize@ok@on + \let\mmz@trace@endmemoize@unmemoizable\mmz@trace@endmemoize@unmemoizable@on + \let\mmz@trace@endmemoize@aborted\mmz@trace@endmemoize@aborted@on + \let\mmz@trace@process@cmemo\mmz@trace@process@cmemo@on + \let\mmz@trace@process@cmemo@ok\mmz@trace@process@cmemo@ok@on + \let\mmz@trace@process@no@cmemo\mmz@trace@process@no@cmemo@on + \let\mmz@trace@process@cmemo@fail\mmz@trace@process@cmemo@fail@on + \let\mmz@trace@cmemo@unmemoizable\mmz@trace@cmemo@unmemoizable@on + \let\mmz@trace@process@ccmemo\mmz@trace@process@ccmemo@on + \let\mmz@trace@resource\mmz@trace@resource@on + \let\mmz@trace@process@ccmemo@ok\mmz@trace@process@ccmemo@ok@on + \let\mmz@trace@process@no@ccmemo\mmz@trace@process@no@ccmemo@on + \let\mmz@trace@process@ccmemo@fail\mmz@trace@process@ccmemo@fail@on +} +\def\mmzTracingOff{% + \let\mmz@trace@Memoize\relax + \let\mmz@trace@code@mdfive\relax + \let\mmz@trace@compile\relax + \let\mmz@trace@memoize\relax + \let\mmz@trace@process@cmemo\relax + \let\mmz@trace@endmemoize@ok\relax + \let\mmz@trace@endmemoize@unmemoizable\relax + \let\mmz@trace@endmemoize@aborted\relax + \let\mmz@trace@process@cmemo\relax + \let\mmz@trace@process@cmemo@ok\relax + \let\mmz@trace@process@no@cmemo\relax + \let\mmz@trace@process@cmemo@fail\relax + \let\mmz@trace@cmemo@unmemoizable\relax + \let\mmz@trace@process@ccmemo\relax + \let\mmz@trace@resource\@gobble + \let\mmz@trace@process@ccmemo@ok\relax + \let\mmz@trace@process@no@ccmemo\relax + \let\mmz@trace@process@ccmemo@fail\relax +} +\mmzTracingOff +% \end{macro} +% \end{key} +% +% +%\newcommand\IfMemoizing[2][]{\@secondoftwo} +%<*mmz> +% +% \subsection{Context} +% \label{sec:code:context} +% +% \begin{macro}{\mmzContext,\mmzContextExtra} +% The context expression is stored in two token registers. Outside +% memoization, we will locally assign to |\mmzContext|; during memoization, +% we will globally assign to |\mmzContextExtra|. +\newtoks\mmzContext +\newtoks\mmzContextExtra +% \end{macro} +% \begin{key}{context, clear context} +% The user interface keys for context manipulation hide the complexity +% underlying the context storage from the user. +\mmzset{% + context/.code={% + \ifmemoizing + \expandafter\gtoksapp\expandafter\mmzContextExtra + \else + \expandafter\toksapp\expandafter\mmzContext + \fi + % We append a comma to the given context chunk, for disambiguation. + {#1,}% + }, + clear context/.code={% + \ifmemoizing + \expandafter\global\expandafter\mmzContextExtra + \else + \expandafter\mmzContext + \fi + {}% + }, + clear context/.value forbidden, + % \end{key} + % \begin{key}{meaning to context, csname meaning to context, key meaning to + % context, key value to context, /handlers/.meaning to context, + % /handlers/.value to context} + % Utilities to put the meaning of various stuff into |context|. + meaning to context/.code={\forcsvlist\mmz@mtoc{#1}}, + csname meaning to context/.code={\mmz@mtoc@cs{#1}}, + key meaning to context/.code={\forcsvlist\mmz@mtoc\mmz@mtoc@keycmd{#1}}, + key value to context/.code={\forcsvlist\mmz@mtoc@key{#1}}, + /handlers/.meaning to context/.code={% + \expanded{\noexpand\mmz@mtoc@cs{pgfk@\pgfkeyscurrentpath/.@cmd}}}, + /handlers/.value to context/.code={% + \expanded{\noexpand\mmz@mtoc@cs{pgfk@\pgfkeyscurrentpath}}}, +} +\def\mmz@mtoc#1{% + \collargs@cs@cases{#1}% + {\mmz@mtoc@cmd{#1}}% + {\mmz@mtoc@error@notcsorenv{#1}}% + {% + \mmz@mtoc@cs{% + %start% + #1}% + \mmz@mtoc@cs{% + %end% + %stop% + #1}% + }% +} +\def\mmz@mtoc@cmd#1{% + \begingroup + \escapechar=-1 + \expandafter\endgroup + \expandafter\mmz@mtoc@cs\expandafter{\string#1}% +} +\def\mmz@mtoc@cs#1{% + \pgfkeysvalueof{/mmz/context/.@cmd}% + \expandafter\string\csname#1\endcsname={\expandafter\meaning\csname#1\endcsname}% + \pgfeov +} +\def\mmz@mtoc@key#1{\mmz@mtoc@cs{pgfk@#1}} +\def\mmz@mtoc@key#1{\mmz@mtoc@cs{pgfk@#1/.@cmd}} +\def\mmz@mtoc@error@notcsorenv#1{% + \PackageError{memoize}{'\detokenize{#1}' passed to key 'meaning to context' is neither a command nor an environment}{}% +} +% \end{key} +% +% +% \subsection{C-memos} +% \label{sec:code:c-memo} +% +% \paragraph{The path} to a c-memo consists of the path prefix, the MD5 sum of +% the memoized code, and suffix |.memo|. +\def\mmz@cmemo@path{\mmz@prefix@path\mmz@code@mdfivesum.memo} +% \begin{macro}{\mmzCMemo} +% The additional, free-form content of the c-memo is collected in this token +% register. +\newtoks\mmzCMemo +% \end{macro} +% +% \begin{key}{include source in cmemo} +% \begin{macro}{\ifmmz@include@source} +% Should we include the memoized code in +% the c-memo? By default, yes. \bigskip +\mmzset{% + include source in cmemo/.is if=mmz@include@source, +} +\newif\ifmmz@include@source +\mmz@include@sourcetrue +% \end{macro} +% \end{key} +% +% \begin{macro}{\mmz@write@cmemo} +% This macro creates the c-memo from the contents of |\mmzContextExtra| and +% |\mmzCMemo|. +\def\mmz@write@cmemo{% + % Open the file for writing. + \immediate\openout\mmz@out{\mmz@cmemo@path}% + % The memo starts with the |\mmzMemo| marker (a signal that the memo is valid). + \immediate\write\mmz@out{\noexpand\mmzMemo}% + % We store the content of |\mmzContextExtra| by writing out a command that + % will (globally) assign its content back into this register. + \immediate\write\mmz@out{% + \global\mmzContextExtra{\the\mmzContextExtra}\collargs@percentchar + }% + % Write out the free-form part of the c-memo. + \immediate\write\mmz@out{\the\mmzCMemo\collargs@percentchar}% + % When |include source in cmemo| is in effect, add the memoized code, hiding + % it behind the |\mmzSource| marker. + \ifmmz@include@source + \immediate\write\mmz@out{\noexpand\mmzSource}% + \immediate\write\mmz@out{\the\mmz@mdfive@source}% + \fi + % Close the file. + \immediate\closeout\mmz@out + % Record that we wrote a new c-memo. + \pgfkeysalso{/mmz/record/new cmemo={\mmz@cmemo@path}}% +} +% \end{macro} +% +% \begin{macro}{\mmz@write@empty@cmemo} +% This macro is used to create an empty c-memo on aborted memoization, to +% make sure that no leftover c-memo tricks Memoize into thinking that the +% code was successfully memoized. +\def\mmz@write@empty@cmemo{% + \immediate\openout\mmz@out{\mmz@cmemo@path}% + \immediate\closeout\mmz@out +} +% \end{macro} +% +% +% \begin{macro}{\mmzSource} +% The c-memo memoized code marker. This macro is synonymous with |\endinput|, +% so the source following it is ignored when inputting the c-memo. +\let\mmzSource\endinput +% \end{macro} +% +% \begin{macro}{\mmz@process@cmemo} +% This macro inputs the c-memo, which will update the context code, which we +% can then compute the MD5 sum of. +\def\mmz@process@cmemo{% + \mmz@trace@process@cmemo + % |\ifmmz@abort| serves as a signal that the c-memo exists and is of correct + % form. + \global\mmz@aborttrue + % If c-memo sets |\ifmmzUnmemoizable|, we will compile regularly. + \global\mmzUnmemoizablefalse + \def\mmzMemo{\global\mmz@abortfalse}% + % Just a safeguard \dots\ c-memo assigns to |\mmzContextExtra| anyway. + \global\mmzContextExtra{}% + % Input the c-memo, if it exists, and record that we have used it. + \IfFileExists{\mmz@cmemo@path}{% + \input{\mmz@cmemo@path}% + \pgfkeysalso{/mmz/record/used cmemo={\mmz@cmemo@path}}% + }{% + \mmz@trace@process@no@cmemo + }% + % Compute the context MD5 sum. + \mmz@compute@context@mdfivesum +} +% \end{macro} +% +% \begin{macro}{\mmz@compute@context@mdfivesum} +% This macro computes the MD5 sum of the concatenation of |\mmzContext| and +% |\mmzContextExtra|, and writes out the tracing info when |trace context| is +% in effect. The argument is the tracing note. +% \end{macro} +\def\mmz@compute@context@mdfivesum{% + \xdef\mmz@context@key{\the\mmzContext\the\mmzContextExtra}% + % A special provision for padding, which occurs in the context by default, + % and may contain otherwise undefined macros referring to the extern + % dimensions. We make sure that when we expand the context key, + % |\mmz@paddings| contains the stringified |\width| etc., while these macros + % (which may be employed by the end user in the context expression), are + % returned to their original definitions. + \begingroup + \begingroup + \def\width{\string\width}% + \def\height{\string\height}% + \def\depth{\string\depth}% + \edef\mmz@paddings{\mmz@paddings}% + \expandafter\endgroup + \expandafter\def\expandafter\mmz@paddings\expandafter{\mmz@paddings}% + % We pre-expand the concatenated context, for tracing\slash inclusion in the + % cc-memo. In \hologo{LaTeX}, we protect the expansion, as the context + % expression may contain whatever. + %\protected@xdef + %\xdef + \mmz@context@key{\mmz@context@key}% + \endgroup + % Compute the MD5 sum. We have to assign globally, because this macro is (also) + % called after inputting the c-memo, while the resulting MD5 sum is used to + % input the cc-memo, which happens outside the |\Memoize| group. + % |\mmz@context@mdfivesum|. + \xdef\mmz@context@mdfivesum{\pdf@mdfivesum{\expandonce\mmz@context@key}}% +} +% +% +% \subsection{Cc-memos} +% \label{sec:code:cc-memo} +% +% \paragraph{The path} to a cc-memo consists of the path prefix, the +% hyphen-separated MD5 sums of the memoized code and the (evaluated) context, +% and suffix |.memo|. +\def\mmz@ccmemo@path{% + \mmz@prefix@path\mmz@code@mdfivesum-\mmz@context@mdfivesum.memo} +% +% \paragraph{The structure} of a cc-memo: +% \begin{itemize} +% \item the list of resources consisting of calls to |\mmzResource|; +% \item the core memo code (which includes the externs when executed), +% introduced by marker |\mmzMemo|; and, +% \item optionally, the context expansion, introduced by marker +% |\mmzThisContext|. +% \end{itemize} +% +% We begin the cc-memo with a list of extern files included by the core memo +% code so that we can check whether these files exist prior to executing the +% core memo code. Checking this on the fly, while executing the core memo +% code, would be too late, as that code is arbitrary (and also executed outside +% the |\Memoize| group). +% +% \begin{macro}{\mmzCCMemo} +% During memoization, the core content of the cc-memo is collected into this +% token register. +\newtoks\mmzCCMemo +% \end{macro} +% +% \begin{key}{include context in ccmemo} +% \begin{macro}{\ifmmz@include@context} +% Should we include the context expansion in the cc-memo? By default, no. +% \bigskip +\newif\ifmmz@include@context +\mmzset{% + include context in ccmemo/.is if=mmz@include@context, +} +% \end{macro} +% \end{key} +% +% \begin{key}{direct ccmemo input} +% \begin{macro}{\ifmmz@direct@ccmemo@input} +% When this conditional is false, the +% cc-memo is read indirectly, via a token register, to facilitate +% inverse search. \bigskip +\newif\ifmmz@direct@ccmemo@input +\mmzset{% + direct ccmemo input/.is if=mmz@direct@ccmemo@input, +} +% \end{macro} +% \end{key} +% +% \begin{macro}{\mmz@write@ccmemo} +% This macro creates the cc-memo from the list of resources in +% |\mmz@ccmemo@resources| and the contents of |\mmzCCMemo|. +\def\mmz@write@ccmemo{% + % Open the cc-memo file for writing. Note that the filename contains the + % context MD5 sum, which can only be computed after memoization, as the + % memoized code can update the context. This is one of the two reasons why + % we couldn't write the cc-memo directly into the file, but had to collect + % its contents into token register |\mmzCCMemo|. + \immediate\openout\mmz@out{\mmz@ccmemo@path}% + % Token register |\mmz@ccmemo@resources| consists of calls to + % |\mmz@ccmemo@append@resource|, so the following code writes down the list + % of created externs into the cc-memo. Wanting to have this list at the top + % of the cc-memo is the other reason for the roundabout creation of the + % cc-memo --- the resources become known only during memoization, as well. + \begingroup + \the\mmz@ccmemo@resources + \endgroup + % Write down the content of |\mmzMemo|, but first introduce it by the + % |\mmzMemo| marker. + \immediate\write\mmz@out{\noexpand\mmzMemo}% + \immediate\write\mmz@out{\the\mmzCCMemo\collargs@percentchar}% + % Write down the context tracing info when |include context in ccmemo| is in + % effect. + \ifmmz@include@context + \immediate\write\mmz@out{\noexpand\mmzThisContext}% + \immediate\write\mmz@out{\expandonce{\mmz@context@key}}% + \fi + % Insert the end-of-file marker and close the file. + \immediate\write\mmz@out{\noexpand\mmzEndMemo}% + \immediate\closeout\mmz@out + % Record that we wrote a new cc-memo. + \pgfkeysalso{/mmz/record/new ccmemo={\mmz@ccmemo@path}}% +} +% \end{macro} +% +% \begin{macro}{\mmz@ccmemo@append@resource} +% Append the resource to the cc-memo (we are nice to external utilities and +% put each resource on its own line). |#1| is the sequential number of the +% extern belonging to the memoized code; below, we assign it to |\mmz@seq|, +% which appears in |\mmz@extern@name|. Note that |\mmz@extern@name| only +% contains the extern filename --- without the path, so that externs can be +% used by several projects, or copied around. +\def\mmz@ccmemo@append@resource#1{% + \mmz@seq=#1\relax + \immediate\write\mmz@out{% + \string\mmzResource{\mmz@extern@name}\collargs@percentchar}% +} +% \end{macro} +% +% \begin{macro}{\mmzResource} +% A list of these macros is located at the top of a cc-memo. The macro +% checks for the existence of the extern file, given as |#1|. If the extern +% does not exist, we redefine |\mmzMemo| to |\endinput|, so that the core +% content of the cc-memo is never executed; see also |\mmz@process@ccmemo| +% above. +\def\mmzResource#1{% + % We check for existence using |\pdffilesize|, because an empty PDF, which + % might be produced by a failed \hologo{TeX}-based extraction, should count + % as no file. The |0| behind |\ifnum| is there because |\pdffilesize| + % returns an empty string when the file does not exist. + \ifnum0\pdf@filesize{\mmz@dir@path/#1}=0 + \ifmmz@direct@ccmemo@input + \let\mmzMemo\endinput + \else + % With indirect cc-memo input, we simulate end-of-input by grabbing + % everything up to the end-of-memo marker. In the indirect cc-memo + % input, a |\par| token shows up after |\mmzEndMemo|, I'm not sure why + % (|\everyeof={}| does not help). + \long\def\mmzMemo##1\mmzEndMemo\par{}% + \fi + \mmz@trace@resource{#1}% + \fi +} +% \end{macro} +% +% \begin{macro}{\mmz@process@ccmemo,\mmzThisContext,\mmzEndMemo} +% This macro processes the cc-memo. +\def\mmz@process@ccmemo{% + \mmz@trace@process@ccmemo + % The following conditional signals whether cc-memo was successfully + % utilized. If the cc-memo file does not exist, |\ifmmz@abort| will remain + % true. If it exists, it is headed by the list of resources. If a + % resource check fails, |\mmzMemo| (which follows the list of resources) is + % redefined to |\endinput|, so |\ifmmz@abort| remains true. However, if + % all resource checks are successful, |\mmzMemo| marker is reached with + % the below definition in effect, so |\ifmmz@abort| becomes false. Note + % that this marker also closes the |\Memoize| group, so that the core + % cc-memo content is executed in the original group --- and that this + % does not happen if anything goes wrong! + \global\mmz@aborttrue + % Note that |\mmzMemo| may be redefined by |\mmzResource| upon an unavailable + % extern file. + \def\mmzMemo{% + \endgroup + \global\mmz@abortfalse + % We |\let| the control sequence used for extern inclusion in the cc-memo to + % the macro which includes the extern from the extern file. + \let\mmzIncludeExtern\mmz@include@extern + }% + % Define |\mmzEndMemo| wrt |\ifmmz@direct@ccmemo@input|, whose value will be + % lost soon because |\mmMemo| will close the group --- that's also why this + % definition is global. + \xdef\mmzEndMemo{% + \ifmmz@direct@ccmemo@input + \noexpand\endinput + \else + % In the indirect cc-memo input, a |\par| token shows up after + % |\mmzEndMemo|, I'm not sure why (|\everyeof={}| does not help). + \unexpanded{% + \def\mmz@temp\par{}% + \mmz@temp + }% + \fi + }% + % The cc-memo context marker, again wrt |\ifmmz@direct@ccmemo@input| and + % globally. With direct cc-memo input, this macro is synonymous with + % |\endinput|, so the (expanded) context following it is ignored when + % inputting the cc-memo. With indirect input, we simulate end-of-input by + % grabbing everything up to the end-of-memo marker (plus gobble the |\par| + % mentioned above). + \xdef\mmzThisContext{% + \ifmmz@direct@ccmemo@input + \noexpand\endinput + \else + \unexpanded{% + \long\def\mmz@temp##1\mmzEndMemo\par{}% + \mmz@temp + }% + \fi + }% + % Input the cc-memo if it exists. + \IfFileExists{\mmz@ccmemo@path}{% + \ifmmz@direct@ccmemo@input + \input{\mmz@ccmemo@path}% + \else + % Indirect cc-memo input reads the cc-memo into a token register and + % executes the contents of this register. + \filetotoks\toks@{\mmz@ccmemo@path}% + \the\toks@ + \fi + % Record that we have used the cc-memo. + \pgfkeysalso{/mmz/record/used ccmemo={\mmz@ccmemo@path}}% + }{% + \mmz@trace@process@no@ccmemo + }% + \ifmmz@abort + % The cc-memo doesn't exist, or some of the resources don't. We need to + % memoize, but we'll do it only if |readonly| is not in effect, otherwise + % we'll perform a regular compilation. (Note that we are still in the group + % opened prior to executing |\Memoize|.) + \mmz@trace@process@ccmemo@fail + \ifnum\mmz@mode=\mmz@mode@readonly\relax + \expandafter\expandafter\expandafter\mmz@compile + \else + \expandafter\expandafter\expandafter\mmz@memoize + \fi + \else + \mmz@trace@process@ccmemo@ok + \fi +} +% \end{macro} +% +% \subsection{The externs} +% \label{sec:code:extern} +% +% \paragraph{The path} to an extern is like the path to a cc-memo, modulo +% suffix |.pdf|, of course. However, in case memoization of a chunk produces +% more than one extern, the filename of any non-first extern includes +% |\mmz@seq|, the sequential number of the extern as well (we start the +% numbering at $0$). We will have need for several parts of the full path to +% an extern: the basename, the filename, the path without the suffix, and the +% full path. +\newcount\mmz@seq +\def\mmz@extern@basename{% + \mmz@prefix\mmz@code@mdfivesum-\mmz@context@mdfivesum + \ifnum\mmz@seq>0 -\the\mmz@seq\fi +} +\def\mmz@extern@name{\mmz@extern@basename.pdf} +\def\mmz@extern@basepath{\mmz@dir@path/\mmz@extern@basename} +\def\mmz@extern@path{\mmz@extern@basepath.pdf} +% +% \begin{key}{padding left, padding right, padding top, padding bottom} +% These options set the amount of space surrounding the bounding box of the +% externalized graphics in the resulting PDF, i.e.\ in the extern file. This +% allows the user to deal with \TikZ; overlays, |\rlap| and |\llap|, etc. +\mmzset{ + padding left/.store in=\mmz@padding@left, + padding right/.store in=\mmz@padding@right, + padding top/.store in=\mmz@padding@top, + padding bottom/.store in=\mmz@padding@bottom, +% \end{key} +% +% \begin{key}{padding} +% A shortcut for setting all four paddings at once. + padding/.style={ + padding left=#1, padding right=#1, + padding top=#1, padding bottom=#1 + }, + % The default padding is what \hologo{pdfTeX} puts around the page anyway, 1 + % inch, but we'll use |1 in| rather than |1 true in|, which is the true + % default value of |\pdfhorigin| and |\pdfvorigin|, as we want the padding to + % adjust with magnification. + padding=1in, +% \end{key} +% + % \begin{key}{padding to context} + % This key adds padding to the context. Note that we add the padding + % expression (|\mmz@paddings|, defined below, refers to all the individual + % padding macros), not the actual value (at the time of expansion). This + % is so because |\width|, |\height| and |\depth| are not defined outside + % extern shipout routines, and the context is evaluated elsewhere. + padding to context/.style={ + context={padding=(\mmz@paddings)}, + }, + % \end{key} + % + % Padding nearly always belongs into the context --- the exception being + % memoized code which produces no externs (\MS\ref{sec:pure-memoization}) --- + % so we execute this key immediately. + padding to context, +} +\def\mmz@paddings{% + \mmz@padding@left,\mmz@padding@bottom,\mmz@padding@right,\mmz@padding@top +} +% +% \begin{macro}{\mmzExternalizeBox} +% This macro is the public interface to externalization. In Memoize itself, +% it is called from the default memoization driver, |\mmzSingleExternDriver|, +% but it should be called by any driver that wishes to produce an extern, see +% \MS\ref{sec:memoization-drivers} for details. It takes two arguments: +% \begin{enumerate}[\tt\#1] +% \item The box that we want to externalize. It's content will remain intact. +% The box may be given either as a control sequence, declared via |\newbox|, +% or as box number (say, 0). +% \item The token register which will receive the code that includes the extern +% into the document; it is the responsibility of the memoization driver to +% (globally) include the contents of the register in the cc-memo, i.e.\ in +% token register |\mmzCCMemo|. This argument may be either a control +% sequence, declared via |\newtoks|, or a |\toks|\meta{token register +% number}. +% \end{enumerate} +\def\mmzExternalizeBox#1#2{% + \begingroup + % A courtesy to the user, so they can define padding in terms of the size of + % the externalized graphics. + \def\width{\wd#1 }% + \def\height{\ht#1 }% + \def\depth{\dp#1 }% + % Store the extern-inclusion code in a temporary macro, which will be + % smuggled out of the group. + \xdef\mmz@global@temp{% + % Executing |\mmzIncludeExtern| from the cc-memo will include the extern + % into the document. + \noexpand\mmzIncludeExtern + % |\mmzIncludeExtern| identifies the extern by its sequence number, + % |\mmz@seq|. + {\the\mmz@seq}% + % What kind of box? We |\noexpand| the answer just in case someone + % redefined them. + \ifhbox#1\noexpand\hbox\else\noexpand\vbox\fi + % The dimensions of the extern. + {\the\wd#1}% + {\the\ht#1}% + {\the\dp#1}% + % The padding values. + {\the\dimexpr\mmz@padding@left}% + {\the\dimexpr\mmz@padding@bottom}% + {\the\dimexpr\mmz@padding@right}% + {\the\dimexpr\mmz@padding@top}% + }% + % Prepend the new extern box into the global extern box where we collect all + % the externs of this memo. Note that we |\copy| the extern box, retaining + % its content --- we will also want to place the extern box in its regular + % place in the document. + \global\setbox\mmz@tbe@box\vbox{\copy#1\unvbox\mmz@tbe@box}% + % Add the extern to the list of resources, which will be included at the top + % of the cc-memo, to check whether the extern files exists at the time the + % cc-memo is utilized. In the cc-memo, the list will contain full extern + % filenames, which are currently unknown, but no matter; right now, providing + % the extern sequence number suffices, the full extern filename will be + % produced at the end of memoization, once the context MD5 sum is known. + \xtoksapp\mmz@ccmemo@resources{% + \noexpand\mmz@ccmemo@append@resource{\the\mmz@seq}% + }% + % Increment the counter containing the sequence number of the extern within + % this memo. + \global\advance\mmz@seq1 + % Assign the extern-including code into the token register given in |#2|. + % This register may be given either as a control sequence or as + % |\toks|\meta{token register number}, and this is why we have temporarily + % stored the code (into |\mmz@global@temp|) globally: a local storage with + % |\expandafter\endgroup\expandafter| here would fail with the receiving + % token register given as |\toks|\meta{token register number}. + \endgroup + #2\expandafter{\mmz@global@temp}% +} +% \end{macro} +% +% \begin{macro}{\mmz@ccmemo@resources} +% This token register, populated by |\mmz@externalize@box| and used by +% |\mmz@write@ccmemo|, holds the list of externs produced by memoization of +% the current chunk. +\newtoks\mmz@ccmemo@resources +% \end{macro} +% +% \begin{macro}{\mmz@tbe@box} +% |\mmz@externalize@box| does not directly dump the extern into the document +% (as a special page). Rather, the externs are collected into +% |\mmz@tbe@box|, whose contents are dumped into the document at the end of +% memoization of the current chunk. In this way, we guarantee that aborted +% memoization does not pollute the document. +\newbox\mmz@tbe@box +% \end{macro} +% +% \begin{macro}{\mmz@shipout@externs} +% This macro is executed at the end of +% memoization, when the externs are waiting for us in |\mmz@tbe@box| and need +% to be dumped into the document. It loops through the contents of +% |\mmz@tbe@box|,\footnote{The looping code is based on TeX.SE +% answer \url(https://){tex.stackexchange.com/a/25142/16819} by Bruno Le +% Floch.}, putting each extern into |\mmz@box| and calling +% |\mmz@shipout@extern|. Note that the latter macro is executed within the +% group opened by |\vbox| below. +\def\mmz@shipout@externs{% + \global\mmz@seq 0 + \setbox\mmz@box\vbox{% + % Set the macros below to the dimensions of the extern box, so that the + % user can refer to them in the padding specification (which is in turn + % used in the page setup in |\mmz@shipout@extern|). + \def\width{\wd\mmz@box}% + \def\height{\ht\mmz@box}% + \def\depth{\dp\mmz@box}% + \vskip1pt + \ifmmzkeepexterns\expandafter\unvcopy\else\expandafter\unvbox\fi\mmz@tbe@box + \@whilesw\ifdim0pt=\lastskip\fi{% + \setbox\mmz@box\lastbox + \mmz@shipout@extern + }% + }% +} +% \end{macro} +% +% \begin{macro}{\mmz@shipout@extern} +% This macro ships out a single extern, which +% resides in |\mmz@box|, and records the creation of the new extern. +\def\mmz@shipout@extern{% + % Calculate the expected width and height. We have to do this now, before we + % potentially adjust the box size and paddings for magnification. + \edef\expectedwidth{\the\dimexpr + (\mmz@padding@left) + \wd\mmz@box + (\mmz@padding@right)}% + \edef\expectedheight{\the\dimexpr + (\mmz@padding@top) + \ht\mmz@box + \dp\mmz@box + (\mmz@padding@bottom)}% + % Apply the inverse magnification, if |\mag| is not at the default + % value. We'll do this in a group, which will last until shipout. + \begingroup + \ifnum\mag=1000 + \else + \mmz@shipout@mag + \fi + % Setup the geometry of the extern page. In \hologo{plainTeX} and + % \hologo{LaTeX}, setting |\pdfpagewidth| and |\pdfpageheight| seems to do + % the trick of setting the extern page dimensions. In \hologo{ConTeXt}, + % however, the resulting extern page ends up with the PDF |/CropBox| + % specification of the current regular page, which is then used (ignoring our + % |mediabox| requirement) when we're including the extern into the document + % by |\mmzIncludeExtern|. Typically, this results in a page-sized extern. + % I'm not sure how to deal with this correctly. In the workaround below, we + % use Lua function |backends.codeinjections.setupcanvas| to set up page + % dimensions: we first remember the current page dimensions + % (|\edef\mmz@temp|), then set up the extern page dimensions + % (|\expanded{...}|), and finally, after shipping + % out the extern page, revert to the current page dimensions by executing + % |\mmz@temp| at the very end of this macro. + %<*plain,latex> + \pdfpagewidth\dimexpr + (\mmz@padding@left) + \wd\mmz@box + (\mmz@padding@right)\relax + \pdfpageheight\dimexpr + (\mmz@padding@top) + \ht\mmz@box + \dp\mmz@box+ (\mmz@padding@bottom)\relax + % + %<*context> + \edef\mmz@temp{% + \noexpand\directlua{ + backends.codeinjections.setupcanvas({ + paperwidth=\the\numexpr\pagewidth, + paperheight=\the\numexpr\pageheight + }) + }% + }% + \expanded{% + \noexpand\directlua{ + backends.codeinjections.setupcanvas({ + paperwidth=\the\numexpr\dimexpr + \mmz@padding@left + \wd\mmz@box + \mmz@padding@right\relax, + paperheight=\the\numexpr\dimexpr + \mmz@padding@top + \ht\mmz@box + \dp\mmz@box+ \mmz@padding@bottom\relax + }) + }% + }% + % + % + % We complete the page setup by setting the content offset. + \hoffset\dimexpr\mmz@padding@left - \pdfhorigin\relax + \voffset\dimexpr\mmz@padding@top - \pdfvorigin\relax + % We shipout the extern page using the |\shipout| primitive, so that the + % extern page is not modified, or even registered, by the shipout code of the + % format or some package. I can't imagine those shipout routines ever + % needing to know about the extern page. In fact, most often knowing about + % it would be undesirable. For example, \hologo{LaTeX} and \hologo{ConTeXt} + % count the ``real'' pages, but usually to know whether they are shipping out + % an odd or an even page, or to make the total number of pages available to + % subsequent compilations. Taking the extern pages into account would + % disrupt these mechanisms. + % + % Another thing: delayed |\write|s. We have to make sure that any + % \hologo{LaTeX}-style protected stuff in those is not expanded. We don't + % bother introducing a special group, as we'll close the |\mag| group right + % after the shipout anyway. + %\let\protect\noexpand + \pdf@primitive\shipout\box\mmz@box + %\mmz@temp + \endgroup + % Advance the counter of shipped-out externs. We do this before preparing + % the recording information below, because the extern extraction tools expect + % the extern page numbering to start with $1$. + \global\advance\mmzExternPages1 + % Prepare the macros which may be used in |record//new extern| code. + \edef\externbasepath{\mmz@extern@basepath}% + % Adding up the counters below should result in the real page number of the + % extern. Macro |\mmzRegularPages| holds the number of pages which were + % shipped out so far using the regular shipout routine of the format; + % |\mmzExternPages| holds the number of shipped-out extern pages; and + % |\mmzExtraPages| holds, or at least should hold, the number of pages + % shipped out using any other means. + \edef\pagenumber{\the\numexpr\mmzRegularPages + % In \hologo{LaTeX}, the |\mmzRegularPages| holds to number of pages + % already shipped out. In \hologo{ConTeXt}, the counter is already + % increased while processing the page, so we need to subtract $1$. +% -1% + +\mmzExternPages+\mmzExtraPages}% + % Record the creation of the new extern. We do this after shipping out the + % extern page, so that the recording mechanism can serve as an after-shipout + % hook, for the unlikely situation that some package really needs to do + % something when our shipout happens. Note that we absolutely refuse to + % provide a before-shipout hook, because we can't allow anyone messing with + % our extern, and that using this after-shipout ``hook'' is unnecessary for + % counting extern shipouts, as we already provide this information in the + % public counter |\mmzExternPages|. + \mmzset{record/new extern/.expanded=\mmz@extern@path}% + % Advance the sequential number of the extern, in the context of the current + % memoized code chunk. This extern numbering starts at 0, so we only do this + % after we wrote the cc-memo and called |record/new extern|. + \global\advance\mmz@seq1 +} +% \end{macro} +% +% \begin{macro}{\mmz@shipout@mag} +% This macro applies the inverse magnification, so that the extern ends up +% with its natural size on the extern page. +\def\mmz@shipout@mag{% + % We scale the extern box using the PDF primitives: |q| and |Q| save and + % restore the current graphics state; |cm| applies the given coordinate + % transformation matrix. ($a$ $b$ $c$ $d$ $e$ $f$ |cm| transforms $(x,y)$ + % into $(ax+cy+e,bx+dy+f)$.) + \setbox\mmz@box\hbox{% + \pdfliteral{q \mmz@inverse@mag\space 0 0 \mmz@inverse@mag\space 0 0 cm}% + \copy\mmz@box\relax + \pdfliteral{Q}% + }% + % We first have to scale the paddings, as they might refer to the |\width| + % etc.\ of the extern. + \dimen0=\dimexpr\mmz@padding@left\relax + \edef\mmz@padding@left{\the\dimexpr\mmz@inverse@mag\dimen0}% + \dimen0=\dimexpr\mmz@padding@bottom\relax + \edef\mmz@padding@bottom{\the\dimexpr\mmz@inverse@mag\dimen0}% + \dimen0=\dimexpr\mmz@padding@right\relax + \edef\mmz@padding@right{\the\dimexpr\mmz@inverse@mag\dimen0}% + \dimen0=\dimexpr\mmz@padding@top\relax + \edef\mmz@padding@top{\the\dimexpr\mmz@inverse@mag\dimen0}% + % Scale the extern box. + \wd\mmz@box=\mmz@inverse@mag\wd\mmz@box\relax + \ht\mmz@box=\mmz@inverse@mag\ht\mmz@box\relax + \dp\mmz@box=\mmz@inverse@mag\dp\mmz@box\relax +} +% \end{macro} +% +% \begin{macro}{\mmz@inverse@mag} +% The inverse magnification factor, i.e.\ the number we have to multiply the +% extern dimensions with so that they will end up in their natural size. We +% compute it, once and for all, at the beginning of the document. To do +% that, we borrow the little macro |\Pgf@geT| from |pgfutil-common| (but +% rename it). +{\catcode`\p=12\catcode`\t=12\gdef\mmz@Pgf@geT#1pt{#1}} +\mmzset{begindocument/.append code={% + \edef\mmz@inverse@mag{\expandafter\mmz@Pgf@geT\the\dimexpr 1000pt/\mag}% + }} +% \end{macro} +% +% \begin{macro}{\mmzRegularPages} +% This counter holds the number of pages shipped out by the format's shipout +% routine. \hologo{LaTeX} and \hologo{ConTeXt} keep track of this in +% dedicated counters, so we simply use those. In \hologo{plainTeX}, we have +% to hack the |\shipout| macro to install our own counter. In fact, we +% already did this while loading the required packages, in order to avoid it +% being redefined by |atbegshi| first. All that is left to do here is to +% declare the counter. +%\let\mmzRegularPages\ReadonlyShipoutCounter +%\let\mmzRegularPages\realpageno +%\newcount\mmzRegularPages +% \end{macro} +% +% \begin{macro}{\mmzExternPages} +% This counter holds the number of extern pages shipped out so far. +\newcount\mmzExternPages +% \end{macro} +% +% The total number of new externs is announced at the end of the compilation, +% so that \hologo{TeX} editors, |latexmk| and such can propose recompilation. +\mmzset{ + enddocument/afterlastpage/.append code={% + \ifnum\mmzExternPages>0 + \PackageWarning{memoize}{The compilation produced \the\mmzExternPages\space + new extern\ifnum\mmzExternPages>1 s\fi.}% + \fi + }, +} +% +% \begin{macro}{\mmzExtraPages} +% This counter will probably remain at zero forever. It should be advanced by +% any package which (like Memoize) ships out pages bypassing the regular +% shipout routine of the format. +\newcount\mmzExtraPages +% \end{macro} +% +% \begin{macro}{\mmz@include@extern} +% This macro, called from cc-memos as |\mmzIncludeExtern|, inserts an extern +% file into the document. |#1| is the sequential number, |#2| is either +% |\hbox| or |\vbox|, |#3|, |#4| and |#5| are the (expected) width, height +% and the depth of the externalized box; |#6|--|#9| are the paddings (left, +% bottom, right, and top). +\def\mmz@include@extern#1#2#3#4#5#6#7#8#9{% + % Set the extern sequential number, so that we open the correct extern file + % (|\mmz@extern@basename|). + \mmz@seq=#1\relax + % Use the primitive PDF graphics inclusion commands to include the extern + % file. Set the correct depth or the resulting box, and shift it as + % specified by the padding. + \setbox\mmz@box=#2{% + \setbox0=\hbox{% + \lower\dimexpr #5+#7\relax\hbox{% + \hskip -#6\relax + \setbox0=\hbox{% + \mmz@insertpdfpage{\mmz@extern@path}{1}% + }% + \unhbox0 + }% + }% + \wd0 \dimexpr\wd0-#8\relax + \ht0 \dimexpr\ht0-#9\relax + \dp0 #5\relax + \box0 + }% + % Check whether the size of the included extern is as expected. There is no + % need to check |\dp|, we have just set it. (|\mmz@if@roughly@equal| is + % defined in section~\ref{sec:code:extract:tex}.) + \mmz@tempfalse + \mmz@if@roughly@equal{\mmz@tolerance}{#3}{\wd\mmz@box}{% + \mmz@if@roughly@equal{\mmz@tolerance}{#4}{\ht\mmz@box}{% + \mmz@temptrue + }{}}{}% + \ifmmz@temp + \else + \mmz@use@memo@warning{\mmz@extern@path}{#3}{#4}{#5}% + \fi + % Use the extern box, with the precise size as remembered at memoization. + \wd\mmz@box=#3\relax + \ht\mmz@box=#4\relax + \box\mmz@box + % Record that we have used this extern. + \pgfkeysalso{/mmz/record/used extern={\mmz@extern@path}}% +} +% \end{macro} +\def\mmz@use@memo@warning#1#2#3#4{% + \PackageWarning{memoize}{Unexpected size of extern "#1"; + expected #2\space x \the\dimexpr #3+#4\relax, + got \the\wd\mmz@box\space x \the\dimexpr\the\ht\mmz@box+\the\dp\mmz@box\relax}% +} +% \begin{macro}{\mmz@insertpdfpage} +% This macro inserts a page from the PDF into the document. We define it +% according to which engine is being used. Note that \hologo{ConTeXt} always +% uses \hologo{LuaTeX}. +%\ifdef\luatexversion{% + \def\mmz@insertpdfpage#1#2{% #1 = filename, #2 = page number + \saveimageresource page #2 mediabox {#1}% + \useimageresource\lastsavedimageresourceindex + }% +%<*latex,plain> +}{% + \ifdef\XeTeXversion{% + \def\mmz@insertpdfpage#1#2{% + \XeTeXpdffile #1 page #2 media + }% + }{% pdfLaTeX + \def\mmz@insertpdfpage#1#2{% + \pdfximage page #2 mediabox {#1}% + \pdfrefximage\pdflastximage + }% + }% +} +% +% \end{macro} +% +% \begin{macro}{\mmz@include@extern@from@tbe@box} +% Include the extern number |#1| residing in |\mmz@tbe@box| into the +% document. This helper macro makes it possible for a complex memoization +% driver to process the cc-memo right after memoization --- by using the +% |\mmzkeepexternstrue\xtoksapp\mmzAfterMemoizationExtra{\the\mmzCCMemo}| +% trick --- to ensure that the result of the memoizing compilation matches +% the result of inputting the cc-memo. The rest of the arguments are +% gobbled, as we don't have to do any size adjustment or checking here, and +% the box is of the correct type. +\def\mmz@include@extern@from@tbe@box#1#2#3#4#5#6#7#8#9{% + \setbox0\vbox{% + \@tempcnta#1\relax + \vskip1pt + \unvcopy\mmz@tbe@box + \@whilenum\@tempcnta>0\do{% + \setbox0\lastbox + \advance\@tempcnta-1\relax + }% + \global\setbox1\lastbox + \@whilesw\ifdim0pt=\lastskip\fi{% + \setbox0\lastbox + }% + \box\mmz@box + }% + \box1 +} +% \end{macro} +% +% \section{Extraction} +% \label{sec:code:extract} +% +% \subsection{Extraction mode and method} +% \label{sec:code:extraction-mode-method} +% +% \begin{key}{extract} +% This key selects the extraction mode and method. It normally occurs in the +% package options list, less commonly in the preamble, and never in the +% document body. +\def\mmzvalueof#1{\pgfkeysvalueof{/mmz/#1}} +\mmzset{ + extract/.estore in=\mmz@extraction@method, + extract/.value required, + begindocument/.append style={extract/.code=\mmz@preamble@only@warning}, + % \end{key} + % + % \begin{key}{extract/perl, extract/python} + % Any other value will select internal extraction with the given method. + % Memoize ships with two extraction scripts, a Perl script and a Python + % script, which are selected by |extract=perl| (the default) and + % |extract=python|, respectively. We run the scripts in verbose mode + % (without |-q|), and keep the |.mmz| file as is, i.e.\ we're not commenting + % out the |\mmzNewExtern| lines, because we're about to overwrite it + % anyway. We also request the log file, which will contain + % |\mmzExtractionSuccessful| if extraction was successful. + extract/perl/.code={% + \mmz@clear@extraction@log + \pdf@system{% + \mmzvalueof{perl extraction command}\space + \mmzvalueof{perl extraction options}% + }% + \mmz@check@extraction@log{perl}% + \def\mmz@mkdir@command##1{\mmzvalueof{perl extraction command}\space --mkdir "##1"}% + }, + perl extraction command/.initial=memoize-extract.pl, + perl extraction options/.initial={% +% \begin{listingregion}{perl-extraction-options.tex} + -e -l "\mmzOutputDirectory\mmzUnquote\jobname.mmz.log" -w + %"\string\PackageWarning{memoize (perl-based extraction)}{\string\warningtext}" + %"\string\warning{memoize (perl-based extraction): \string\warningtext}" + %"\string\warning{memoize (perl-based extraction): \string\warningtext}" + "\mmzOutputDirectory\mmzUnquote\jobname.mmz" +% \end{listingregion} + }, + extract=perl, + extract/python/.code={% + \mmz@clear@extraction@log + \pdf@system{% + \mmzvalueof{python extraction command}\space + \mmzvalueof{python extraction options}% + }% + \mmz@check@extraction@log{python}% + \def\mmz@mkdir@command##1{\mmzvalueof{python extraction command}\space --mkdir "##1"}% + }, + python extraction command/.initial=memoize-extract.py, + python extraction options/.initial={% + -e -l "\mmzOutputDirectory\mmzUnquote\jobname.mmz.log" -w + %"\string\PackageWarning{memoize (python-based extraction)}{\string\warningtext}" + %"\string\warning{memoize (python-based extraction): \string\warningtext}" + %"\string\warning{memoize (python-based extraction): \string\warningtext}" + "\mmzOutputDirectory\mmzUnquote\jobname.mmz" + }, +} +\def\mmz@preamble@only@warning{% + \PackageWarning{memoize}{% + Ignoring the invocation of "\pgfkeyscurrentkey". + This key may only be executed in the preamble}% +} +% \end{key} +% +% \paragraph*{The extraction log} +% As we cannot access the exit status of a system command in \hologo{TeX}, we +% communicate with the system command via the ``extraction log file,'' produced +% by both \hologo{TeX}-based extraction and the Perl and Python extraction +% script. This file signals whether the embedded extraction was successful --- +% if it is, the file contains |\mmzExtractionSuccessful| --- and also contains +% any size-mismatch warnings (these are currently only thrown by the +% \hologo{TeX}-based extraction). As the log is really a \hologo{TeX} file, +% the idea is to simply input it after extracting each extern (for +% \hologo{TeX}-based extraction) or after the extraction of all externs (for +% the external scripts). +\def\mmz@clear@extraction@log{% + \begingroup + \immediate\openout0{\mmzUnquote\jobname.mmz.log"}% + \immediate\closeout0 + \endgroup +} +% |#1| is the extraction method. +\def\mmz@check@extraction@log#1{% + \begingroup \def\extractionmethod{#1}% + \mmz@tempfalse \let\mmz@orig@endinput\endinput + \def\endinput{\mmz@temptrue\mmz@orig@endinput}% + \@input{\jobname.mmz.log}% + \ifmmz@temp \else \mmz@extraction@error \fi \endgroup } +\def\mmz@extraction@error{% + \PackageWarning{memoize}{Extraction of externs from document "\mmzUnquote\jobname.pdf" + using method "\extractionmethod" was unsuccessful. Have you set the + shell escape mode as suggested in chapter 1 of the manual?}{}} +% +% \subsection{The record files} +% \label{sec:code:record} +% +% \begin{key}{record} +% This key activates a record \meta{type}: the hooks defined by that record +% \meta{type} will henceforth be executed at the appropriate places. +% +% A \meta{hook} of a particular \meta{type} resides in |pgfkeys| path +% |/mmz/record/|\meta{type}|/|\meta{hook}, and is invoked via +% |/mmz/record/|\meta{hook}. Record type activation thus appends a call of +% the former to the latter. It does so using handler |.try|, so that +% unneeded hooks may be left undefined. +% +\mmzset{ + record/.style={% + record/begin/.append style={ + /mmz/record/#1/begin/.try, + % The |begin| hook also executes the |prefix| hook, so that |\mmzPrefix| + % surely occurs at the top of the |.mmz| file. Listing each prefix type + % separately in this hook ensures that |prefix| of a certain type is + % executed after that type's |begin|. + /mmz/record/#1/prefix/.try/.expanded=\mmz@prefix@path, + }, + record/prefix/.append style={/mmz/record/#1/prefix/.try={##1}}, + record/new extern/.append style={/mmz/record/#1/new extern/.try={##1}}, + record/used extern/.append style={/mmz/record/#1/used extern/.try={##1}}, + record/new cmemo/.append style={/mmz/record/#1/new cmemo/.try={##1}}, + record/new ccmemo/.append style={/mmz/record/#1/new ccmemo/.try={##1}}, + record/used cmemo/.append style={/mmz/record/#1/used cmemo/.try={##1}}, + record/used ccmemo/.append style={/mmz/record/#1/used ccmemo/.try={##1}}, + record/end/.append style={/mmz/record/#1/end/.try}, + }, +} +% \end{key} +% +% \begin{key}{no record} +% This key deactivates all record types. Below, we use it to initialize the +% relevant keys; in the user code, it may be used to deactivate the +% preactivated |mmz| record type. +\mmzset{ + no record/.style={% + % The |begin| hook clears itself after invocation, to prevent double + % execution. Consequently, |record/begin| may be executed by the user in + % the preamble, without any ill effects. + record/begin/.style={record/begin/.style={}}, + % The |prefix| key invokes itself again when the group closes. This way, + % we can correctly track the path prefix changes in the |.mmz| even if + % |path| is executed in a group. + record/prefix/.code={\aftergroup\mmz@record@prefix}, + record/new extern/.code={}, + record/used extern/.code={}, + record/new cmemo/.code={}, + record/new ccmemo/.code={}, + record/used cmemo/.code={}, + record/used ccmemo/.code={}, + % The |end| hook clears itself after invocation, to prevent double + % execution. Consequently, |record/end| may be executed by the user before + % the end of the document, without any ill effects. + record/end/.style={record/end/.code={}}, + } +} +% \end{key} +% We define this macro because |\aftergroup|, used in |record/prefix|, only +% accepts a token. +\def\mmz@record@prefix{% + \mmzset{/mmz/record/prefix/.expanded=\mmz@prefix@path}% +} +% \paragraph{Initialize} the hook keys, preactivate |mmz| record type, and +% execute hooks |begin| and |end| at the edges of the document. +\mmzset{ + no record, + record=mmz, + begindocument/.append style={record/begin}, + enddocument/afterlastpage/.append style={record/end}, +} +% +% \subsubsection{The \texttt{.mmz} file} +% \label{sec:code:record:mmz} +% +% Think of the |.mmz| record file as a \hologo{TeX}-readable log file, which +% lets the extraction procedure know what happened in the previous compilation. +% The file is in \hologo{TeX} format, so that we can trigger internal +% \hologo{TeX}-based extraction by simply inputting it. The commands it +% contains are intentionally as simple as possible (just a macro plus braced +% arguments), to facilitate parsing by the external scripts. +% +% These hooks simply put the calls of the corresponding macros into the file. +% All but hooks but |begin| and |end| receive the full path to the relevant +% file as the only argument (ok, |prefix| receives the full path prefix, as set +% by key |path|). +% +% \begin{key}{record/mmz/...} +\mmzset{ + record/mmz/begin/.code={% + \newwrite\mmz@mmzout + % The record file has a fixed name (the jobname plus the |.mmz| suffix) and + % location (the current directory, i.e.\ the directory where \hologo{TeX} + % is executed from; usually, this will be the directory containing the + % \hologo{TeX} source). + \immediate\openout\mmz@mmzout{\jobname.mmz}% + }, + % The |\mmzPrefix| is used by the clean-up script, which will remove all + % files with the given path prefix but (unless called with |--all|) those + % mentioned in the |.mmz|. Now this script could in principle figure out + % what to remove by inspecting the paths to utilized\slash created + % memos\slash externs in the |.mmz| file, but this method could lead to + % problems in case of an incomplete (perhaps empty) |.mmz| file created by a + % failed compilation. Recording the path prefix in the |.mmz| radically + % increases the chances of a successful clean-up, which is doubly important, + % because a clean-up is sometimes precisely what we need to do to recover + % after a failed compilation. + record/mmz/prefix/.code={% + \immediate\write\mmz@mmzout{\noexpand\mmzPrefix{#1}}% + }, + record/mmz/new extern/.code={% + % While this key receives a single formal argument, Memoize also prepares + % macros |\externbasepath| (|#1| without the |.pdf| suffix), |\pagenumber| + % (of the extern page in the document PDF), and |\expectedwidth| and + % |\expectedheight| (of the extern page). + \immediate\write\mmz@mmzout{% + \noexpand\mmzNewExtern{#1}{\pagenumber}{\expectedwidth}{\expectedheight}% + }% + }, + record/mmz/new cmemo/.code={% + \immediate\write\mmz@mmzout{\noexpand\mmzNewCMemo{#1}}% + }, + record/mmz/new ccmemo/.code={% + \immediate\write\mmz@mmzout{\noexpand\mmzNewCCMemo{#1}}% + }, + record/mmz/used extern/.code={% + \immediate\write\mmz@mmzout{\noexpand\mmzUsedExtern{#1}}% + }, + record/mmz/used cmemo/.code={% + \immediate\write\mmz@mmzout{\noexpand\mmzUsedCMemo{#1}}% + }, + record/mmz/used ccmemo/.code={% + \immediate\write\mmz@mmzout{\noexpand\mmzUsedCCMemo{#1}}% + }, + record/mmz/end/.code={% + % Add the |\endinput| marker to signal that the file is complete. + \immediate\write\mmz@mmzout{\noexpand\endinput}% + \immediate\closeout\mmz@mmzout + }, + % + % \end{key} + % \subsubsection{The shell scripts} +% +% We define two shell script record types: |sh| for Linux, and |bat| for +% Windows. +% +% \begin{key}{sh, bat} +% These keys set the shell script filenames. +% \bigskip + sh/.store in=\mmz@shname, + sh=memoize-extract.\jobname.sh, + bat/.store in=\mmz@batname, + bat=memoize-extract.\jobname.bat, +% \end{key} +% +% \begin{key}{record/sh/...} Define the Linux shell script record type. + record/sh/begin/.code={% + \newwrite\mmz@shout + \immediate\openout\mmz@shout{\mmz@shname}% + }, + record/sh/new extern/.code={% + \begingroup + % Macro |\mmz@tex@extraction@systemcall| is customizable through |tex + % extraction command|, |tex extraction options| and |tex extraction + % script|. + \immediate\write\mmz@shout{\mmz@tex@extraction@systemcall}% + \endgroup + }, + record/sh/end/.code={% + \immediate\closeout\mmz@shout + }, +% \end{key} +% +% \begin{key}{record/bat/...} +% Rinse and repeat for Windows. + record/bat/begin/.code={% + \newwrite\mmz@batout + \immediate\openout\mmz@batout{\mmz@batname}% + }, + record/bat/new extern/.code={% + \begingroup + \immediate\write\mmz@batout{\mmz@tex@extraction@systemcall}% + \endgroup + }, + record/bat/end/.code={% + \immediate\closeout\mmz@batout + }, +% \end{key} +% +% \subsubsection{The Makefile} +% +% The implementation of the Makefile record type is the most complex so far, as +% we need to keep track of the targets. +% +% \begin{key}{makefile} +% This key sets the makefile filename. + makefile/.store in=\mmz@makefilename, + makefile=memoize-extract.\jobname.makefile, +} +% \end{key} +% +% We need to define a macro which expands to the tab character of catcode +% ``other'', to use as the recipe prefix. +\begingroup +\catcode`\^^I=12 +\gdef\mmz@makefile@recipe@prefix{^^I}% +\endgroup +% \begin{key}{record/makefile/...} +% Define the Makefile record type. +\mmzset{ + record/makefile/begin/.code={% + % We initialize the record type by opening the file and setting makefile + % variables |.DEFAULT_GOAL| and |.PHONY|. + \newwrite\mmz@makefileout + \newtoks\mmz@makefile@externs + \immediate\openout\mmz@makefileout{\mmz@makefilename}% + \immediate\write\mmz@makefileout{.DEFAULT_GOAL = externs}% + \immediate\write\mmz@makefileout{.PHONY: externs}% + }, + % The crucial part, writing out the extraction rule. The target comes first, + % then the recipe, which is whatever the user has set by |tex extraction + % command|, |tex extraction options| and |tex extraction script|. + record/makefile/new extern/.code={% + % The target extern file: + \immediate\write\mmz@makefileout{#1:}% + \begingroup + % The recipe is whatever the user set by |tex extraction + % command|, |tex extraction options| and |tex extraction script|. + \immediate\write\mmz@makefileout{% + \mmz@makefile@recipe@prefix\mmz@tex@extraction@systemcall}% + \endgroup + % Append the extern file to list of targets. + \xtoksapp\mmz@makefile@externs{#1\space}% + }, + record/makefile/end/.code={% + % Before closing the file, we list the extern files as the prerequisites of + % our phony default target, |externs|. + \immediate\write\mmz@makefileout{externs: \the\mmz@makefile@externs}% + \immediate\closeout\mmz@makefileout + }, +} +% \end{key} +% +% +% \subsection{\hologo{TeX}-based extraction} +% \label{sec:code:extract:tex} +% +% \begin{key}{extract/tex} +% We trigger the \hologo{TeX}-based extraction by inputting the |.mmz| record +% file. +\mmzset{ + extract/tex/.code={% + \begingroup + \@input{\jobname.mmz}% + \endgroup + }, +} +% \end{key} +% +% \begingroup +% \setlength\MacroIndent{2em} +% \begin{macro}{\mmzUsedCMemo,\mmzUsedCCMemo,\mmzUsedExtern,\mmzNewCMemo,\mmzNewCCMemo,\mmzPrefix} +% We can ignore everything but |\mmzNewExtern|s. All these macros receive a +% single argument. +\def\mmzUsedCMemo#1{} +\def\mmzUsedCCMemo#1{} +\def\mmzUsedExtern#1{} +\def\mmzNewCMemo#1{} +\def\mmzNewCCMemo#1{} +\def\mmzPrefix#1{} +% \end{macro} +% \endgroup +% +% \begin{macro}{\mmzNewExtern} +% Command |\mmzNewExtern| takes four arguments. It instructs us to extract +% page |#2| of document |\jobname.pdf| to file |#1|. During the extraction, +% we will check whether the size of the extern matches the given expected +% width (|#3|) and total height (|#4|). +% +% We perform the extraction by an embedded \hologo{TeX} call. The system +% command that gets executed is stored in |\mmz@tex@extraction@systemcall|, +% which is set by |tex extraction command| and friends; by default, we +% execute |pdftex|. +% +\def\mmzNewExtern#1{% + % The \hologo{TeX} executable expects the basename as the argument, so we + % strip away the |.pdf| suffix. + \mmz@new@extern@i#1\mmz@temp +} +\def\mmz@new@extern@i#1.pdf\mmz@temp#2#3#4{% + \begingroup + % Define the macros used in |\mmz@tex@extraction@systemcall|. + \def\externbasepath{#1}% + \def\pagenumber{#2}% + \def\expectedwidth{#3}% + \def\expectedheight{#4}% + % Empty out the extraction log. + \mmz@clear@extraction@log + % Extract. + \pdf@system{\mmz@tex@extraction@systemcall}% + % Was the extraction successful? We temporarily redefine the extraction + % error message macro (suited for the external extraction scripts, which + % extract all externs in one go) to report the exact problematic extern page. + \let\mmz@extraction@error\mmz@pageextraction@error + \mmz@check@extraction@log{tex}% + \endgroup +} +% \end{macro} +\def\mmz@pageextraction@error{% + \PackageError{memoize}{Extraction of extern page \pagenumber\space from + document "\mmzUnquote\jobname.pdf" using method "\extractionmethod" was unsuccessful. + Have you set the shell escape mode as suggested in chapter 1 of the + manual?}{If "\mmzvalueof{tex extraction command}" was executed, + shell escape mode is not the problem, and inspecting "\externbasepath.log" + might give you a clue what's wrong}} +% +% \begin{key}{tex extraction command, tex extraction options, tex extraction script} +% Using these keys, we set the system call +% which will be invoked for each extern page. The value of this key is +% expanded when executing the system command. The user may deploy the +% following macros in the value of these keys: +% \begin{itemize} +% \item |\externbasepath|: the extern PDF that should be produced, minus the +% |.pdf| suffix; +% \item |\pagenumber|: the page number to be extracted; +% \item |\expectedwidth|: the expected width of the extracted page; +% \item |\expectedheight|: the expected total height of the extracted page; +% \end{itemize} +\def\mmz@tex@extraction@systemcall{% + \mmzvalueof{tex extraction command}\space + \mmzvalueof{tex extraction options}\space + "\mmzvalueof{tex extraction script}"% +} +% \end{key} +% +% \paragraph{The default} system call for \hologo{TeX}-based extern extraction. +% As this method, despite being \hologo{TeX}-based, shares no code with the +% document, we're free to implement it with any engine and format we want. For +% reasons of speed, we clearly go for the plain \hologo{pdfTeX}.\footnote{I +% implemented the first version of \hologo{TeX}-based extraction using +% \hologo{LaTeX} and package \texttt{graphicx}, and it was (running with +% \hologo{pdfTeX} engine) almost four times slower than the current plain +% \hologo{TeX} implementation.} We perform the extraction by a little +% \hologo{TeX} script, |memoize-extract-one|, inputted at the end of the value +% given to |tex extraction script|. +\mmzset{ + tex extraction command/.initial=pdftex, + tex extraction options/.initial={% +% \begin{listingregion}{tex-extraction-options.tex} + -halt-on-error + -interaction=batchmode + -jobname "\externbasepath" + \ifdefempty\mmzOutputDirectory{}{-output-directory "\mmzOutputDirectory"} +% \end{listingregion} + }, + tex extraction script/.initial={% +% \begin{listingregion}{tex-extraction-script.tex} ^^A todo: context + \def\noexpand\fromdocument{"\mmzOutputDirectory"\jobname.pdf}% + \def\noexpand\pagenumber{\pagenumber}% + \def\noexpand\expectedwidth{\expectedwidth}% + \def\noexpand\expectedheight{\expectedheight}% + \def\noexpand\logfile{\jobname.mmz.log}% + \unexpanded{% + \def\warningtemplate{% + %\noexpand\PackageWarning{memoize}{\warningtext}% + %\warning{memoize: \warningtext}% + %\warning{memoize: \warningtext}% + }}% + \ifdef\XeTeXversion{}{% + \def\noexpand\mmzpdfmajorversion{\the\pdfmajorversion}% + \def\noexpand\mmzpdfminorversion{\the\pdfminorversion}% + }% + \noexpand\input memoize-extract-one +% \end{listingregion} + }, +} +% +% +% \subsubsection{\texttt{memoize-extract-one.tex}} +% +% The rest of the code of this section resides in file +% |memoize-extract-one.tex|. It is used to extract a single extern page from +% the document; it also checks whether the extern page dimensions are as +% expected, and passes a warning to the main job if that is not the case. For +% the reason of speed, the extraction script is in \hologo{plainTeX} format. +% For the same reason, it is compiled by \hologo{pdfTeX} engine by default, but +% we nevertheless take care that it will work with other (supported) engines as +% well. +% +%<*extract-one> +\catcode`\@11\relax +\def\@firstoftwo#1#2{#1} +\def\@secondoftwo#1#2{#2} +% +% Set the PDF version (maybe) passed to the script via |\mmzpdfmajorversion| +% and |\mmzpdfminorversion|. +\ifdefined\XeTeXversion +\else + \ifdefined\luatexversion + \def\pdfmajorversion{\pdfvariable majorversion}% + \def\pdfminorversion{\pdfvariable minorversion}% + \fi + \ifdefined\mmzpdfmajorversion + \pdfmajorversion\mmzpdfmajorversion\relax + \fi + \ifdefined\mmzpdfminorversion + \pdfminorversion\mmzpdfminorversion\relax + \fi +\fi +% Allocate a new output stream, always --- |\newwrite| is |\outer| and thus +% cannot appear in a conditional. +\newwrite\extractionlog +% Are we requested to produce a log file? +\ifdefined\logfile + \immediate\openout\extractionlog{\logfile}% + % Define a macro which both outputs the warning message and writes it to the + % extraction log. + \def\doublewarning#1{% + \message{#1}% + \def\warningtext{#1}% + % This script will be called from different formats, so it is up to the + % main job to tell us, by defining macro |\warningtemplate|, how to throw a + % warning in the log file. + \immediate\write\extractionlog{% + \ifdefined\warningtemplate\warningtemplate\else\warningtext\fi + }% + }% +\else + \let\doublewarning\message +\fi +\newif\ifforce +\ifdefined\force + \csname force\force\endcsname +\fi +% \begin{macro}{\mmz@if@roughly@equal} +% This macro checks whether the given dimensions (|#2| and |#3|) are equal +% within the tolerance given by |#1|. We use the macro both in the +% extraction script and in the main package. (We don't use |\ifpdfabsdim|, +% because it is unavailable in \hologo{XeTeX}.) +% +%<*mmz,extract-one> +\def\mmz@tolerance{0.01pt} +\def\mmz@if@roughly@equal#1#2#3{% + \dimen0=\dimexpr#2-#3\relax + \ifdim\dimen0<0pt + \dimen0=-\dimen0\relax + \fi + \ifdim\dimen0>#1\relax + \expandafter\@secondoftwo + \else + % The exact tolerated difference is, well, tolerated. This is a must to + % support |tolerance=0pt|. + \expandafter\@firstoftwo + \fi +}% +% +%<*extract-one> +% Grab the extern page from the document and put it in a box. +\ifdefined\XeTeXversion + \setbox0=\hbox{\XeTeXpdffile \fromdocument\space page \pagenumber media}% +\else + \ifdefined\luatexversion + \saveimageresource page \pagenumber mediabox {\fromdocument}% + \setbox0=\hbox{\useimageresource\lastsavedimageresourceindex}% + \else + \pdfximage page \pagenumber mediabox {\fromdocument}% + \setbox0=\hbox{\pdfrefximage\pdflastximage}% + \fi +\fi +% Check whether the extern page is of the expected size. +\newif\ifbaddimensions +\ifdefined\expectedwidth + \ifdefined\expectedheight + \mmz@if@roughly@equal{\mmz@tolerance}{\wd0}{\expectedwidth}{% + \mmz@if@roughly@equal{\mmz@tolerance}{\ht0}{\expectedheight}% + {}% + {\baddimensionstrue}% + }{\baddimensionstrue}% + \fi +\fi +% We'll setup the page geometry of the extern file and shipout the extern --- +% if all is well, or we're forced to do it. +\ifdefined\luatexversion + \let\pdfpagewidth\pagewidth + \let\pdfpageheight\pageheight + \def\pdfhorigin{\pdfvariable horigin}% + \def\pdfvorigin{\pdfvariable vorigin}% +\fi +\def\do@shipout{% + \pdfpagewidth=\wd0 + \pdfpageheight=\ht0 + \ifdefined\XeTeXversion + \hoffset -1 true in + \voffset -1 true in + \else + \pdfhorigin=0pt + \pdfvorigin=0pt + \fi + \shipout\box0 +} +\ifbaddimensions + \doublewarning{I refuse to extract page \pagenumber\space from + "\fromdocument", because its size (\the\wd0 \space x \the\ht0) is not + what I expected (\expectedwidth\space x \expectedheight)}% + \ifforce\do@shipout\fi +\else + \do@shipout +\fi +% If logging is in effect and the extern dimensions were not what we expected, +% write a warning into the log. +\ifdefined\logfile + \immediate\write\extractionlog{\noexpand\endinput}% + \immediate\closeout\extractionlog +\fi +\bye +% +% \end{macro} +% +% \section{Automemoization} +% \label{sec:code:automemoization} +% +% \paragraph{Install} the advising framework implemented by our auxiliary +% package Advice, which automemoization depends on. This will define keys +% |auto|, |activate| etc.\ in our keypath. +%<*mmz> +\mmzset{ + .install advice={setup key=auto, activation=deferred}, + % We switch to the immediate activation at the end of the preamble. + begindocument/before/.append style={activation=immediate}, +} +% +% \begin{key}{manual} +% Unless the user switched on |manual|, we perform the deferred +% (de)activations at the beginning of the document (and then clear the style, +% so that any further deferred activations will start with a clean slate). +% In \hologo{LaTeX}, we will use the latest possible hook, +% |begindocument/end|, as we want to hack into commands defined by other +% packages. (The \hologo{TeX} conditional needs to be defined before using +% it in |.append code| below. +\newif\ifmmz@manual +\mmzset{ + manual/.is if=mmz@manual, + begindocument/end/.append code={% + \ifmmz@manual + \else + \pgfkeysalso{activate deferred,activate deferred/.code={}}% + \fi + }, +% \end{key} +% +% \paragraph{Announce} Memoize's run conditions and handlers. + auto/.cd, + run if memoization is possible/.style={ + run conditions=\mmz@auto@rc@if@memoization@possible + }, + run if memoizing/.style={run conditions=\mmz@auto@rc@if@memoizing}, + apply options/.style={ + bailout handler=\mmz@auto@bailout, + outer handler=\mmz@auto@outer, + }, + memoize/.style={ + run if memoization is possible, + apply options, + inner handler=\mmz@auto@memoize + }, + %<*latex> + noop/.style={run if memoization is possible, noop \AdviceType}, + noop command/.style={apply options, inner handler=\mmz@auto@noop}, + noop environment/.style={ + outer handler=\mmz@auto@noop@env, bailout handler=\mmz@auto@bailout}, + % + %noop/.style={inner handler=\mmz@auto@noop}, + nomemoize/.style={noop, options=disable}, + replicate/.style={run if memoizing, inner handler=\mmz@auto@replicate}, +} +% +% \paragraph{Abortion} +% We cheat and let the |run conditions| do the work --- it is cheaper to just +% always abort than to invoke the outer handler. (As we don't set +% |\AdviceRuntrue|, the run conditions will never be satisfied.) +% \begin{listingregion}{_auto-abort.tex} +\mmzset{ + auto/abort/.style={run conditions=\mmzAbort}, +} +% \end{listingregion} +% And the same for |unmemoizable|: +\mmzset{ + auto/unmemoizable/.style={run conditions=\mmzUnmemoizable}, +} +% For one, we abort upon |\pdfsavepos| (called |\savepos| in \hologo{LuaTeX}). +% Second, unless in \hologo{LuaTeX}, we submit |\errmessage|, which allows us +% to detect at least some errors --- in \hologo{LuaTeX}, we have a more +% bullet-proof system of detecting errors, see |\mmz@memoize| in +% \S\ref{sec:code:memoization-process}. +\ifdef\luatexversion{% + \mmzset{auto=\savepos{abort}} +}{% + \mmzset{ + auto=\pdfsavepos{abort}, + auto=\errmessage{abort}, + } +} +% +% \begin{mmzautokey}{run if memoization is possible} +% \begin{macro}{\mmz@auto@rc@if@memoization@possible} +% These run conditions are used by |memoize| and |noop|: Memoize should be +% enabled, but we should not be already within Memoize, i.e.\ memoizing or +% normally compiling some code submitted to memoization. +% \begin{listingregion}{_auto-run-if-memoization-is-possible.tex} +\def\mmz@auto@rc@if@memoization@possible{% + \ifmemoize + \ifinmemoize + \else + \AdviceRuntrue + \fi + \fi +} +% \end{listingregion} +% \end{macro} +% \end{mmzautokey} +% +% \begin{mmzautokey}{run if memoizing} +% \begin{macro}{\mmz@auto@rc@if@memoizing} +% These run conditions are used by |\label| and |\ref|: they should +% be handled only during memoization (which implies that Memoize is enabled). +\def\mmz@auto@rc@if@memoizing{% + \ifmemoizing\AdviceRuntrue\fi +} +% \end{macro} +% \end{mmzautokey} +% +% \begin{macro}{\mmznext} +% The next-options, set by this macro, will be applied to the next, and only +% next instance of automemoization. We set the next-options globally, so +% that only the linear order of the invocation matters. Note that |\mmznext|, +% being a user command, must also be defined in package |nomemoize|. +% +%\def\mmznext#1{\ignorespaces} +%<*mmz> +\def\mmznext#1{\gdef\mmz@next{#1}\ignorespaces} +\mmznext{}% +% \end{macro} +% +% \begin{mmzautokey}{apply options} +% \begin{macro}{\mmz@auto@outer,\mmz@auto@bailout} +% The outer and the bailout handler defined here work as a team. The outer +% handler's job is to apply the auto- and the next-options; therefore, the +% bailout handler must consume the next-options as well. To keep the +% option application local, the outer handler opens a group, which is +% expected to be closed by the inner handler. This key is used by +% |memoize| and |noop command|. +% \begin{listingregion}{_auto-memoize-outer.tex} +\def\mmz@auto@outer{% + \begingroup + \mmzAutoInit + \AdviceCollector +} +% \end{listingregion} +% \begin{listingregion}{_auto-memoize-bailout.tex} +\def\mmz@auto@bailout{% + \mmznext{}% +} +% \end{listingregion} +% \bigskip +% \end{macro} +% \end{mmzautokey} +% +% \begin{macro}{\mmzAutoInit} +% Apply first the auto-options, and then the next-options (and clear the +% latter). Finally, if we have any extra collector options (set by the +% |verbatim| keys), append them to Advice's (raw) collector options. +\def\mmzAutoInit{% + \ifdefempty\AdviceOptions{}{\expandafter\mmzset\expandafter{\AdviceOptions}}% + \ifdefempty\mmz@next{}{\expandafter\mmzset\expandafter{\mmz@next}\mmznext{}}% + \eappto\AdviceRawCollectorOptions{\expandonce\mmzRawCollectorOptions}% +} +% \end{macro} +% +% \begin{mmzautokey}{memoize} +% \begin{macro}{\mmz@auto@memoize} +% This key installs the inner handler for memoization. If you compare this +% handler to the definition of |\mmz| in +% section~\ref{sec:code:memoization:manual}, you will see that the only +% thing left to do here is to start memoization with |\Memoize|, everything +% else is already done by the advising framework, as customized by Memoize. +% +% The first argument to |\Memoize| is the memoization key (which the code +% md5sum is computed off of); it consists of the handled code (the contents +% of |\AdviceReplaced|) and its arguments, which were collected into |##1|. +% The second argument is the code which the memoization driver will +% execute. |\AdviceOriginal|, if invoked right away, would execute the +% original command; but as this macro is only guaranteed to refer to this +% command within the advice handlers, we expand it before calling |\Memoize|. +% that command. +% +% Note that we don't have to define different handlers for commands and +% environments, and for different \hologo{TeX} formats. When memoizing +% command |\foo|, |\AdviceReplaced| contains |\foo|. When memoizing +% environment |foo|, |\AdviceReplaced| contains |\begin{foo}|, |\foo| or +% |\startfoo|, depending on the format, while the closing tag +% (|\end{foo}|, |\endfoo| or |\stopfoo|) occurs at the end of the +% collected arguments, because |apply options| appended +% |\collargsEndTagtrue| to |raw collector options|. +% +% This macro has no formal parameters, because the collected arguments will +% be grabbed by |\mmz@marshal|, which we have to go through because +% executing |\Memoize| closes the memoization group and we lose the current +% value of |\ifmmz@ignorespaces|. (We also can't use |\aftergroup|, +% because closing the group is not the final thing |\Memoize| does.) +% \begin{listingregion}{_auto-memoize-inner.tex} +\long\def\mmz@auto@memoize#1{% + \expanded{% + \noexpand\Memoize + {\expandonce\AdviceReplaced\unexpanded{#1}}% + {\expandonce\AdviceOriginal\unexpanded{#1}}% + \ifmmz@ignorespaces\ignorespaces\fi + }% +} +% \end{listingregion} +% \end{macro} +% \end{mmzautokey} +% +% \begin{mmzautokey}{noop} +% \begin{macro}{\mmz@auto@noop,\mmz@auto@noop@env} +% The no-operation handler can be used to apply certain options for the +% span of the execution of the handled command or environment. This is +% exploited by |auto/nomemoize|, which sets |disable| as an auto-option. +% +% The handler for commands and non-\hologo{LaTeX} environments is +% implemented as an inner handler. On its own, it does nothing except +% honor |verbatim| and |ignore spaces| (only takes care of |verbatim| and +% |ignore spaces| (in the same way as the memoization handler above), but +% it is intended to be used alongside the default outer handler, which +% applies the auto- and the next-options. As that handler opens a group +% (and this handler closes it), we have effectively delimited the effect of +% those options to this invocation of the handled command or environment. +\long\def\mmz@auto@noop#1{% + \expandafter\mmz@maybe@scantokens\expandafter{\AdviceOriginal#1}% + \expandafter\endgroup + \ifmmz@ignorespaces\ignorespaces\fi +} +% In \hologo{LaTeX}, and only there, commands and environments need separate +% treatment. As \hologo{LaTeX} environments introduce a group of their own, we +% can simply hook our initialization into the beginning of the environment (as +% a one-time hook). Consequently, we don't need to collect the environment +% body, so this can be an outer handler. +%<*latex> +\def\mmz@auto@noop@env{% + \AddToHookNext{env/\AdviceName/begin}{% + \mmzAutoInit + \ifmmz@ignorespaces\ignorespacesafterend\fi + }% + \AdviceOriginal +} +% +% \end{macro} +% \end{mmzautokey} +% +% \begin{mmzautokey}{replicate} +% \begin{macro}{\mmz@auto@replicate} +% This inner handler writes a copy of the handled command or environment's +% invocation into the cc-memo (and then executes it). As it is used +% alongside |run if memoizing|, the replicated command in the cc-memo will +% always execute the original command. The system works even if +% replication is off when the cc-memo is input; in that case, the control +% sequence in the cc-memo directly executes the original command. +% +% This handler takes an option, |expanded| --- if given, the collected +% arguments will be expanded (under protection) before being written into +% the cc-memo. +\def\mmz@auto@replicate#1{% + \begingroup + \let\mmz@auto@replicate@expansion\unexpanded + \expandafter\pgfqkeys\expanded{{/mmz/auto/replicate}{\AdviceOptions}}% + %\let\protect\noexpand + \expanded{% + \endgroup + \noexpand\gtoksapp\noexpand\mmzCCMemo{% + \expandonce\AdviceReplaced\mmz@auto@replicate@expansion{#1}}% + \expandonce\AdviceOriginal\unexpanded{#1}% + }% +} +\pgfqkeys{/mmz/auto/replicate}{ + expanded/.code={\let\mmz@auto@replicate@expansion\@firstofone}, +} +% \end{macro} +% \end{mmzautokey} +% +% \subsection{\hologo{LaTeX}-specific handlers} +% +% We handle cross-referencing (both the |\label| and the |\ref| side) and +% indexing. Note that the latter is a straightforward instance of replication. +% +%<*latex> +% \begin{listingregion}{_auto-memoize-ref.tex} +\mmzset{ + auto/.cd, + ref/.style={outer handler=\mmz@auto@ref\mmzNoRef, run if memoizing}, + force ref/.style={outer handler=\mmz@auto@ref\mmzForceNoRef, run if memoizing}, +} +% \end{listingregion} +\mmzset{ + auto=\ref{ref}, + auto=\pageref{ref}, + auto=\label{run if memoizing, outer handler=\mmz@auto@label}, + auto=\index{replicate, args=m, expanded}, +} +% +% \begin{mmzautokey}{ref, force ref} +% \begin{macro}{\mmz@auto@ref} +% These keys install an outer handler which appends a cross-reference to the +% context. |force ref| does this even if the reference key is undefined, +% while |ref| aborts memoization in such a case --- the idea is that it makes +% no sense to memoize when we expect the context to change in the next +% compilation anyway. +% +% Any command taking a mandatory braced reference key argument potentially +% preceded by optional arguments of (almost) any kind may be submitted to +% these keys. This follows from the parameter list of |\mmz@auto@ref@i|, +% where |#2| grabs everything up to the first opening brace. The downside of +% the flexibility regarding the optional arguments is that unbraced +% single-token reference keys will cause an error, but as such usages of +% |\ref| and friends should be virtually inexistent, we let the bug stay. +% +% |#1| should be either |\mmzNoRef| or |\mmzForceNoRef|. |#2| will receive +% any optional arguments of |\ref| (or |\pageref|, or whatever), and |#3| in +% |\mmz@auto@ref@i| is the cross-reference key. +% \begin{listingregion}{_auto-memoize-ref.tex} +\def\mmz@auto@ref#1#2#{\mmz@auto@ref@i#1{#2}} +\def\mmz@auto@ref@i#1#2#3{% + #1{#3}% + \AdviceOriginal#2{#3}% +} +% \end{listingregion} +% \end{macro} +% \end{mmzautokey} +% +% \begin{macro}{\mmzForceNoRef,\mmzNoRef} +% These macros do the real job in the outer handlers for cross-referencing, +% but it might be useful to have them publicly available. |\mmzForceNoRef| +% appends the reference key to the context. |\mmzNoRef| only does that if +% the reference is defined, otherwise it aborts the memoization. +\def\mmzForceNoRef#1{% + \ifmemoizing + \expandafter\gtoksapp\expandafter\mmzContextExtra + \else + \expandafter\toksapp\expandafter\mmzContext + \fi + {r@#1={\csname r@#1\endcsname}}% + \ignorespaces +} +\def\mmzNoRef#1{% + \ifcsundef{r@#1}{\mmzAbort}{\mmzForceNoRef{#1}}% + \ignorespaces +} +% \end{macro} +% +% \begin{mmzautokey}{refrange, force refrange} +% \begin{macro}{\mmz@auto@refrange} +% Let's rinse and repeat for reference ranges. The code is virtually the +% same as above, but we grab two reference key arguments (|#3| and |#4|) in +% the final macro.\indentmacrocode +\mmzset{ + auto/.cd, + refrange/.style={outer handler=\mmz@auto@refrange\mmzNoRef, + bailout handler=\relax, run if memoizing}, + force refrange/.style={outer handler=\mmz@auto@refrange\mmzForceNoRef, + bailout handler=\relax, run if memoizing}, +} +% \noindentmacrocode +\def\mmz@auto@refrange#1#2#{\mmz@auto@refrange@i#1{#2}} +\def\mmz@auto@refrange@i#1#2#3#4{% + #1{#3}% + #1{#4}% + \AdviceOriginal#2{#3}{#4}% +} +% \end{macro} +% \end{mmzautokey} +% +% \begin{mmzautokey}{multiref, force multiref} +% \begin{macro}{\mmz@auto@multiref} +% And one final time, for ``multi-references'', such as |cleveref|'s +% |\cref|, which can take a comma-separated list of reference keys in the +% sole argument. Again, only the final macro is any different, this time +% distributing |#1| (|\mmzNoRef| or |\mmzForceNoRef|) over |#3| by +% |\forcsvlist|. +\mmzset{ + auto/.cd, + multiref/.style={outer handler=\mmz@auto@multiref\mmzNoRef, + bailout handler=\relax, run if memoizing}, + force multiref/.style={outer handler=\mmz@auto@multiref\mmzForceNoRef, + bailout handler=\relax, run if memoizing}, +} +\def\mmz@auto@multiref#1#2#{\mmz@auto@multiref@i#1{#2}} +\def\mmz@auto@multiref@i#1#2#3{% + \forcsvlist{#1}{#3}% + \AdviceOriginal#2{#3}% +} +% \end{macro} +% \end{mmzautokey} +% +% \begin{macro}{\mmz@auto@label} +% The outer handler for \cs{label} must be defined specifically for this +% command. The generic replicating handler is not enough here, as we need to +% replicate both the invocation of |\label| and the definition of +% |\@currentlabel|. +\def\mmz@auto@label#1{% + \xtoksapp\mmzCCMemo{% + \noexpand\mmzLabel{#1}{\expandonce\@currentlabel}% + }% + \AdviceOriginal{#1}% +} +% \end{macro} +% +% \begin{macro}{\mmzLabel} +% This is the macro that |\label|'s handler writes into the cc-memo. The +% first argument is the reference key; the second argument is the value of +% |\@currentlabel| at the time of invocation |\label| during memoization, +% which this macro temporarily restores. +\def\mmzLabel#1#2{% + \begingroup + \def\@currentlabel{#2}% + \label{#1}% + \endgroup +} +% +% +% +% \end{macro} +% +% \section{Support for various classes and packages} +% +% \subsection{\TikZ;} +% \label{sec:code:mmz:tikz} +% +% In this section, we activate \TikZ; support (the collector is defined by +% Advice). All the action happens at the end of the preamble, so that we can +% detect whether \TikZ; was loaded (regardless of whether Memoize was loaded +% before \TikZ;, or vice versa), but still input the definitions. +%<*mmz> +\mmzset{ + begindocument/before/.append code={% + %\@ifpackageloaded{tikz}{% + %\ifdefined\tikz + \input advice-tikz.code.tex + %}{}% + %\fi + % We define and activate the automemoization handlers for the \TikZ; command + % and environment. + \mmzset{% + % \begin{listingregion}{_auto-tikz-collector.tex} + auto=\tikz{memoize, collector=\AdviceCollectTikZArguments}, + % \end{listingregion} + %/utils/exec={\tracingall}, + auto={tikzpicture}{memoize}, + % A hack to prevent memoizing pictures which are accidentally marked as + % remembered --- accidentally in the sense that because the document + % changed, the |.aux| file contains a |\pgfsyspdfmark| command which + % erroneously refers to the picture being memoized. We know that + % memoizing a remembered picture can't be right, as we always abort on + % |\pdfsavepos|. This is implemented by hacking into PGF's + % |\pgfsys@getposition|, and aborting memoization if the mark does not + % equal |\relax|. (We have to duplicate |#| because of |.append code|.) + auto=\pgfsys@getposition{ + run if memoizing, outer handler=\mmz@pgfsys@getposition}, + }% + \def\mmz@pgfsys@getposition##1{% + \expandafter\ifx\csname pgf@sys@pdf@mark@pos@##1\endcsname\relax + \else + \mmzAbort + \fi + \AdviceOriginal{##1}% + }% + }, +} +% +% +% +% \subsection{Forest} +% \label{sec:code:forest} +% +% Forest will soon feature extensive memoization support, but for now, let's +% just enable the basic, single extern externalization. +%<*mmz> +%<*latex> +\mmzset{ + begindocument/before/.append code={% + \@ifpackageloaded{forest}{% + \mmzset{ + auto={forest}{memoize}, +% Yes, |\Forest| is defined using |xparse|. + auto=\Forest{memoize}, + }% + }{}% + }, +} +% +% +% \subsection{Beamer} +% \label{sec:code:beamer} +% +% The Beamer code is explained in \MS\ref{sec:per-overlay}. +% +%<*latex> +\AddToHook{begindocument/before}{\@ifclassloaded{beamer}{% + % \begin{listingregion}{per-overlay.tex} + \mmzset{per overlay/.style={ + /mmz/context={% + overlay=\csname beamer@overlaynumber\endcsname, + pauses=\ifmemoizing + \mmzBeamerPauses + \else + \expandafter\the\csname c@beamerpauses\endcsname + \fi + }, + /mmz/at begin memoization={% + \xdef\mmzBeamerPauses{\the\c@beamerpauses}% + \xtoksapp\mmzCMemo{% + \noexpand\mmzSetBeamerOverlays{\mmzBeamerPauses}{\beamer@overlaynumber}}% + \gtoksapp\mmzCCMemo{% + \only<\mmzBeamerOverlays>{}}% + }, + /mmz/at end memoization={% + \xtoksapp\mmzCCMemo{% + \noexpand\setcounter{beamerpauses}{\the\c@beamerpauses}}% + }, + /mmz/per overlay/.code={}, + }} + \def\mmzSetBeamerOverlays#1#2{% + \ifnum\c@beamerpauses=#1\relax + \gdef\mmzBeamerOverlays{#2}% + \ifnum\beamer@overlaynumber<#2\relax \mmz@temptrue \else \mmz@tempfalse \fi + \else + \mmz@temptrue + \fi + \ifmmz@temp + \appto\mmzAtBeginMemoization{% + \gtoksapp\mmzCMemo{\mmzSetBeamerOverlays{#1}{#2}}}% + \fi + }% + % \end{listingregion} +}{}} +% +% +% +% \section{Initialization} +% \label{sec:code:initialization} +% +% \begin{key}{begindocument/before, begindocument, begindocument/end, +% enddocument/afterlastpage} +% These styles contain the initialization +% and the finalization code. They were populated throughout the source. +% Hook |begindocument/before| contains the package support code, which must +% be loaded while still in the preamble. Hook |begindocument| contains the +% initialization code whose execution doesn't require any particular timing, +% as long as it happens at the beginning of the document. Hook +% |begindocument/end| is where the commands are activated; this must +% crucially happen as late as possible, so that we successfully override +% foreign commands (like |hyperref|'s definitions). In \hologo{LaTeX}, we +% can automatically execute these hooks at appropriate places: +%<*latex> +\AddToHook{begindocument/before}{\mmzset{begindocument/before}} +\AddToHook{begindocument}{\mmzset{begindocument}} +\AddToHook{begindocument/end}{\mmzset{begindocument/end}} +\AddToHook{enddocument/afterlastpage}{\mmzset{enddocument/afterlastpage}} +% +% In \hologo{plainTeX}, the user must execute these hooks manually; but at +% least we can group them together and given them nice names. Provisionally, +% manual execution is required in \hologo{ConTeXt} as well, as I'm not sure +% where to execute them --- please help! +%<*plain,context> +\mmzset{ + begin document/.style={begindocument/before, begindocument, begindocument/end}, + end document/.style={enddocument/afterlastpage}, +} +% +% \end{key} +% +% \paragraph{\texttt{memoize.cfg}} Load the configuration file. Note that +% |nomemoize| must input this file as well, because any special +% memoization-related macros defined by the user should be available; for +% example, my |memoize.cfg| defines |\ifregion| (see +% \MS\ref{sec:tut:multifile}). +% +%\InputIfFileExists{memoize.cfg}{}{} +%<*mmz> +% +% Formats other than \hologo{plainTeX} need a way to prevent extraction during +% package-loading. +\mmzset{ + %extract/no/.code={}, +% \begin{key}{output directory} +% \begin{macro}{mmzOutputDirectory} +% Set the |-output-directory| --- manually, as there is no other way. +% \indentmacrocode + output-directory/.store in=\mmzOutputDirectory, +} +% \end{macro} +% \end{key} +% +% \paragraph{Process} the package options (except in \hologo{plainTeX}). +%\ProcessPgfPackageOptions{/mmz} +%\expandafter\mmzset\expandafter{\currentmoduleparameters} +% +% Define |\mmzOutputDirectory| if |output-directory| was not given. +\ifdefined\mmzOutputDirectory +\else + \def\mmzOutputDirectory{}% +\fi +\mmzset{output directory/.code={\PackageError{memoize}{Key "output-directory" + may only be used as a package option}{}}} +\mmzset{ + % \paragraph{Extract} the externs using the method given by |memoize.cfg| or + % the package options --- unless we're running \hologo{plainTeX}. + %<*!plain> + extract/\mmz@extraction@method, + % In non-\hologo{plainTeX} formats, also disable |extract| in the preamble. + extract/.code={\PackageError{memoize}{Key "extract" is only allowed as a + package option.}{If you really want to extract in the preamble, execute + "extract/".}}, + % + %<*plain> + % In \hologo{plainTeX}, where extraction must be invoked after + % loading the package, we now have to redefine |extract|, so that it will + % immediately trigger extraction. + extract/.is choice, + extract/.default=\mmz@extraction@method, + % But only once: + extract/.append style={ + extract/.code={\PackageError{memoize}{Key "extract" is only allowed to + be used once.}{If you really want to extract again, execute + "extract/".}}, + }, + % +} +% +% Memoize was not really born for the draft mode, as it cannot produce new +% externs there. But we don't want to disable the package, as utilization is +% still perfectly valid in this mode, so let's just warn the user. +\ifnum\pdf@draftmode=1 + \PackageWarning{memoize}{No memoization will be performed in the draft mode}% +\fi +% +% +% \paragraph{The end} of |memoize|, |nomemoize| and |memoizable|. +%<*mmz,nommz,mmzable> +%\resetatcatcode +%\stopmodule +%\protect +% +% That's all, folks! +% +% \end{macrocode} +% +% \endinput +% +% Local Variables: +% TeX-engine: luatex +% TeX-master: "doc/memoize-code.tex" +% TeX-auto-save: nil +% End: \ No newline at end of file -- cgit v1.2.3