\ProvidesPackage{robust-externalize}[2024/03/15 v2.9 Cache anything (tikz, latex, python) in a robust, efficient and pure way.] % todo: % change order argument replace from list, it is hard to read this way \RequirePackage{pgfkeys} % We use the /robExt/... path to store our keys. \RequirePackage{pgffor} % For the .list keys \RequirePackage{graphicx} % For the includegraphics command \RequirePackage{verbatim} % For the \verbatim command, useful for debugging purpose for instance \RequirePackage{xsimverb} % To easily write verbatim code to files \RequirePackage{etoolbox} % To easily write verbatim code to files \RequirePackage{iftex} % computing md5 sum differs on lualatex/pdflatex/xelatex \RequirePackage{xparse} % solve https://github.com/leo-colisson/robust-externalize/issues/6 \RequirePackage{atveryend} % to compile in parallel all images %\usepackage{etl} % For auto forward https://tex.stackexchange.com/a/700844/116348 %% TODO list: % - provide an easy way to use cross-ref, bibtex etc (we just need to add them when writing the file) without recompiling the whole document (we don't want to lose the cache everytime a new bib entry is added) but while preserving. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%% Utils %%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % If the file contains a space, the jobname gets quotes around... % https://tex.stackexchange.com/questions/418670/avoid-quotation-marks-when-using-jobname-or-currfilename \newcommand*{\jobnameNoQuotes}{} \newcommand*{\setjobnameNoQuotes}{% \edef\jobnameNoQuotes{\jobname}% \expandafter\expandafter\expandafter\setjobnameNoQuotesaux \expandafter\jobnameNoQuotes\expandafter"\jobnameNoQuotes"\relax } \newcommand*{\setjobnameNoQuotesaux}{} \def\setjobnameNoQuotesaux#1"#2"#3\relax{\def\jobnameNoQuotes{#2}} \setjobnameNoQuotes \ExplSyntaxOn \NewDocumentCommand{\robExtIfWindowsTF}{mm}{% \sys_if_platform_windows:TF {#1}{#2}% } % This will be printed in from of the error message, mostly useful in texstudio that only print lines % containing errors \def\robExtPrefixLogMessage{} % Prints the next 4 lines after an error: \def\robExtLinesAfterError{3} \def\robExtRemoveLineNumber{} %% Needed for studio to prepend ! in front of all lines, including big ones. %% https://tex.stackexchange.com/questions/705502/message-add-symbol-in-front-of-new-lines-even-if-there-is-a-line-break \def\robExtMessageWithPrefixNumberLines{^^J}% \cs_new:Nn \robExt__message_with_prefix:n { \str_set:Nn \l_tmpa_str {#1} \int_compare:nNnTF {\str_count:N \l_tmpa_str + \str_count:N \robExtPrefixLogMessage} > {78} { \typeout{\robExtPrefixLogMessage \str_range:Nnn \l_tmpa_str {1} {78 - \str_count:N \robExtPrefixLogMessage}\robExtMessageWithPrefixNumberLines} \robExt__message_with_prefix:x {\str_range:Nnn \l_tmpa_str {78 - \str_count:N \robExtPrefixLogMessage + 1} {-1}} }{ \typeout{\robExtPrefixLogMessage \l_tmpa_str \robExtMessageWithPrefixNumberLines} } } \cs_generate_variant:Nn \robExt__message_with_prefix:n { x } % We might want to remove l.XX in front of lines as emacs interpret this as a line of error and goes to a wrong place. \cs_new:Nn \robExt__clean_line_if_needed:n { \ifdefined\robExtRemoveLineNumber% \str_compare:eNeTF {\str_range:nnn {#1} {1} {2}} = {l.} { \str_range:nnn {#1} {3} {-1}% } { #1 } \else #1 \fi } \cs_generate_variant:Nn \robExt__clean_line_if_needed:n { V } \cs_new:Nn \robExt_get_errors_from_file:n { \ior_open:Nn \g__robExt_read_ior {#1}% \cs_undefine:N \l__robExt_I_saw_an_error: \str_clear_new:N \l__robExt_tmp_str% \int_set:Nn \l_tmpa_int {0} \ior_str_map_inline:Nn \g__robExt_read_ior {% \ifdefined\robExtPrintWholeFile \str_gput_right:Nx \l__robExt_tmp_str {\tl_to_str:n{##1}^^J}% \cs_set:Nn \l__robExt_I_saw_an_error: {} \int_set:Nn \l_tmpa_int {\robExtLinesAfterError} \else \str_if_in:xnTF {\str_lowercase:n{##1}} {error} { \str_gput_right:Nx \l__robExt_tmp_str {\robExt__clean_line_if_needed:V{\tl_to_str:n{##1}^^J}}% \cs_set:Nn \l__robExt_I_saw_an_error: {} \int_set:Nn \l_tmpa_int {\robExtLinesAfterError} } { \str_if_in:xnTF {\str_lowercase:n{##1}} {invalid} { \str_gput_right:Nx \l__robExt_tmp_str {\robExt__clean_line_if_needed:V{\tl_to_str:n{##1}^^J}}% \cs_set:Nn \l__robExt_I_saw_an_error: {} \int_set:Nn \l_tmpa_int {\robExtLinesAfterError} } { % Check if it starts with a ! (usually latex errors start with this symbol) \str_compare:eNeTF {\str_range:nnn {##1} {1} {1}} = {!} { \str_gput_right:Nx \l__robExt_tmp_str {\robExt__clean_line_if_needed:V{\tl_to_str:n{##1}^^J}}% \cs_set:Nn \l__robExt_I_saw_an_error: {} \int_set:Nn \l_tmpa_int {\robExtLinesAfterError} }{ % Check if we saw an error not too long ago: \int_compare:nNnTF {\l_tmpa_int} > {0} { \str_gput_right:Nx \l__robExt_tmp_str {\robExt__clean_line_if_needed:V{\tl_to_str:n{##1}^^J}}% } { \str_set:Nn \l_tmp_str {##1} } \int_set:Nn \l_tmpa_int {\l_tmpa_int - 1} } } } \fi }% \cs_if_exist:NTF \l__robExt_I_saw_an_error: {} { % We print the last line if we found no error. Might be useful for errors like: % sh: ligne 1: gnuplot : commande introuvable. \str_set_eq:NN \l__robExt_tmp_str \l_tmp_str } } \cs_new:Nn \robExt_write_file_in_log:n { \ior_open:Nn \g__robExt_read_ior {#1}% \ior_str_map_inline:Nn \g__robExt_read_ior {% \ifdefined\robExtPrintWholeFile \ifdefempty{\robExtPrefixLogMessage}{ \typeout{\tl_to_str:n{##1}}% }{ \robExt__message_with_prefix:x {\tl_to_str:n{##1}} } \else \typeout{\tl_to_str:n{##1}}% \fi }% } \cs_generate_variant:Nn \robExt_write_file_in_log:n { x } \ExplSyntaxOff % Debug function: by default, do nothing \def\robExtDebugMessage#1{} \def\robExtDebugInfo#1{\message{#1}} \def\robExtDebugWarning#1{\message{^^J[robExt warning] #1}} \NewDocumentCommand{\robExtConfigure}{m}{% \pgfqkeys{/robExt}{#1}% faster than \pgfkeys{/robExt/.cd,#1} } % https://tex.stackexchange.com/questions/690700/latex3-elegant-way-to-forward-a-variable-outside-of-the-group % modified to deal with csname instead \def\robExtKeepaftergroup#1{% %\global \expandafter \expandafter \let \csname x:#1\endcsname =\csname #1\endcsname \global \expanded{\noexpand \let \expandafter\noexpand\csname x:#1\endcsname =\expandafter\noexpand\csname #1\endcsname}% \aftergroup\let% \expandafter\aftergroup\csname #1\endcsname% \expandafter\aftergroup \csname x:\string#1\endcsname% } \ExplSyntaxOn %%% %%% Expl3 variants %%% \cs_generate_variant:Nn \str_replace_all:Nnn { Nnx, Nnv, cnx, cxx, NnV, Nnx, Nnv } \cs_generate_variant:Nn \str_set:Nn { NV } \cs_generate_variant:Nn \str_if_in:nnTF { xnTF } \cs_generate_variant:Nn \tl_rescan:nn { nv } \cs_generate_variant:Nn \tl_set_rescan:Nnn { Nnv, NnV } \cs_generate_variant:Nn \tl_gset_rescan:Nnn { cnv } \cs_generate_variant:Nn \seq_remove_all:Nn { NV } \cs_generate_variant:Nn \iow_now:Nn { NV,Nx } \cs_generate_variant:Nn \iow_open:Nn { Nx } \cs_generate_variant:Nn \ior_str_get:NN { Nc } \cs_generate_variant:Nn \file_if_exist:nTF { xTF } \cs_generate_variant:Nn \file_mdfive_hash:n { x } \cs_generate_variant:Nn \cs_replacement_spec:N { c } \cs_generate_variant:Nn \cs_argument_spec:N { c } \cs_generate_variant:Nn \str_count_ignore_spaces:n { V } \cs_generate_variant:Nn \str_count_spaces:n { e } \cs_generate_variant:Nn \regex_extract_all:nnN { VVN, nVN, VVN } \cs_generate_variant:Nn \regex_extract_all:NnN { NVN } \cs_generate_variant:Nn \regex_match:nnTF { nVTF } \cs_generate_variant:Nn \seq_set_split_keep_spaces:Nnn {Nnv} %%% %%% String manipulation %%% \cs_set:Nn \robExt_set_hash_robust:Nn { \str_set:Nn {#1} {#2} \str_replace_all:Nnx {#1} { ## } { \c_hash_str } } \cs_generate_variant:Nn \robExt_set_hash_robust:Nn { cn } \cs_generate_variant:Nn \robExt_set_hash_robust:Nn { cx } \NewDocumentCommand{\robExt@set@hash@robust}{mm}{\robExt_set_hash_robust:Nn #1 {#2}} % Double the number of hashes... quite dirty but cannot find any solution or the user need to double it itself %% See also https://tex.stackexchange.com/questions/695432/latex3-latex-doubles-the-number-of-hashes-when-storing-them-in-string/695460#695460 \NewDocumentCommand{\robExtStrSetDoubleHash}{mm}{ \str_set:Nn {#1} {#2} \str_replace_all:Nnx {#1} { ## } { \c_hash_str \c_hash_str } } \NewDocumentCommand{\robExtStrSetHashRobust}{mm}{ \robExt_set_hash_robust:Nn {#1} {#2} } \NewDocumentCommand{\robExtRescanHashRobust}{m}{ \robExt_set_hash_robust:Nn \l__robExt_tmp_str {#1} \tl_rescan:nv {}{ l__robExt_tmp_str } } %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%% Error messages %%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% In LaTeX3 error messages are defined here, this allows people to translate them, filter them, change them, %% make them more verbose etc. \msg_new:nnn {robExt}{underscore in name}{Error:~all~the~placeholders~should~contain~two~consecutive~underscores~in~their~name.} \msg_new:nnn {robExt}{placeholder not existing}{[robExtGetPlaceholderInResultFromSinglePlaceholder] ~ The ~ placeholder ~ #1 ~ does ~ not ~ exists.} \msg_new:nnn {robExt}{forgot template}{You ~ are ~ writing ~ __ROBEXT_TEMPLATE__ ~ to ~ your ~ file: ~ seems ~ like ~ you ~ forgot ~ to ~ define ~ your ~ template ~ or ~ set ~ a ~ preset} \msg_new:nnn {robExt}{need shell escape}{You ~ need ~ to ~ compile ~ with ~ shell-escape ~ as ~ in: ~ "pdflatex ~ -shell-escape ~ yourfile.tex" ~ to ~ be ~ able ~ to ~ compile ~ automatically ~ the ~ figures} \msg_new:nnn {robExt}{need shell escape with}{You ~ need ~ to ~ compile ~ with ~ shell-escape ~ as ~ in: ~ "pdflatex ~ -shell-escape ~ yourfile.tex" ~ to ~ be ~ able ~ to ~ #1} \msg_new:nnn {robExt}{need shell escape parallel}{You ~ need ~ to ~ compile ~ with ~ shell-escape ~ as ~ in: ~ "pdflatex ~ -shell-escape ~ yourfile.tex" ~ to ~ be ~ able ~ to ~ compile ~ automatically ~ the ~ figures ~ in ~ parallel} \msg_new:nnn {robExt}{missing compiled pdf parallel}{The~compilation~of~block~at~line~#2~failed~as~the~following ~ file~is~missing: ~ #1.pdf. ~ The ~ compilation ~ command ~ "#3"~used~to~compile~the~environment~on~line~#2~ certainly ~ failed, ~ see ~ logs ~ above ~ or ~ in ~ #1.log.} \msg_new:nnn {robExt}{dependency does not exist}{The~dependency~#1~does~not~exist.} \msg_new:nnn {robExt}{missing compiled pdf parallel with log}{The~compilation~of~the~code~block~at~line~#2~failed:~the~following ~ file~is~indeed~missing: ~ #1.pdf. ~ The ~ compilation ~ command ~ "#3"~used~to~compile~the~environment~on~line~#2~ certainly ~ failed~with~errors:^^Jvvvvvv^^J\l__robExt_tmp_str^^J\string^\string^\string^\string^\string^\string^ ^^JSee~full~logs~#4~ or ~ in ~ #1-compilation.log.} \msg_new:nnn {robExt}{remove spaces until non spaces characters}{The~placeholder~#1~contains~characters~other~than~spaces~(#2)~before~the~separator~#3.} \msg_new:nnn {robExt}{auto forward not in cachemecode}{Auto~forward~is~less~efficient~in~cacheMeCode.} \msg_new:nnn {robExt}{file does not exist}{The~file~#1~does~not~exist.} % warnings \msg_new:nnn {robExt}{recommend two underscores in placeholders}{We~recommend~to~use~only~placeholders~containing~two~consecutive~underscores~in~their~name} \msg_new:nnn {robExt}{enabled parallel no shell escape}{Warning:~you~enabled~parallel~compilation~but~shell-escape~is~disabled.} \msg_new:nnn {robExt}{rerun because parallel}{Warning:~Compiling~all~missing~figures~in~parallel~with~"#1".~You~need~to~rerun~LaTeX~to~include~them.} \msg_new:nnn {robExt}{gpgetvar recompilation needed}{Warning:~you~need~to~recompile~as~the~gpgetvar~variable~"#1"~does~not~exist~yet.} \msg_new:nnn {robExt}{warning res not defined}{Warning:~the~result~\\res{#1}~is~not~defined,~maybe~try~to~recompile..} % dummy placeholders added if image is not present \def\robExtImagePlaceholderIfManualMode{ \framebox[\linewidth] { \begin{minipage}{\linewidth} \textbf{Manual ~ mode: ~ either ~ compile ~ with ~ \texttt{-shell-escape} ~ or ~ compile:\newline \texttt{\robExtAddCachePathAndName{\robExtFinalHash.tex}}\newline via ~ \newline \texttt{\l__robExt_current_compilation_command_str}\newline or ~ call ~ \newline\texttt{bash ~ \jobnameNoQuotes-\robExtAddPrefixName{compile-missing-figures.sh}}\newline to ~ compile ~ all ~ missing ~ figures.} \end{minipage} } } \def\robExtImagePlaceholderIfFallbackMode{ \framebox[\linewidth] { \begin{minipage}{\linewidth} \textbf{Falling ~ back ~ to ~ draft ~ mode: ~ either ~ compile ~ with ~ \texttt{-shell-escape} ~ or ~ compile:\newline \texttt{\robExtAddCachePathAndName{\robExtFinalHash.tex}}\newline via ~ \newline \texttt{\l__robExt_current_compilation_command_str}\newline or ~ call ~ \newline\texttt{bash ~ \jobnameNoQuotes-\robExtAddPrefixName{compile-missing-figures.sh}}\newline to ~ compile ~ all ~ missing ~ figures.} \end{minipage} } } \def\robExtImagePlaceholderIfParallelCompilation{ \framebox[\linewidth] { \begin{minipage}{\linewidth} \textbf{The~file:\\ \texttt{\robExtAddCachePathAndName{\robExtFinalHash.tex}}\\ is~compiled~in~parallel~mode.~You~need~to~recompile~your~document~to~include~it.} \end{minipage} } } \ExplSyntaxOff %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%% SCRIPTS %%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Important: this script is NOT executed by this file, it is only created so that users can remove unused previously cached files by running this script %%% outside of LaTeX. %%% Create scripts to remove useless files: \begin{filecontents}[noheader,overwrite]{robExt-remove-old-figures.py} #!/usr/bin/env python3 ## This file is automatically generated, so do not change that file directly ## Instead, if you need to change it, copy it with a different name and edit the copy import os import re import glob # Just run this script in order to remove all old figures not listed in robExt-all-figures.txt. # Note that this part is not extracted from the pdf file since it might be different on a previous run. You can however hardcode # it here, your updated script will not be overriden unless you remove it yourself. prefixes = [ "robExt-" ] folders = [ "robustExternalize" ] def main(): imagesToKeep = dict() list_all_figures_file = glob.glob('*robExt-all-figures.txt') for filename in list_all_figures_file: with open(filename) as f: for line in f: line = line.strip() if line.endswith('.tex'): imagesToKeep[line[:-4]] = True # The exact value is not important, we mostly use dict to get ~O(1) access listOfFilesToRemove = [] # We are looking for images in the folders for folder in folders: for root, dirs, files in os.walk(folder): for f in files: for prefix in prefixes: # Not the most efficient, but anyway we typically have a single prefix # In case prefix contains weird caracters that collide with regexps: prefixEsc = re.escape(prefix) # result_search = re.search(rf"^({prefixEsc}[A-F0-1]{32}).*", f) result_search = re.search(rf"^(.*[A-F0-9]{{32}}).*", f) if result_search: if result_search.group(1) not in imagesToKeep: listOfFilesToRemove.append(os.path.join(root,f)) for f in listOfFilesToRemove: print(f"-- {f}") print(f"Above are the files to remove, are you sure you want to proceed? [y/N] (based on prefixes {prefixes})") x = input().strip() if x not in ["y", "Y"]: print("All right, we abort.") exit(1) for f in listOfFilesToRemove: os.remove(f) print(f"Removed {f}") if __name__ == '__main__': main() \end{filecontents} \begin{filecontents}[noheader,overwrite]{robExt-prepare-for-arxiv.py} #!/usr/bin/env python3 ## This file is automatically generated, so do not change that file directly ## Instead, if you need to change it, copy it with a different name and edit the copy ## This file must be called when sending stuff to the arxiv website, that would otherwise remove the .pdf ## in presence of a .tex. ## This script will simply rename the .tex files in robustExternalize into .tex-backup. ## Then, you should call "rename backup files for arxiv" to rename back the files. import os import re import glob # Just run this script in order to remove all old figures not listed in robExt-all-figures.txt. # Note that this part is not extracted from the pdf file since it might be different on a previous run. You can however hardcode # it here, your updated script will not be overriden unless you remove it yourself. prefixes = [ "robExt-" ] folders = [ "robustExternalize" ] def main(): listOfFilesToMove = [] listOfFilesAlreadyAdded = set() for folder in folders: for root, dirs, files in os.walk(folder): for f in files: if f.endswith(".tex"): pdf = os.path.splitext(f)[0] + ".pdf" if os.path.exists(os.path.join(root,pdf)): listOfFilesToMove.append((os.path.join(root,f), os.path.join(root,f + "-backup"))) if f.endswith(".tex-backup"): tex = os.path.splitext(f)[0] + ".tex" listOfFilesAlreadyAdded.add(os.path.join(root,tex)) for (f,f2) in listOfFilesToMove: print(f"-- {f} moved to {f2}") listOfFilesAlreadyAdded.discard(f) for f in listOfFilesAlreadyAdded: print(f"-- {f} already moved") print(f"Above are the files to move or already moved, are you sure you want to proceed? [y/N] (based on prefixes {prefixes})") x = input().strip() if x not in ["y", "Y"]: print("All right, we abort.") exit(1) for (f,f2) in listOfFilesToMove: os.replace(f, f2) print(f"Moved {f} to {f2}") with open('robExt-arxiv-files-to-rename.txt', 'w') as fd: for (f,f2) in listOfFilesToMove: fd.write(f'{f}\n') for f in listOfFilesAlreadyAdded: fd.write(f'{f}\n') print('The files have been written to robExt-arxiv-files-to-rename.txt.') print('Add \\robExtConfigure{rename backup files for arxiv} in your tex file.') if __name__ == '__main__': main() \end{filecontents} \ExplSyntaxOn %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%% Compatibility %%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % These commands exist on really recent kernels (~june 2023), so we provide them in case they do not already exist \ProvideDocumentCommand \NewEnvironmentCopy {mm} { \expandafter \NewCommandCopy \csname#1\expandafter\endcsname \csname#2\endcsname \expandafter \NewCommandCopy \csname end#1\expandafter\endcsname \csname end#2\endcsname } \ProvideDocumentCommand \RenewEnvironmentCopy {mm} { \expandafter \RenewCommandCopy \csname#1\expandafter\endcsname \csname#2\endcsname \expandafter \RenewCommandCopy \csname end#1\expandafter\endcsname \csname end#2\endcsname } \ProvideDocumentCommand \DeclareEnvironmentCopy {mm} { \expandafter \DeclareCommandCopy \csname#1\expandafter\endcsname \csname#2\endcsname \expandafter \DeclareCommandCopy \csname end#1\expandafter\endcsname \csname end#2\endcsname } \ProvideDocumentCommand \RenewEnvironmentCopy {mm} { \expandafter \RenewCommandCopy \csname#1\expandafter\endcsname \csname#2\endcsname \expandafter \RenewCommandCopy \csname end#1\expandafter\endcsname \csname end#2\endcsname } %% Depending on the engine, computing md5 is done in different ways \sys_if_engine_luatex:TF{ \def\robExt@pdfmdfivesum#1{% \directlua{tex.print(string.upper(md5.sumhexa("\luaescapestring{#1}")))}% }% }{ \sys_if_engine_xetex:TF {% \def\robExt@pdfmdfivesum{\mdfivesum}% }{% \def\robExt@pdfmdfivesum{\pdfmdfivesum}% }% } %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%% Paths %%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % For support of --output-directory \def\robExtPrefixAllCompilationCommands{\ifdefined\robExtOutputDirectory cd~"\robExtOutputDirectory"~&&~\fi} \def\robExtPrefixFilename{robExt-} \NewExpandableDocumentCommand{\robExtAddCachePathAndName}{m}{% \ifdefined\robExtCacheFolder% \robExtCacheFolder% \fi\robExtPrefixFilename#1% } \NewExpandableDocumentCommand{\robExtAddCachePath}{m}{% \ifdefined\robExtCacheFolder% \robExtCacheFolder% \fi#1% } % mv in unix and move in windows: \robExtIfWindowsTF{ \def\robExtMv{move} \def\robExtCp{copy} }{ \def\robExtMv{mv} \def\robExtCp{cp} } \NewDocumentCommand{\robExtBackupSource}{m}{% % arXiv will remove the .pdf file if the .tex is present. Solution: we move the .tex into .tex-backup % and restore it during compilation. We could also backup the pdf but usually this file is bigger and will contain % binary data that might not be so easy to copy in pure tex (in arxiv shell escape is disabled so we cannot % use cp). % \show\inBackupSource \robExtRunCmdIfPossible{% \robExtMv\space"\robExtAddCachePath{#1}"~% "\robExtAddCachePath{#1-backup}"% }% } \NewDocumentCommand{\robExtRunCmdIfPossible}{m}{% % Check if the output directory exists \bool_if:nTF { \sys_if_shell_unrestricted_p: || \cs_if_exist_p:N \robExtForceCompilation} { \ifdefined\robExtManualMode \message{Warning:~running~in~manual~mode~so~we~cannot~run~the~command~``#1''.} \iow_now:Nx \g__robExt_write_manually_compile_all_missing_figures_iow {#1} \else \message{Running:~\robExtPrefixAllCompilationCommands #1}% \sys_shell_now:x {\robExtPrefixAllCompilationCommands #1} \fi }{ \ifdefined\robExtFallbackManualMode \message{Warning:~shell~escape~fallback~to~manual~mode~so~cannot~run~#1.} \iow_now:Nx \g__robExt_write_manually_compile_all_missing_figures_iow {#1} \else \msg_error:nnx{robExt}{need shell escape with}{run~#1} \fi } } \NewDocumentCommand{\robExtCheckIfPrefixFolderExists}{}{ % Check if the output directory exists \ifdefined\robExtCacheFolder \bool_if:nTF { \sys_if_shell_unrestricted_p: || \cs_if_exist_p:N \robExtForceCompilation} { \ifdefined\robExtDoNotMkdirFolder\else \ifdefined\robExtManualMode \message{If ~ you ~ get~ an~ error,~ make ~ sure ~ to ~ enable ~ pdflatex ~ -shell-escape ~ or ~ to ~ MANUALLY ~ CREATE ~ THE ~ FOLDER ~ \robExtCacheFolder.} \else \sys_shell_now:x {\robExtPrefixAllCompilationCommands mkdir ~ -p ~ \robExtCacheFolder} \fi \fi }{ \message{Warning: If ~ you ~ get~ an~ error,~ make ~ sure ~ to ~ enable ~ pdflatex ~ -shell-escape ~ or ~ to ~ manually ~ CREATE ~ THE ~ FOLDER ~ \robExtCacheFolder.} } \fi } \NewDocumentCommand{\robExtCopyFileToCache}{m}{ \robExtCheckIfPrefixFolderExists% \file_if_exist:xTF {#1}{}{ \msg_error:nnx{robExt}{file does not exist}{#1~(to~copy~to~cache)} } \file_if_exist:xTF {\robExtAddCachePath{#1}}{ \robExtDebugInfo{The~file~\robExtAddCachePath{#1}~already~exists,~let~us~compare~md5~sum.} \str_set:Nx \l_tmpa_str {\file_mdfive_hash:n{#1}} \str_set:Nx \l_tmpb_str {\file_mdfive_hash:n{\robExtAddCachePath{#1}}} \str_compare:eNeTF {\l_tmpa_str} = {\l_tmpb_str} { \robExtDebugInfo{The~file~\robExtAddCachePath{#1}~has~the~good~md5~hash.} }{ \robExtDebugInfo{The~md5~hashes~of~#1~and~\robExtAddCachePath{#1}~are~different(\l_tmpa_str \space vs \space \l_tmpb_str),~let~us~copy~the~file.}% \robExtRunCmdIfPossible{\robExtCp \space "#1"~"\robExtAddCachePath{#1}"}% } }{ \robExtDebugInfo{The~file~\robExtAddCachePath{#1}~does~not~exist,~let~us~copy~it:} \robExtRunCmdIfPossible{\robExtCp \space "#1"~"\robExtAddCachePath{#1}"}% } } \let\copyFileToCache\robExtCopyFileToCache % \robExtRenameBackupFilesForArxiv{robExt-arxiv-files-to-rename.txt} takes all elements listed in the .tex file % (like robustExternalize/robExt-F83AB245D8043E653A89489C2E25AA6A.tex) and rename the % robustExternalize/robExt-F83AB245D8043E653A89489C2E25AA6A.tex-backup into % robustExternalize/robExt-F83AB245D8043E653A89489C2E25AA6A.tex % (needed for arxiv) % This is done, importantly, without shell escape as arxiv does not have shell escape. \NewDocumentCommand{\robExtRenameBackupFilesForArxiv}{O{-backup}m}{ \file_if_exist:xTF{#2} { \robExtDebugInfo{We~will~read~the~file~#2.} \ior_open:Nn \g__robExt_read_ior {#2}% %% We avoid creating a new ior since the number of available files is limited in tex \seq_clear:N \l_tmpa_seq \ior_str_map_inline:Nn \g__robExt_read_ior {% \file_if_exist:xTF{##1#1} { \seq_put_right:Nn \l_tmpa_seq {##1} }{ \robExtDebugInfo{Warning:~##1#1~does~not~exist,~so~we~will~not~rename~back~this~file~for~arXiv.} } }% \ior_close:N \g__robExt_read_ior \seq_map_inline:Nn \l_tmpa_seq { \robExtDebugInfo{We~copy~the~source~##1#1~to~##1^^J}% % First we read the file: \ior_open:Nn \g__robExt_read_ior {##1#1}% \iow_open:Nx \g__robExt_write_iow {##1} \ior_str_map_inline:Nn \g__robExt_read_ior {% \iow_now:Nx \g__robExt_write_iow {\tl_to_str:n{####1}}% }% \iow_close:N \g__robExt_write_iow \ior_close:N \g__robExt_read_ior } }{ \robExtDebugInfo{Warning:~#2~does~not~exist,~so~we~will~not~rename~back~elements~for~arXiv.} } } \NewExpandableDocumentCommand{\robExtGetPrefixPath}{}{% \ifdefined\robExtCacheFolder% \robExtCacheFolder% \fi% } \NewExpandableDocumentCommand{\robExtAddPrefixName}{m}{% \robExtPrefixFilename#1% } %% Todo: not sure if I should use \seq_push:Nx \l_file_search_path_seq {subfolder} %% to find the subfolder (seems to work for input/includegraphics/...), or if it's %% better to hardcode the subfolder. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%% Setup new commands and variables %%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Temporary: used when quickly writing to a file \iow_new:N \g__robExt_write_iow \ior_new:N \g__robExt_read_ior %% This is used to write the full list of figures in a single file (used for instance by Makefile etc...) %% TODO: create a makefile. \iow_new:N \g__robExt_write_list_all_figures_iow %% Create a file robExt-all-figures.txt with the list of .tex files \iow_open:Nx \g__robExt_write_list_all_figures_iow {\jobnameNoQuotes-\robExtAddPrefixName{all-figures.txt}} \iow_new:N \g__robExt_write_manually_compile_all_missing_figures_iow \iow_open:Nx \g__robExt_write_manually_compile_all_missing_figures_iow {\jobnameNoQuotes-\robExtAddPrefixName{compile-missing-figures.sh}} % Contains the list of dependency files (useful to compute the final md5sum) \seq_new:N \l__robExt_dependencies_str % Contains a string where on each line we have: "md5sum, dependency". The first line has nothing as "dependency" as it is the main fine whose final name is the md5sum of the dependencies. \str_new:N \l__robExt_dependencies_mdfive_str % Contains the current compilation command (including the name of the file to compile). \str_new:N \l__robExt_current_compilation_command_str \str_new:N \l__robExt_tmp_str % To have a code compatible even on older versions % https://tex.stackexchange.com/questions/615010/how-can-i-use-latex-hooks-in-historical-versions-of-tex-live \providecommand\RobExtIfFormatAtLeastTF{\@ifl@t@r\fmtversion} \RobExtIfFormatAtLeastTF{2020-10-01}% { % https://ctan.math.illinois.edu/macros/latex/base/lthooks-doc.pdf \NewHook{robust-externalize/just-before-writing-files} \def\robExtUseHookJustBeforeWritingFiles{\UseHook{robust-externalize/just-before-writing-files}}% }% { \def\robExtUseHookJustBeforeWritingFiles{} } %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%% Placeholders %%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Placeholders are strings like "__MYLIBRARY__" or "__MAINCONTENT__" that will be replaced by a given content. %% In practice: %% - for every placeholder MYPLACEHOLDER, a macro \l__robExt_placeholder_MYPLACEHOLDER_str is created, containing %% the string to use to replace it %% - a sequence \l__robExt_placeholder_group_main_seq that is a list of string, contains the list of all placeholders, %% in a string, like [MYLIBRARY, MAINCONTENT] etc... %% One issue is that LaTeX does not keep some symbols (e.g. % etc...) when used inside a macro, so we define %% different commands depending on whether they can be used in a macro or not, whether they should be taken %% from an external file etc... \seq_clear_new:N \l__robExt_placeholder_group_main_seq %%% %%% Debug in terminal %%% \NewDocumentCommand{\robExtShowPlaceholder}{sm}{ \cs_if_exist:cTF {l__robExt_placeholder_#2_str} { \message{Placeholder ~ #2 ~ contains:^^J \use:c{l__robExt_placeholder_#2_str}} }{ \message{Placeholder ~ #2 ~ does ~ not ~ exist.} } \IfBooleanTF{#1}{}{\show\def} } \NewDocumentCommand{\robExtShowPlaceholders}{s}{ \message{List ~ of ~ placeholders:} \seq_map_inline:Nn \l__robExt_placeholder_group_main_seq {\message{##1,}} \IfBooleanTF{#1}{}{\show\def} } \NewDocumentCommand{\robExtShowPlaceholdersContents}{s}{ \message{List ~ of ~ placeholders:} \seq_map_inline:Nn \l__robExt_placeholder_group_main_seq {\robExtShowPlaceholder*{##1}} \IfBooleanTF{#1}{}{\show\def} } %%% %%% Debug in document %%% \NewDocumentCommand{\robExtPrintPlaceholderNoReplacement}{sm}{% % For some reasons, newlines are displayed as \Omega. We need to replace them with \\ % https://tex.stackexchange.com/questions/694716/print-latex3-string-verbatim/694717 \tl_set_eq:Nc \l__robExt_tmp_str { l__robExt_placeholder_#2_str } \tl_replace_all:Nnn \l__robExt_tmp_str {^^J} { \mbox{}\par } % mbox is helpful to print empty lines \tl_replace_all:Nnn \l__robExt_tmp_str { ~ } { \ } \IfBooleanTF{#1}{\texttt{\use:c{l__robExt_placeholder_#2_str}}}{\begin{flushleft}\ttfamily% \l__robExt_tmp_str \end{flushleft}% } } \let\printPlaceholderNoReplacement\robExtPrintPlaceholderNoReplacement \NewDocumentCommand{\robExtPrintPlaceholder}{sm}{% \robExtGetPlaceholderInResult{#2} % For some reasons, newlines are displayed as \Omega. We need to replace them with \\ % https://tex.stackexchange.com/questions/694716/print-latex3-string-verbatim/694717 \tl_set_eq:NN \l__robExt_tmp_str \l_robExt_result_str \tl_replace_all:Nnn \l__robExt_tmp_str {^^J} { \mbox{}\par } \tl_replace_all:Nnn \l__robExt_tmp_str { ~ } { \ } \IfBooleanTF{#1}{\texttt{\l__robExt_tmp_str}}{\begin{flushleft}\ttfamily% \l__robExt_tmp_str \end{flushleft}% } } \let\printPlaceholder\robExtPrintPlaceholder \NewDocumentCommand{\robExtPrintAllPlaceholdersExceptDefaults}{s}{ List ~ of ~ placeholders:\\ \seq_map_inline:Nn \l__robExt_placeholder_group_main_seq { % We hide the elements starting with __ROBEXT_ \str_if_in:nnTF { ##1 } { __ROBEXT_ } { \IfBooleanTF {#1} { - ~ Placeholder ~ called ~ \texttt{\tl_to_str:n {##1}} ~ defined ~ by ~ default ~ (we ~ hide ~ the ~ definition ~ to ~ save ~ space)\\ }{} }{ - ~ Placeholder ~ called ~ \texttt{\tl_to_str:n {##1}} ~ contains: \robExtPrintPlaceholderNoReplacement{##1} } } } \let\printAllPlaceholdersExceptDefaults\robExtPrintAllPlaceholdersExceptDefaults \let\printImportedPlaceholdersExceptDefaults\robExtPrintAllPlaceholdersExceptDefaults \NewDocumentCommand{\robExtPrintAllPlaceholders}{}{ List ~ of ~ placeholders:\\ \seq_map_inline:Nn \l__robExt_placeholder_group_main_seq {- ~ Placeholder ~ called ~ \texttt{\tl_to_str:n {##1}} ~ contains: \robExtPrintPlaceholderNoReplacement{##1}} } \let\printAllPlaceholders\robExtPrintAllPlaceholders \let\printImportedPlaceholders\robExtPrintAllPlaceholders %%% %%% Evaluation of placeholders %%% \NewDocumentCommand{\robExtEvalPlaceholderNoReplacement}{m}{ % scantokens add an empty space at the end, so we need to add \empty to avoid it having effects % https://tex.stackexchange.com/questions/213659/could-someone-further-elucidate-expansion-catcodes-and-scantokens \tl_rescan:nv {}{ l__robExt_placeholder_#1_str } } \let\evalPlaceholderNoReplacement\robExtEvalPlaceholderNoReplacement \NewExpandableDocumentCommand{\robExtGetPlaceholderNoReplacement}{m}{ \str_use:c { l__robExt_placeholder_#1_str } } \let\getPlaceholderNoReplacement\robExtGetPlaceholderNoReplacement \NewDocumentCommand{\robExtRescanPlaceholderInVariableNoReplacement}{mm}{ \tl_gset_rescan:cnv {#1} {} { l__robExt_placeholder_#2_str } } \let\rescanPlaceholderInVariableNoReplacement\robExtRescanPlaceholderInVariableNoReplacement % Make sure that the placeholder is in the list \l__robExt_placeholder_group_main_seq. % This should automatically be called by other tools \NewDocumentCommand{\robExtAddPlaceholderToList}{m}{ \cs_if_exist:NTF {\robExtCompletelyDisableWholeImportSystem}{}{ % Make sure we have a string here: \robExt_set_hash_robust:Nn \l__robExt_tmp_str { #1 } % First we remove existing occurrences (useful to avoid listing the same macro more than once % if we redefine a macro): \seq_remove_all:NV \l__robExt_placeholder_group_main_seq \l__robExt_tmp_str \seq_put_right:NV \l__robExt_placeholder_group_main_seq \l__robExt_tmp_str } } \let\robExtImportPlaceholder\robExtAddPlaceholderToList \let\importPlaceholder\robExtAddPlaceholderToList \NewDocumentCommand{\robExtClearImportedPlaceholders}{}{ \seq_clear:N \l__robExt_placeholder_group_main_seq } \let\clearImportedPlaceholders\robExtClearImportedPlaceholders \NewDocumentCommand{\robExtRemoveImportedPlaceholder}{m}{ \robExt_set_hash_robust:Nn \l__robExt_tmp_str { #1 } \seq_remove_all:NV \l__robExt_placeholder_group_main_seq \l__robExt_tmp_str } \let\removeImportedPlaceholder\robExtRemoveImportedPlaceholder % add it first in the list \NewDocumentCommand{\robExtAddPlaceholderToListFirst}{m}{ % Make sure we have a string here: \robExt_set_hash_robust:Nn \l__robExt_tmp_str { #1 } % First we remove existing occurrences (useful to avoid listing the same macro more than once % if we redefine a macro): \seq_remove_all:NV \l__robExt_placeholder_group_main_seq \l__robExt_tmp_str \seq_put_left:NV \l__robExt_placeholder_group_main_seq \l__robExt_tmp_str } \let\robExtImportPlaceholderFirst\robExtAddPlaceholderToListFirst \let\importPlaceholderFirst\robExtAddPlaceholderToListFirst % add it first in the list \NewDocumentCommand{\robExtAddPlaceholdersToListFirst}{m}{ \robExt_set_hash_robust:Nn \l__robExt_tmp_str { #1 } \seq_set_from_clist:NN \l_tmp_seq \l__robExt_tmp_str % First we remove existing occurrences (useful to avoid listing the same macro more than once % if we redefine a macro): \seq_map_inline:Nn \l_tmp_seq { % This seems to be required to ensure catcodes are correct before removing the elements: \seq_remove_all:Nn \l__robExt_placeholder_group_main_seq {##1} } \seq_put_left:NV \l__robExt_placeholder_group_main_seq \l__robExt_tmp_str } \let\robExtImportPlaceholdersFirst\robExtAddPlaceholdersToListFirst \let\importPlaceholdersFirst\robExtAddPlaceholdersToListFirst \NewDocumentCommand{\robExtRemovePlaceholder}{m}{ % This seems to be required to ensure catcodes are correct before removing the elements: \robExt_set_hash_robust:Nn \l__robExt_tmp_str { #1 } % First we check if it exists, no need to loop over everything otherwise \str_if_exist:cTF { l__robExt_placeholder_#1_str } { \seq_remove_all:NV \l__robExt_placeholder_group_main_seq \l__robExt_tmp_str \expandafter \let \csname l__robExt_placeholder_#1_str \endcsname \undefined } { } } \let\removePlaceholder\robExtRemovePlaceholder \NewDocumentCommand{\checkIfPlaceholderNameIsLegal}{m}{ \cs_if_exist:NTF {\robExtPlaceholderOnlyWithUnderscores} { \str_if_in:nnTF {#1}{__}{}{ \msg_error:nn{robExt}{underscore in name} } } { \str_if_in:nnTF {#1}{__}{}{ \msg_warning:nn{robExt}{recommend two underscores in placeholders} } } } %% Usage: \placeholderFromContent{MYTITLE}{My slide title} %% MYTITLE will contain at the end "My slide title" %% Warning: only LaTeX-friendly code should be placed here, as LaTeX does not preserve some symbols and adds spaces %% Tested! %% The star version does NOT import the placeholder (useful for efficiency reasons) \NewDocumentCommand{\robExtPlaceholderFromContent}{sm+m}{ \robExt_set_hash_robust:cn { l__robExt_placeholder_#2_str } {#3} \checkIfPlaceholderNameIsLegal{#2} \ifdefined\robExtCompletelyDisableWholeImportSystem\else% Mostly for time optimizations... \IfBooleanTF {#1} {} {\robExtAddPlaceholderToList{#2}}% \fi } \let\placeholderFromContent\robExtPlaceholderFromContent \let\robExtSetPlaceholder\robExtPlaceholderFromContent \let\setPlaceholder\robExtPlaceholderFromContent \NewDocumentCommand{\robExtPlaceholderFromContentFirst}{smm}{ \robExt_set_hash_robust:cn { l__robExt_placeholder_#2_str } {#3} \checkIfPlaceholderNameIsLegal{#2} \IfBooleanTF {#1} {} {\robExtAddPlaceholderToListFirst{#2}} } \let\placeholderFromContentFirst\robExtPlaceholderFromContentFirst \let\robExtSetPlaceholderFirst\robExtPlaceholderFromContentFirst \let\setPlaceholderFirst\robExtPlaceholderFromContentFirst \NewDocumentCommand{\robExtPlaceholderFromString}{smm}{ \str_set_eq:cN { l__robExt_placeholder_#2_str } #3 \checkIfPlaceholderNameIsLegal{#2} \IfBooleanTF {#1} {} {\robExtAddPlaceholderToList{#2}} } \let\placeholderFromString\robExtPlaceholderFromString \let\setPlaceholderFromString\robExtPlaceholderFromString \NewDocumentCommand{\robExtPlaceholderFromStringExpanded}{smm}{ \str_set:cx { l__robExt_placeholder_#2_str } {#3} \checkIfPlaceholderNameIsLegal{#2} \IfBooleanTF {#1} {} {\robExtAddPlaceholderToList{#2}} } \let\placeholderFromStringExpanded\robExtPlaceholderFromStringExpanded \let\setPlaceholderFromStringExpanded\robExtPlaceholderFromStringExpanded \NewDocumentCommand{\robExtCopyPlaceholder}{smm}{ \str_set_eq:cc { l__robExt_placeholder_#2_str } { l__robExt_placeholder_#3_str } \IfBooleanTF {#1} {} {\robExtAddPlaceholderToList{#2}} } \let\copyPlaceholder\robExtCopyPlaceholder %% Add something to the right of the placeholder %% By default it adds a space in between, unless we use the star version \NewDocumentCommand{\robExtAddToPlaceholder}{smm}{ \str_if_exist:cTF { l__robExt_placeholder_#2_str } { \robExt_set_hash_robust:Nn \l_tmp_str {#3} %% needed as put_right does not convert to string first \IfBooleanTF{#1}{}{\str_put_right:cn { l__robExt_placeholder_#2_str } { ~ } } \str_put_right:cV { l__robExt_placeholder_#2_str } \l_tmp_str }{ \robExt_set_hash_robust:cn { l__robExt_placeholder_#2_str } {#3} \robExtAddPlaceholderToList{#2} } } \let\addToPlaceholder\robExtAddToPlaceholder \NewDocumentCommand{\robExtAddToPlaceholderNoImport}{smm}{ \str_if_exist:cTF { l__robExt_placeholder_#2_str } { \robExt_set_hash_robust:Nn \l_tmp_str {#3} %% needed as put_right does not convert to string first \IfBooleanTF{#1}{}{\str_put_right:cn { l__robExt_placeholder_#2_str } { ~ } } \str_put_right:cV { l__robExt_placeholder_#2_str } \l_tmp_str }{ \robExt_set_hash_robust:cn { l__robExt_placeholder_#2_str } {#3} } } \let\addToPlaceholderNoImport\robExtAddToPlaceholderNoImport %% Add something to the right of the placeholder %% By default it adds a space in between, unless we use the star version \NewDocumentCommand{\robExtAddBeforePlaceholder}{smm}{ \str_if_exist:cTF { l__robExt_placeholder_#2_str } { \robExt_set_hash_robust:Nn \l_tmp_str {#3} %% needed as put_right does not convert to string first \IfBooleanTF{#1}{}{\str_put_left:cn { l__robExt_placeholder_#2_str } { ~ } } \str_put_left:cV { l__robExt_placeholder_#2_str } \l_tmp_str }{ \robExt_set_hash_robust:cn { l__robExt_placeholder_#2_str } {#3} \robExtAddPlaceholderToList{#2} } } \let\addBeforePlaceholder\robExtAddBeforePlaceholder \NewDocumentCommand{\robExtAddBeforePlaceholderNoImport}{smm}{ \str_if_exist:cTF { l__robExt_placeholder_#2_str } { \robExt_set_hash_robust:Nn \l_tmp_str {#3} %% needed as put_right does not convert to string first \IfBooleanTF{#1}{}{\str_put_left:cn { l__robExt_placeholder_#2_str } { ~ } } \str_put_left:cV { l__robExt_placeholder_#2_str } \l_tmp_str }{ \robExt_set_hash_robust:cn { l__robExt_placeholder_#2_str } {#3} } } \let\addBeforePlaceholderNoImport\robExtAddBeforePlaceholderNoImport \ExplSyntaxOff % except inside setplaceholderfromcode, we want to modify by default the main content orig placeholder \def\robExtCurrentPlaceholderName{__ROBEXT_MAIN_CONTENT_ORIG__} \pgfqkeys{/robExt}{ defaultPlaceholderFromCodeStyle/.style={ remove leading spaces, }, } \ExplSyntaxOn % Usage: % \begin{placeholderFromCode}{HELPERFUNCTION} % def my_helper_function(bla): % return bla + 1 % \end{placeholderFromCode} % HELPERFUNCTION will contain at the end "def ..." % This environment cannot be placed inside any other macro/align/... \NewDocumentEnvironment{RobExtPlaceholderFromCode}{sO{defaultPlaceholderFromCodeStyle}m}{% \checkIfPlaceholderNameIsLegal{#3}% % % debug part % \str_set:Nn \l_test_str {#1} % \show\l_test_str %% Environments can't use verbatim yet: https://github.com/latex3/latex3/issues/591 % Might be related: https://tex.stackexchange.com/questions/633523/saving-the-body-of-an-environment-to-a-file-verbatim-using-xparse %% Here is the first part: %% https://tex.stackexchange.com/a/680259/116348 might work and be more efficient, but it might be less reliable %% and definitely more complicated and error prone. Instead, we write to a file and read the result. %% TODO: try to do it using verbatim, might be trivial or complicated, not sure, maybe see https://tex.stackexchange.com/questions/555359/reading-lines-verbatim-into-a-sequence-variable \XSIMfilewritestart{\jobnameNoQuotes-robExt-tmp-file-you-can-remove.tmp}% }{% \XSIMfilewritestop% \ior_open:Nn \g__robExt_read_ior {\jobnameNoQuotes-robExt-tmp-file-you-can-remove.tmp}% %% Put the file in l__robExt_tmp_contain_file_str \str_clear_new:N \l__robExt_tmp_str% \ior_str_map_inline:Nn \g__robExt_read_ior {% \str_gput_right:Nx \l__robExt_tmp_str {\tl_to_str:n{##1}^^J}% }% \str_set_eq:cN {l__robExt_placeholder_#3_str} \l__robExt_tmp_str% %% We apply the style, useful for instance to remove indentation \def\robExtCurrentPlaceholderName{#3}% \pgfqkeys{/robExt}{#2}% \IfBooleanTF {#1} {} { \robExtAddPlaceholderToList{#3} %% Otherwise they will be lost when the environment ends \robExtKeepaftergroup{l__robExt_placeholder_group_main_seq}% }% %% for other variable \robExtKeepaftergroup{l__robExt_placeholder_#3_str}% }% \let\PlaceholderFromCode\RobExtPlaceholderFromCode \let\endPlaceholderFromCode\endRobExtPlaceholderFromCode \let\SetPlaceholderCode\RobExtPlaceholderFromCode \let\endSetPlaceholderCode\endRobExtPlaceholderFromCode \NewDocumentCommand{\robExtKeepPlaceholderAfterGroup}{m}{ \robExtKeepaftergroup{l__robExt_placeholder_#1_str}% } \let\keepPlaceholderAfterGroup\robExtKeepPlaceholderAfterGroup %% Usage: %% \placeholderPathFromFilename{MYLIBPATH}{mylib.py} %% This will copy mylib.py in the cache, and set MYLIBPATH to the name of the file in the cache like %% MYLIBPATH = robExt-abcmylib.py %% Tested! \NewDocumentCommand{\robExtPlaceholderPathFromFilename}{smm}{ \ior_open:Nn \g__robExt_read_ior {#3} %% Put the file in l__robExt_tmp_contain_file_str \str_clear_new:N \l__robExt_tmp_contain_file_str \ior_str_map_inline:Nn \g__robExt_read_ior { \str_put_right:Nx \l__robExt_tmp_contain_file_str {\tl_to_str:n{##1}^^J} } %% computes the new filename based on the md5 \str_set:Nx \l__robExt_tmp_filename_no_prefix_str {\robExt@pdfmdfivesum{\l__robExt_tmp_contain_file_str}#3} %% Writes the content to the file \robExtCheckIfPrefixFolderExists \iow_open:Nx \g__robExt_write_iow {\robExtAddCachePathAndName{\l__robExt_tmp_filename_no_prefix_str}} \iow_now:NV \g__robExt_write_iow \l__robExt_tmp_contain_file_str \iow_close:N \g__robExt_write_iow %% sets the template name to the relative path to the file \str_set:cx { l__robExt_placeholder_#2_str } {\robExtPrefixFilename\l__robExt_tmp_filename_no_prefix_str} \IfBooleanTF {#1} {} {\robExtAddPlaceholderToList{#2}} } \let\placeholderPathFromFilename\robExtPlaceholderPathFromFilename %% Usage: %% \placeholderFromFileContent{MYLIB}{mylib.py} %% This will set MYLIB to the content of the file mylib.py %% Tested! \NewDocumentCommand{\robExtPlaceholderFromFileContent}{smm}{ \checkIfPlaceholderNameIsLegal{#2} \ior_open:Nn \g__robExt_read_ior {#3} %% Put the file in l__robExt_tmp_contain_file_str \str_clear_new:N \l__robExt_tmp_contain_file_str \ior_str_map_inline:Nn \g__robExt_read_ior { \str_put_right:Nx \l__robExt_tmp_contain_file_str {\tl_to_str:n{##1}^^J} } %% sets the template name to the relative path to the file \str_set_eq:cN { l__robExt_placeholder_#2_str } \l__robExt_tmp_contain_file_str \IfBooleanTF {#1} {} {\robExtAddPlaceholderToList{#2}} } \let\placeholderFromFileContent\robExtPlaceholderFromFileContent %% Usage: %% \placeholderPathFromContent{MYLIBPATH}{some code} %% This will copy "some code" in the cache, and set MYLIBPATH to the name of the file in the cache like %% MYLIBPATH = robExt-abc.py %% Tested! \NewDocumentCommand{\robExtPlaceholderPathFromContent}{smO{.tex}m}{ \robExt_set_hash_robust:Nn \l__robExt_tmp_contain_file_str {#4} %% computes the new filename based on the md5 \str_set:Nx \l__robExt_tmp_filename_no_prefix_str {\robExt@pdfmdfivesum{\l__robExt_tmp_contain_file_str}#3} %% Writes the content to the file \robExtCheckIfPrefixFolderExists \iow_open:Nx \g__robExt_write_iow {\robExtAddCachePathAndName{\l__robExt_tmp_filename_no_prefix_str}} \iow_now:NV \g__robExt_write_iow \l__robExt_tmp_contain_file_str \iow_close:N \g__robExt_write_iow %% sets the template name to the relative path to the file \str_set:cx { l__robExt_placeholder_#2_str } {\robExtPrefixFilename\l__robExt_tmp_filename_no_prefix_str} \IfBooleanTF {#1} {} {\robExtAddPlaceholderToList{#2}} } \let\placeholderPathFromContent\robExtPlaceholderPathFromContent %% Usage: %% \begin{PlaceholderPathFromCode}{mylibpath} %% def blabla(): %% \end{PlaceholderPathFromCode} %% This will copy "some code" in the cache, and set MYLIBPATH to the name of the file in the cache like %% MYLIBPATH = robExt-abc.py \NewDocumentEnvironment{RobExtPlaceholderPathFromCode}{sO{}O{defaultPlaceholderFromCodeStyle}m}{ \XSIMfilewritestart*{\jobnameNoQuotes-robExt-tmp-file-you-can-remove.tmp} }{ \XSIMfilewritestop \ior_open:Nn \g__robExt_read_ior {\jobnameNoQuotes-robExt-tmp-file-you-can-remove.tmp} %% Put the file in \l__robExt_tmp_contain_file_str \str_clear_new:N \l__robExt_tmp_contain_file_str \ior_str_map_inline:Nn \g__robExt_read_ior { \str_gput_right:Nx \l__robExt_tmp_contain_file_str {\tl_to_str:n{##1}^^J} } %% computes the new filename based on the md5 \str_set:Nx \l__robExt_tmp_filename_no_prefix_str {\robExt@pdfmdfivesum{\l__robExt_tmp_contain_file_str}#2} %% Writes the content to the file \robExtCheckIfPrefixFolderExists \iow_open:Nx \g__robExt_write_iow {\robExtAddCachePathAndName{\l__robExt_tmp_filename_no_prefix_str}} \iow_now:NV \g__robExt_write_iow \l__robExt_tmp_contain_file_str \iow_close:N \g__robExt_write_iow %% sets the template name to the relative path to the file \str_set:cx { l__robExt_placeholder_#4_str } {\robExtPrefixFilename\l__robExt_tmp_filename_no_prefix_str} %% We apply the style, useful for instance to remove indentation \def\robExtCurrentPlaceholderName{#4}% \pgfqkeys{/robExt}{#3}% \IfBooleanTF {#1} {} { \robExtAddPlaceholderToList{#4} \robExtKeepaftergroup{l__robExt_placeholder_group_main_seq} } %% Otherwise they will be lost when the environment ends \robExtKeepaftergroup{l__robExt_placeholder_#4_str} } \let\PlaceholderPathFromCode\RobExtPlaceholderPathFromCode \let\endPlaceholderPathFromCode\endRobExtPlaceholderPathFromCode % %%% Evaluate a string by replacing the placeholders until there is none left % %%% the result will be in \robExtResult \cs_set:Nn \__robExt_replace_until_impossible: { \robExtDebugMessage{We~needed~to~run~replace_until_impossible} % Try to replace directly if single placeholder \str_if_exist:cTF { l__robExt_placeholder_ \l_robExt_result_str _str }{ \str_set_eq:Nc {\l_robExt_result_str}{ l__robExt_placeholder_ \l_robExt_result_str _str } \str_set:Nn \l_robext_tmp_before {} } { \str_set_eq:NN \l_robext_tmp_before \l_robExt_result_str \seq_map_inline:Nn \l__robExt_placeholder_group_main_seq { \robExtDebugMessage{Trying ~ to ~ replace ##1^^J} \str_if_exist:cTF { l__robExt_placeholder_##1_str } { \str_replace_all:Nnv \l_robExt_result_str { ##1 } { l__robExt_placeholder_##1_str } }{ \robExtDebugWarning{WARNING: ~ the ~ placeholder ~ ##1 ~ does ~ not ~ exist.} } } } % In quick mode, we first try to check if the string contains __ since by default all placeholders start with __ % If not, no need to do one more run \cs_if_exist:NTF {\robExtPlaceholderOnlyWithUnderscores} { \str_if_in:NnTF { \l_robExt_result_str } { __ } { \str_compare:eNeTF \l_robext_tmp_before = \l_robExt_result_str { % We converged }{ % The strings are different: we restart %\message{The strings are different, we restart: It ~ used ~ to ~ be ~\l_robext_tmp_before^^J^^J now it is^^J^^J \l_robExt_result_str} \__robExt_replace_until_impossible: } }{ % We found no __ so we stop before even if a change was made } }{ % We compare the result \str_compare:eNeTF \l_robext_tmp_before = \l_robExt_result_str { % \str_set_eq:cN { l__robExt_placeholder_#1_str } \l_robExt_result_str % \robExtAddPlaceholderToList{#1} }{ % The strings are different: we restart \__robExt_replace_until_impossible: } } } \NewDocumentCommand{\robExtGetPlaceholderInResult}{sO{}m}{ \robExt_set_hash_robust:Nn \l_robExt_result_str { #3 } \robExtDebugMessage{Begin~to~evaluate~placeholder^^J~\l_robExt_result_str} \__robExt_replace_until_impossible: \robExtDebugMessage{Ended~the~replacement^^J~\l_robExt_result_str} \tl_if_blank:nTF {#2} {} { % To avoid infinite recursion later and allow concatenation to a placeholder that does not exists % we remove the name of the placeholder at the end \str_remove_all:Nn \l_robExt_result_str { #2 } \str_set_eq:cN { l__robExt_placeholder_#2_str } \l_robExt_result_str \IfBooleanTF {#1} {} {\robExtAddPlaceholderToList{#2}} } } \let\getPlaceholderInResult\robExtGetPlaceholderInResult %% like robExtGetPlaceholderInResult put assumes that there is a single placeholder name. %% It is used mainly for efficiency as it avoids a loop on all placeholder. \NewDocumentCommand{\robExtGetPlaceholderInResultFromSinglePlaceholder}{sO{}m}{ \cs_if_exist:cTF { l__robExt_placeholder_#3_str }{ \str_set_eq:Nc { \l_robExt_result_str }{ l__robExt_placeholder_#3_str } }{ \msg_error:nnx{robExt}{placeholder not existing}{#3} } \robExtDebugMessage{Starting~to~replace~placeholder~#3~to~get~\l_robExt_result_str} \__robExt_replace_until_impossible: \robExtDebugMessage{Ended~up~with~\l_robExt_result_str} \tl_if_blank:nTF {#2} {} { % To avoid infinite recursion later and allow concatenation to a placeholder that does not exists % we remove the name of the placeholder at the end \str_remove_all:Nn \l_robExt_result_str { #2 } \str_set_eq:cN { l__robExt_placeholder_#2_str } \l_robExt_result_str \IfBooleanTF {#1} {} {\robExtAddPlaceholderToList{#2}} } } \cs_set:Nn \__replace_from_list:N { \str_set_eq:NN \l_robext_tmp_before \l_robExt_result_str \clist_map_inline:Nn {#1} { \str_if_exist:cTF { l__robExt_placeholder_##1_str }{ \str_replace_all:Nnv \l_robExt_result_str { ##1 } { l__robExt_placeholder_##1_str } } { } } % We compare the result \str_compare:eNeTF \l_robext_tmp_before = \l_robExt_result_str { % \str_set_eq:cN { l__robExt_placeholder_#1_str } \l_robExt_result_str % \robExtAddPlaceholderToList{#1} }{ % The strings are different: we restart \__replace_from_list:N { #1 } } } \NewDocumentCommand{\robExtGetPlaceholderInResultReplaceFromList}{smO{}m}{ \robExt_set_hash_robust:Nn \l_robExt_result_str { #4 } \clist_set:Nn \l__robExt_list_placeholder_replace_clist {#2} \__replace_from_list:N \l__robExt_list_placeholder_replace_clist \tl_if_blank:nTF {#3} {} { \str_remove_all:Nn \l_robExt_result_str { #3 } \str_set_eq:cN { l__robExt_placeholder_#3_str } \l_robExt_result_str \IfBooleanTF {#1} {} {\robExtAddPlaceholderToList{#3}} } } \let\getPlaceholderInResultReplaceFromList\robExtGetPlaceholderInResultReplaceFromList % Otherwise we need latex3 \def\robExtResult{\l_robExt_result_str} %%%% Same version, but replaces only one (useful sometimes) \cs_set:Nn \__replace_n_times:n { \str_set_eq:NN \l_robext_tmp_before \l_robExt_result_str \seq_map_inline:Nn \l__robExt_placeholder_group_main_seq { \str_replace_all:Nnv \l_robExt_result_str { ##1 } { l__robExt_placeholder_##1_str } } \int_compare:nTF { #1 > 1}{ \__replace_n_times:n {\int_eval:n {#1-1}} }{} } \NewDocumentCommand{\robExtSetPlaceholderRec}{smm}{ \IfBooleanTF {#1} { \robExtGetPlaceholderInResult*[#2]{#3} } { \robExtGetPlaceholderInResult[#2]{#3} } } \let\setPlaceholderRec\robExtSetPlaceholderRec \NewDocumentCommand{\robExtSetPlaceholderRecReplaceFromList}{smmm}{ \IfBooleanTF {#1} { \robExtGetPlaceholderInResultReplaceFromList*{#2}[#3]{#4} }{ \robExtGetPlaceholderInResultReplaceFromList{#2}[#3]{#4} } } \let\setPlaceholderRecReplaceFromList\robExtSetPlaceholderRecReplaceFromList \NewDocumentCommand{\robExtGetPlaceholder}{O{}m}{ \robExtGetPlaceholderInResult[#1]{#2} \l_robExt_result_str } \let\getPlaceholder\robExtGetPlaceholder %%% Evaluate a string by replacing the placeholders until there is none left %%% the result will be in \robExtResult \NewDocumentCommand{\robExtEvalPlaceholder}{m}{ \robExt_set_hash_robust:Nn \l_test_str {#1} \robExtGetPlaceholderInResult{#1} \tl_rescan:nv {} { l_robExt_result_str } } \let\evalPlaceholder\robExtEvalPlaceholder %%% Evaluate a string by replacing the placeholders until there is none left %%% the result will be in \robExtResult \NewDocumentCommand{\robExtEvalPlaceholderReplaceFromList}{mm}{ \robExt_set_hash_robust:Nn \l_test_str {#2} \robExtGetPlaceholderInResultReplaceFromList{#1}{#2} \tl_rescan:nv {} { l_robExt_result_str } } \let\evalPlaceholderReplaceFromList\robExtEvalPlaceholderReplaceFromList %%% Evaluate a placeholder by first trying some strings, then checking if a template is still present (if the %%% mode is enabled), and then it continues normally. Mostly for performance reasons. \NewDocumentCommand{\robExtEvalPlaceholderStartFromList}{mm}{ \robExt_set_hash_robust:Nn \l_test_str {#2} \robExtGetPlaceholderInResultReplaceFromList{#1}{#2} \cs_if_exist:NTF {\robExtPlaceholderOnlyWithUnderscores} { %% there might be non-important placeholders that are replaced later: \str_set_eq:NN \l__robExt_result_minus_str \l_robExt_result_str \str_remove_all:Nn \l__robExt_result_minus_str {__ROBEXT_OUTPUT_PREFIX__} \str_remove_all:Nn \l__robExt_result_minus_str {__ROBEXT_SOURCE_FILE__} \str_remove_all:Nn \l__robExt_result_minus_str {__ROBEXT_OUTPUT_PDF__} \str_remove_all:Nn \l__robExt_result_minus_str {__ROBEXT_WAY_BACK__} \str_remove_all:Nn \l__robExt_result_minus_str {__ROBEXT_CACHE_FOLDER__} \str_if_in:NnTF { \l__robExt_result_minus_str } { __ } { \__robExt_replace_until_impossible: }{ } }{ \__robExt_replace_until_impossible: } \tl_rescan:nv {} { l_robExt_result_str } } \let\evalPlaceholderStartFromList\robExtEvalPlaceholderStartFromList \NewDocumentCommand{\robExtEvalPlaceholderInplace}{m}{ \robExtGetPlaceholderInResult{#1} \tl_set_rescan:Nnx \l__robExt_tmp_tl {} { \l_robExt_result_str } \robExt_set_hash_robust:cx { l__robExt_placeholder_#1_str } { \l__robExt_tmp_tl } } \let\evalPlaceholderInplace\robExtEvalPlaceholderInplace \NewDocumentCommand{\robExtEvalPlaceholderInplaceFromSinglePlaceholder}{m}{ \robExtGetPlaceholderInResultFromSinglePlaceholder{#1} \tl_set_rescan:Nnx \l__robExt_tmp_tl {} { \l_robExt_result_str } \robExt_set_hash_robust:cx { l__robExt_placeholder_#1_str } { \l__robExt_tmp_tl } } \let\evalPlaceholderInplaceFromSinglePlaceholder\robExtEvalPlaceholderInplaceFromSinglePlaceholder % Sometimes two symbols ## (with a different catcode than a normal hash) are added instead of a single # \NewDocumentCommand{\robExtPlaceholderDoubleNumberHashesInplace}{m}{ \str_replace_all:cxx { l__robExt_placeholder_#1_str } { \c_hash_str } { \c_hash_str \c_hash_str } } \let\placeholderDoubleNumberHashesInplace\robExtPlaceholderDoubleNumberHashesInplace \NewDocumentCommand{\robExtPlaceholderHalveNumberHashesInplace}{m}{ \str_replace_all:cxx { l__robExt_placeholder_#1_str } { \c_hash_str \c_hash_str } { \c_hash_str } } \let\placeholderHalveNumberHashesInplace\robExtPlaceholderHalveNumberHashesInplace \NewDocumentCommand{\robExtPlaceholderReplaceInplace}{mmm}{ \str_replace_all:cnn { l__robExt_placeholder_#1_str } { #2 } { #3 } } \let\placeholderReplaceInplace\robExtPlaceholderReplaceInplace \NewDocumentCommand{\robExtPlaceholderReplaceInplaceEval}{mmm}{ \str_replace_all:cxx { l__robExt_placeholder_#1_str } { #2 } { #3 } } \let\placeholderReplaceInplaceEval\robExtPlaceholderReplaceInplaceEval % \group_begin: % \char_set_catcode_other:N \^^I % Usage: % \robExtPlaceholderRemoveSpacesUntil{__MY_PLACEHOLDER__}{>>>} \group_begin: \char_set_catcode_other:N \^^I \cs_new_protected:Npn \__robExt_replace_tabs:N #1 { \str_replace_all:Nnn #1 { ^^I } { ~ } } \group_end: \NewDocumentCommand{\robExtPlaceholderRemoveSpacesUntil}{mO{1}m}{ %% Cut the string in lines \seq_set_split_keep_spaces:Nnv \l_tmpa_seq {^^J} { l__robExt_placeholder_#1_str } \str_clear:c { l__robExt_placeholder_#1_str } %% We iterate over the lines \seq_map_variable:NNn \l_tmpa_seq \l_tmpa_tl { % \l_tmpa_tl contains the current line \seq_set_split_keep_spaces:NnV \l_tmpb_seq {#3} \l_tmpa_tl %% In any case, the first item must only contain spaces. % The separator was present. Check that the line only contains spaces before the separator \seq_get_left:NN \l_tmpb_seq \l_tmpb_tl \__robExt_replace_tabs:N \l_tmpb_tl %\str_replace_all:Nnn \l__robExt_tmp_str { ^^I } { ~ } %\tl_replace_all:Nnn \l_tmpb_tl {^^I} {~} % we replace tabs with spaces, check if it works % Save the size of the string \int_set:Nn \l_tmpa_int {\str_count:N \l_tmpb_tl} % we remove spaces \tl_trim_spaces:N \l_tmpb_tl \tl_if_empty:VTF \l_tmpb_tl { \int_compare:nNnTF {\seq_count:N \l_tmpb_seq} > {1} { % Empty line: we add it to the current placeholder \str_put_right:cx { l__robExt_placeholder_#1_str } { % the placeholder is a string, so I expect l_tmpa_tl to also be a string. No problem? \str_range:Nnn \l_tmpa_tl {\l_tmpa_int + \str_count:n {#3} + 1 + #2} {-1} ^^J } } { % No separator: it means the line is empty \str_put_right:cn { l__robExt_placeholder_#1_str } {^^J} } }{ % The string was not completely empty: raise an error \msg_error:nnxxx{robExt}{remove spaces until non spaces characters}{#1}{\l_tmpa_tl}{#3} } } } \let\placeholderRemoveSpacesUntil\robExtPlaceholderRemoveSpacesUntil % Usage: % \robExtPlaceholderPrependAllLines{__MY_PLACEHOLDER__}{ } \NewDocumentCommand{\robExtPlaceholderPrependAllLines}{mm}{ %% Cut the string in lines \seq_set_split_keep_spaces:Nnv \l_tmpa_seq {^^J} { l__robExt_placeholder_#1_str } \str_clear:c { l__robExt_placeholder_#1_str } %% We iterate over the lines \seq_map_variable:NNn \l_tmpa_seq \l_tmpa_tl { \str_put_right:cx { l__robExt_placeholder_#1_str } { #2 \l_tmpa_tl ^^J} } } \let\placeholderPrependAllLines\robExtPlaceholderPrependAllLines %% https://tex.stackexchange.com/questions/709973/latex3-efficient-way-to-remove-spaces-in-front-of-a-command/710006?noredirect=1#comment1765909_710006 %% Modifies \l_tmpa_tl so that it contains the current number of spaces to remove %% It also uses \l_tmpa_str \group_begin: \char_set_catcode_other:N \^^I \cs_new_protected:Npn \__robExt_count_leading_whitespace:n #1 { \str_set:Nn \l_tmpa_str {#1} \str_replace_all:Nnn \l_tmpa_str { ^^I } { ~ } \tl_if_blank:VF \l_tmpa_str { \int_set:Nn \l_tmpa_int { \int_min:nn { \l_tmpa_int } { \str_count_spaces:N \l_tmpa_str - \str_count_spaces:e { \exp_last_unbraced:NV \use:n \l_tmpa_str {} } } } } } \cs_generate_variant:Nn \__robExt_count_leading_whitespace:n { V } \group_end: \NewDocumentCommand{\robExtPlaceholderRemoveLeadingSpaces}{m}{ %% Cut the string in lines \seq_set_split_keep_spaces:Nnv \l_tmpa_seq {^^J} { l__robExt_placeholder_#1_str } %% Stores the number of spaces we can trim. Since we will take the minimum, we add infinity first \int_set_eq:NN \l_tmpa_int \c_max_int %% We iterate over the lines to find the minimum number of spaces to trim \seq_map_variable:NNn \l_tmpa_seq \l_tmpa_tl { % If the line is empty, let's just remove it: \int_compare:nNnTF {\str_count_ignore_spaces:V \l_tmpa_tl} > {0} { % The line contains also letters, let us count the number of spaces. \__robExt_count_leading_whitespace:V \l_tmpa_tl } { % line contains only spaces, we don't care } } %% We check if the string was not all empty (e.g. if the content is empty) \int_compare:nNnTF {\l_tmpa_int} = {\c_max_int} {} { \str_clear:c { l__robExt_placeholder_#1_str } %% We iterate over the lines to recreate the appropriate placeholder \int_set:Nn \l_tmpb_int {\seq_count:N \l_tmpa_seq} \seq_map_indexed_inline:Nn \l_tmpa_seq { \str_put_right:cx { l__robExt_placeholder_#1_str } { \str_range:nnn {##2} {\l_tmpa_int + 1} {-1} % We do not want to add ^^J for the very last line, otherwise it will create a new line everytime we call it \int_compare:nNnTF {##1} = {\l_tmpb_int} {} { ^^J } } } } } \let\placeholderRemoveLeadingSpaces\robExtPlaceholderRemoveLeadingSpaces %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%% Placeholders groups %%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % I used to put all placeholders into the same group, but it was quite inefficient (latex is really slow) % Then I tried to hide this group but keep a single list of all items, but still automatically add items to it: % also quite slow, and the list was not so useful anyway. % In practice, however, we do not need to put the placeholders created by default in a group: we can just store them % in a standalone way, and load them when needed. So now I create the concept of group: people can add placeholders % to a group of placeholders to load them later if needed (e.g. to list all placeholders created by this library). % The group named "main" will be the main one with imported groups, where stuff like "set placeholder rec" will search for the % existing placeholders. % The group named "preexisting" will contain the placeholders created by this library (useful mostly for % debugging purpose). \NewDocumentCommand{\robExtAddPlaceholdersToGroup}{mm}{ % Make sure we have a string here: \robExt_set_hash_robust:Nn \l__robExt_tmp_str { #2 } \seq_set_from_clist:NN \l__robExt_tmp_seq \l__robExt_tmp_str \cs_if_exist:cTF {l__robExt_placeholder_group_#1_seq}{}{ \seq_clear_new:c {l__robExt_placeholder_group_#1_seq}} % First we remove existing occurrences (useful to avoid listing the same macro more than once % if we redefine a macro): \seq_map_inline:Nn \l__robExt_tmp_seq { \robExtDebugMessage{Adding ##1 to #1} \seq_remove_all:cn {l__robExt_placeholder_group_#1_seq} {##1} \seq_put_right:cn {l__robExt_placeholder_group_#1_seq} {##1} } } \let\addPlaceholdersToGroup\robExtAddPlaceholdersToGroup \NewDocumentCommand{\robExtEnsureGroupPlaceholdersExists}{m}{ \cs_if_exist:cTF {l__robExt_placeholder_group_#1_seq}{}{ \seq_clear_new:c {l__robExt_placeholder_group_#1_seq}} } \let\ensureGroupPlaceholdersExists\robExtEnsureGroupPlaceholdersExists \NewDocumentCommand{\robExtAddPlaceholdersToGroupBefore}{mm}{ % Make sure we have a string here: \robExt_set_hash_robust:Nn \l__robExt_tmp_str { #2 } \seq_set_from_clist:NN \l__robExt_tmp_seq \l__robExt_tmp_str \seq_reverse:N \l__robExt_tmp_seq \cs_if_exist:cTF {l__robExt_placeholder_group_#1_seq}{}{ \seq_clear_new:c {l__robExt_placeholder_group_#1_seq}} % First we remove existing occurrences (useful to avoid listing the same macro more than once % if we redefine a macro): \seq_map_inline:Nn \l__robExt_tmp_seq { \seq_remove_all:cn {l__robExt_placeholder_group_#1_seq} {##1} \seq_put_left:cn {l__robExt_placeholder_group_#1_seq} {##1} } } \let\addPlaceholdersToGroupBefore\robExtAddPlaceholdersToGroupBefore \NewDocumentCommand{\robExtRemovePlaceholdersFromGroup}{mm}{ % Make sure we have a string here: \robExt_set_hash_robust:Nn \l__robExt_tmp_str { #2 } \seq_set_from_clist:NN \l__robExt_tmp_seq \l__robExt_tmp_str \seq_reverse:N \l__robExt_tmp_seq \cs_if_exist:cTF {l__robExt_placeholder_group_#1_seq}{}{ \seq_clear_new:c {l__robExt_placeholder_group_#1_seq}} % First we remove existing occurrences (useful to avoid listing the same macro more than once % if we redefine a macro): \seq_map_inline:Nn \l__robExt_tmp_seq { \seq_remove_all:cn {l__robExt_placeholder_group_#1_seq} {##1} } } \let\removePlaceholdersFromGroup\robExtRemovePlaceholdersFromGroup \let\removePlaceholderFromGroup\robExtRemovePlaceholdersFromGroup \NewDocumentCommand{\robExtClearGroupPlaceholders}{m}{ \seq_clear_new:c {l__robExt_placeholder_group_#1_seq} } \let\clearGroupPlaceholders\robExtClearGroupPlaceholders \NewDocumentCommand{\robExtCopyGroupPlaceholders}{mm}{ \seq_set_eq:cc {l__robExt_placeholder_group_#1_seq} {l__robExt_placeholder_group_#2_seq} } \let\copyGroupPlaceholders\robExtCopyGroupPlaceholders \NewDocumentCommand{\robExtAppendGroupPlaceholders}{mm}{ \seq_map_inline:cn {l__robExt_placeholder_group_#2_seq} { \seq_remove_all:cn {l__robExt_placeholder_group_#1_seq} {##1} \seq_put_right:cn {l__robExt_placeholder_group_#1_seq} {##1} } } \let\appendGroupPlaceholders\robExtAppendGroupPlaceholders \NewDocumentCommand{\robExtAppendBeforeGroupPlaceholders}{mm}{ \seq_eq:Nc \l__robExt_tmp_seq {l__robExt_placeholder_group_#2_seq} \seq_reverse:N \l__robExt_tmp_seq \seq_map_inline:Nn \l__robExt_tmp_seq { \seq_remove_all:cn {l__robExt_placeholder_group_#1_seq} {##1} \seq_put_left:cn {l__robExt_placeholder_group_#1_seq} {##1} } } \let\appendBeforeGroupPlaceholders\robExtAppendBeforeGroupPlaceholders \NewDocumentCommand{\robExtImportPlaceholdersFromGroup}{m}{ \robExtAppendGroupPlaceholders{main}{#1} } \let\importPlaceholdersFromGroup\robExtImportPlaceholdersFromGroup \NewDocumentCommand{\robExtImportAllPlaceholders}{}{ \seq_map_inline:Nn \l__robExt_list_groups_seq { \robExtImportPlaceholdersFromGroup{##1} } } \let\importAllPlaceholders\robExtImportAllPlaceholders %%%% %%%% List groups, mostly for debugging purpose %%%% \seq_new:N \l__robExt_list_groups_seq % contains the list of all groups %%%% \robExtRegisterGroupPlaceholders{bash} adds "bash" to the list of existing groups \NewDocumentCommand{\robExtRegisterGroupPlaceholders}{m}{ \seq_if_exist:cTF {l__robExt_placeholder_group_#1_seq}{}{ \seq_clear_new:c {l__robExt_placeholder_group_#1_seq} } \seq_put_right:Nn \l__robExt_list_groups_seq {#1} } \let\registerGroupPlaceholders\robExtRegisterGroupPlaceholders \let\newGroupPlaceholders\robExtRegisterGroupPlaceholders \NewDocumentCommand{\robExtShowAllRegisteredGroupsAndPlaceholders}{s}{% \seq_map_inline:Nn \l__robExt_list_groups_seq {% \message{^^J- Group ##1: ^^J}% \seq_map_inline:cn {l__robExt_placeholder_group_##1_seq} {% \message{, ####1 }% \IfBooleanTF{#1}{ \message{Content:\use:c{l__robExt_placeholder_####1_str}} }{} }% \message{^^J} } \show\def } \let\showAllRegisteredGroupsAndPlaceholders\robExtShowAllRegisteredGroupsAndPlaceholders \NewDocumentCommand{\robExtPrintAllRegisteredGroups}{s}{% \seq_map_inline:Nn \l__robExt_list_groups_seq {% \texttt{##1}\par } } \let\printAllRegisteredGroups\robExtPrintAllRegisteredGroups \NewDocumentCommand{\robExtPrintAllRegisteredGroupsAndPlaceholders}{s}{% \seq_map_inline:Nn \l__robExt_list_groups_seq {% - ~ Group \texttt{##1}:\par \seq_map_inline:cn {l__robExt_placeholder_group_##1_seq} {% Placeholder \texttt{####1}% \IfBooleanTF {#1}{\robExtPrintPlaceholderNoReplacement{####1}}{\par} }% } } \let\printAllRegisteredGroupsAndPlaceholders\robExtPrintAllRegisteredGroupsAndPlaceholders % The star version displays the content as well \NewDocumentCommand{\robExtPrintGroupPlaceholders}{sm}{% Content~ of~ group~ \texttt{#2}:\par% \seq_map_inline:cn {l__robExt_placeholder_group_#2_seq} {% -~Placeholder ~ \texttt{##1}\par \IfBooleanTF {#1}{\robExtPrintPlaceholderNoReplacement{##1}}{} }% } \let\printGroupPlaceholders\robExtPrintGroupPlaceholders %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%% Dependencies %%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \NewDocumentCommand{\robExtResetDependencies}{m}{ \seq_clear:N \l__robExt_dependencies_str } \NewDocumentCommand{\robExtAddDependency}{m}{ \file_if_exist:nTF {#1} { \seq_put_left:Nx \l__robExt_dependencies_str {#1} } { \msg_error:nnx{robExt}{dependency does not exist}{#1} } } \NewDocumentCommand{\robExtDebugDependency}{}{ \show\l__robExt_dependencies_str } % Useful when compiling commands \NewDocumentCommand{\robExtSaveDependencies}{m}{ \seq_gset_eq:cN {#1} \l__robExt_dependencies_str } \NewDocumentCommand{\robExtRestoreDependencies}{m}{ \seq_set_eq:Nc \l__robExt_dependencies_str {#1} } %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%% Externalization %%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% The idea is to populate a placeholder called __ROBEXT_TEMPLATE__ that will contain the file to generate %% together with a placeholder called __ROBEXT_COMPILATION_COMMAND__ that will contain the command to compile the file \NewDocumentCommand{\robExtSetCompilationCommand}{m}{ \robExtSetPlaceholderFirst{__ROBEXT_COMPILATION_COMMAND__} {#1} } \NewDocumentCommand{\robExtAddArgumentToCompilationCommand}{m}{ \robExtSetPlaceholderRec{__ROBEXT_COMPILATION_COMMAND__} {__ROBEXT_COMPILATION_COMMAND__ ~ "#1"} } %% Alias of robExtFinalFile to \robExtSourceFile, as I don't like anymore the name I chose %\def\robExtSourceFile{\robExtFinalFile} % First is name of sequence for only try (we only replace those and stop), second is name of sequence for first try (we first try with the sequence and % then do a more normal thing), third is the name of the placeholder \cs_set:Nn \try_to_evaluate_single_placeholder_without_going_to_replace_until_impossible:NNn { %%%%% Take care of the compilation command: might seem a bit weird and convoluted, but this is needed to get really good efficiency %%%%% since \__robExt_replace_until_impossible: is quite costly, we try to avoid it \cs_if_exist:NTF {#1} { % We only replace fixed state of placeholders \str_set_eq:Nc \l_robExt_result_str {l__robExt_placeholder_#3_str} \seq_map_inline:Nn {#1} { \robExtDebugMessage{replacing~##1~in~\l_robExt_result_str} \str_replace_all:Nnv \l_robExt_result_str {##1} {l__robExt_placeholder_##1_str} } \robExtDebugMessage{Cool,~we~avoided~the~costly~replace~(only~try).} }{ \cs_if_exist:NTF {#2} { % We start by replacing a fixed state of placeholders \str_set_eq:Nc \l_robExt_result_str {l__robExt_placeholder_#3_str} \robExtDebugMessage{We~start~by~replacing~a~fixed~state~of~placeholders} \seq_map_inline:Nn {#2} { \str_if_exist:cTF {l__robExt_placeholder_##1_str} { \robExtDebugMessage{Replacing~##1~into~\use:c{l__robExt_placeholder_##1_str}}% \str_replace_all:Nnv \l_robExt_result_str {##1} {l__robExt_placeholder_##1_str} } { \robExtDebugMessage{Warning: ~ trying~to~replace~##1~that~does~not~exist.} } } %\show\l_robExt_result_str %% Check if there is some more stuff to replace (possible only if we consider only templates with underscores \cs_if_exist:NTF {\robExtPlaceholderOnlyWithUnderscores} { % all templates have underscores % first check if some non-important elements are here: for this we first remove them \str_set_eq:NN \l__robExt_result_minus_str \l_robExt_result_str \str_remove_all:Nn \l__robExt_result_minus_str {__ROBEXT_OUTPUT_PREFIX__} \str_remove_all:Nn \l__robExt_result_minus_str {__ROBEXT_SOURCE_FILE__} \str_remove_all:Nn \l__robExt_result_minus_str {__ROBEXT_OUTPUT_PDF__} \str_remove_all:Nn \l__robExt_result_minus_str {__ROBEXT_WAY_BACK__} \str_remove_all:Nn \l__robExt_result_minus_str {__ROBEXT_CACHE_FOLDER__} % Now, we check if some __ are left: \str_if_in:NnTF { \l__robExt_result_minus_str } { __ } { % Some templates are left \robExtDebugMessage{Still~some~templates~are~left.} \__robExt_replace_until_impossible: } { % No more template: the end \robExtDebugMessage{Cool,~we~avoided~the~costly~replace: \l_robExt_result_str.} } }{ % no way to know, must do it anyway \robExtDebugMessage{Seems~like~not~all~templates~contain~underscore.~Too~bad,~it~will~be~slower} \__robExt_replace_until_impossible: } }{ \robExtDebugMessage{No~advice~on~how~to~start:~doing~it~the~long~way} \robExtGetPlaceholderInResultFromSinglePlaceholder{#3} } } } \NewDocumentCommand{\robExtOnlyPlaceholdersInCompilationCommand}{m}{ \seq_set_from_clist:Nn \robExtOnlyTryThisSequenceOnCompilationCommand {#1} } \let\onlyPlaceholdersInCompilationCommand\robExtOnlyPlaceholdersInCompilationCommand \NewDocumentCommand{\robExtFirstPlaceholdersInCompilationCommand}{m}{ \seq_set_from_clist:Nn \robExtFirstTryThisSequenceOnCompilationCommand {#1} } \let\firstPlaceholdersInCompilationCommand\robExtFirstPlaceholdersInCompilationCommand \NewDocumentCommand{\robExtOnlyPlaceholdersInTemplate}{m}{ \seq_set_from_clist:Nn \robExtOnlyTryThisSequenceOnTemplate {#1} } \let\onlyPlaceholdersInTemplate\robExtOnlyPlaceholdersInTemplate \NewDocumentCommand{\robExtFirstPlaceholdersInTemplate}{m}{ \seq_set_from_clist:Nn \robExtFirstTryThisSequenceOnTemplate {#1} } \let\firstPlaceholdersInTemplate\robExtFirstPlaceholdersInTemplate % for use outside of expl3 (pgf) \def\robExtCurrentCompilationCommand{\l__robExt_current_compilation_command_str} \NewDocumentCommand{\robExtGetNearlyFinalValueTemplateAndCompilationCommand}{}{ %% oututs the compilation in \l__robExt_current_compilation_command_str and the template in \l_robExt_result_str %% This is for efficiency reasons: we only allow a few placeholders: %% In compilation command: %% %% In template: %% - __ROBEXT_MAIN_CONTENT__ %% - __ROBEXT_MAIN_CONTENT_ORIG__ %% - __ROBEXT_LATEX_PREAMBLE__ %% - __ROBEXT_OUTPUT_PREFIX__ %% - __ROBEXT_LATEX_WRITE_DEPTH_TO_OUT_FILE__ %% - __ROBEXT_LATEX_CREATE_OUT_FILE__ %% Others %% - __ROBEXT_SOURCE_FILE__ %% - __ROBEXT_OUTPUT_PDF__ %% - __ROBEXT_WAY_BACK__ %% - __ROBEXT_CACHE_FOLDER__ \ifdefined\robExtDisablePlaceholders% \robExtDebugMessage{You~have~disabled~placeholders~(this~is~likely~a~compiled~template)}% \str_set_eq:NN \l__robExt_current_compilation_command_str \l__robExt_placeholder___ROBEXT_COMPILATION_COMMAND___str \str_set_eq:NN \l_robExt_result_str \l__robExt_placeholder___ROBEXT_TEMPLATE___str% \str_replace_all:Nnv \l_robExt_result_str {__ROBEXT_MAIN_CONTENT__} {l__robExt_placeholder___ROBEXT_MAIN_CONTENT___str} \str_replace_all:Nnv \l_robExt_result_str {__ROBEXT_LATEX_PREAMBLE__} {l__robExt_placeholder___ROBEXT_LATEX_PREAMBLE___str} \str_replace_all:Nnv \l_robExt_result_str {__ROBEXT_MAIN_CONTENT_ORIG__} {l__robExt_placeholder___ROBEXT_MAIN_CONTENT_ORIG___str} \else \ifdefined\robExtEnableImportMechanism\else\robExtImportAllPlaceholders\fi% %%%%% Take care of the compilation command: might seem a bit weird and convoluted, but this is needed to get really good efficiency %%%%% since \__robExt_replace_until_impossible: is quite costly, we try to avoid it \try_to_evaluate_single_placeholder_without_going_to_replace_until_impossible:NNn \robExtOnlyTryThisSequenceOnCompilationCommand \robExtFirstTryThisSequenceOnCompilationCommand {__ROBEXT_COMPILATION_COMMAND__} \str_set_eq:NN \l__robExt_current_compilation_command_str \l_robExt_result_str %%%%% Take care of \try_to_evaluate_single_placeholder_without_going_to_replace_until_impossible:NNn \robExtOnlyTryThisSequenceOnTemplate \robExtFirstTryThisSequenceOnTemplate {__ROBEXT_TEMPLATE__} \fi } \NewDocumentCommand{\robExtSetBackCompilationCommandAndTemplate}{}{ \robExtPlaceholderFromString{__ROBEXT_COMPILATION_COMMAND__}{\l__robExt_current_compilation_command_str}% \robExtPlaceholderFromString{__ROBEXT_TEMPLATE__}{\l_robExt_result_str}% } \NewDocumentCommand{\robExtWriteFile}{m}{ %%% First we get all dependencies stored in \l__robExt_dependencies_str to create a csv-like file: \str_clear:N \l__robExt_dependencies_mdfive_str \robExtGetNearlyFinalValueTemplateAndCompilationCommand % We first add on the first line the compilation command, and on the second line the template file. \str_set:Nx \l__robExt_dependencies_mdfive_str {command,\l__robExt_current_compilation_command_str^^J\robExt@pdfmdfivesum{\l_robExt_result_str ^^J},^^J} %% ^^J is a newline: LaTeX will automatically add a new line when writing the file \seq_map_inline:Nn \l__robExt_dependencies_str { \str_put_right:Nx \l__robExt_dependencies_mdfive_str {\file_mdfive_hash:n{##1},##1^^J} %% ^^J is a newline } %% %% Compute the final hash (the hash of all dependencies, including the current picture that is on the first line): %% The last newline is needed as the write operation automatically adds a newline. \tl_set:Nx \robExtFinalHash {\robExt@pdfmdfivesum{\l__robExt_dependencies_mdfive_str^^J}} % Might be useful to clean the previously compiled files, or change the MD5 sum, for instance if we do % not want to recompile when the file changes \robExtUseHookJustBeforeWritingFiles% %% We add the figure in the list of files. % Avoid writing the same command multiple times (might occur in environments that execute the % same command multiple times like align etc) \cs_if_exist:cTF {l__robExt_write_list_all_figures_\robExtAddPrefixName{\robExtFinalHash.tex}:}{}{ \iow_now:Nx \g__robExt_write_list_all_figures_iow {\robExtAddPrefixName{\robExtFinalHash.tex}} \cs_gset:cn {l__robExt_write_list_all_figures_\robExtAddPrefixName{\robExtFinalHash.tex}:} {} } %% We can now set the placeholders, and recompute the final value of the file: %% I was doing before \robExtPlaceholderFromContent + \robExtEvalPlaceholderInplace, but it is too slow \str_set:Nx \l_tmp_output_prefix_str {\robExtAddPrefixName{\robExtFinalHash}} %%% No need to re-evaluate from scratch the string, \str_replace_all:Nnx \l_robExt_result_str {__ROBEXT_OUTPUT_PREFIX__}{\l_tmp_output_prefix_str} \str_replace_all:Nnx \l_robExt_result_str {__ROBEXT_SOURCE_FILE__}{\l_tmp_output_prefix_str .tex} \str_replace_all:Nnx \l_robExt_result_str {__ROBEXT_OUTPUT_PDF__}{\l_tmp_output_prefix_str .pdf} \str_replace_all:Nnx \l_robExt_result_str {__ROBEXT_WAY_BACK__}{\robExtCacheFolderWayBack} \str_replace_all:Nnx \l_robExt_result_str {__ROBEXT_CACHE_FOLDER__}{\robExtCacheFolder} \file_if_exist:xTF{\robExtAddCachePathAndName{\robExtFinalHash.tex}}{ \robExtDebugInfo{The\space file\space \robExtAddCachePathAndName{\robExtFinalHash.tex} \space already\space exists.^^J} }{ % arXiv removes the .pdf if the .tex is present... so we need to manually remove the .tex file. \file_if_exist:xTF{\robExtAddCachePathAndName{\robExtFinalHash.pdf}}{ \robExtDebugInfo{The\space file\space \robExtAddCachePathAndName{\robExtFinalHash.pdf} \space already\space exists.^^J} % Sometimes we still want to refer to the original tex file, for instance to input the source. % So if a file .tex-backup exists, we copy it back to .tex: \file_if_exist:xTF{\robExtAddCachePathAndName{\robExtFinalHash.tex-backup}} { \robExtDebugInfo{We~copy~the~source~\robExtFinalHash.tex-backup~to~\robExtFinalHash.tex^^J}% % First we read the file: \ior_open:Nn \g__robExt_read_ior {\robExtAddCachePathAndName{\robExtFinalHash.tex-backup}}% \iow_open:Nx \g__robExt_write_iow {\robExtAddCachePathAndName{\robExtFinalHash.tex}} \ior_str_map_inline:Nn \g__robExt_read_ior {% \iow_now:Nx \g__robExt_write_iow {\tl_to_str:n{##1}}% }% \iow_close:N \g__robExt_write_iow \ior_close:N \g__robExt_read_ior }{} }{ \str_if_eq:VnTF { \l_robExt_result_str }{__ROBEXT_TEMPLATE__} { \msg_error:nn{robExt}{forgot template} }{ % Check if the output directory exists \robExtCheckIfPrefixFolderExists \iow_open:Nx \g__robExt_write_iow {\robExtAddCachePathAndName{\robExtFinalHash.deps}} \iow_now:NV \g__robExt_write_iow \l__robExt_dependencies_mdfive_str \iow_close:N \g__robExt_write_iow %% Save the final file: \iow_open:Nx \g__robExt_write_iow {\robExtAddCachePathAndName{\robExtFinalHash.tex}} \iow_now:NV \g__robExt_write_iow \l_robExt_result_str \iow_close:N \g__robExt_write_iow \robExtDebugInfo{Source ~ saved ~ in ~ \robExtAddCachePathAndName{\robExtFinalHash.tex}.} \ifdefined\robExtPrintSourceWhenSaving \typeout{Content~of~the~deps~file~\robExtAddCachePathAndName{\robExtFinalHash.deps}:^^J} \robExt_write_file_in_log:x {\robExtAddCachePathAndName{\robExtFinalHash.deps}} \typeout{Content~of~the~source~file~\robExtAddCachePathAndName{\robExtFinalHash.tex}:^^J} \robExt_write_file_in_log:x {\robExtAddCachePathAndName{\robExtFinalHash.tex}} \fi } } } } % https://tex.stackexchange.com/questions/133324/shell-escape-with-latex-3 % We need shell escape to work (but it's enabled by default on overleaf!) % Think about the number of compilations. \NewDocumentCommand{\robExtCompileFile}{m}{ \file_if_exist:xTF{\robExtAddCachePathAndName{\robExtFinalHash.pdf}}{ \cs_if_exist:NTF {\robExtForceRecompilation}{ % We cannot do an OR with file_if_exist since it is not expandable, hence this trick \let\l__robExt_do_not_compile\undefined }{ \def\l__robExt_do_not_compile{} } }{ \let\l__robExt_do_not_compile\undefined } \cs_if_exist:NTF {\l__robExt_do_not_compile}{ \robExtDebugInfo{No ~ need ~ to ~ recompile ~ \robExtAddCachePathAndName{\robExtFinalHash.pdf}^^J} }{ % Used to know later if we have run a compilation command at that step or not \cs_undefine:N \l__robExt_I_just_ran_a_compilation_command: % No need to re-evaluate it % \robExtGetPlaceholderInResultFromSinglePlaceholder{__ROBEXT_COMPILATION_COMMAND__} \str_replace_all:Nnx \l__robExt_current_compilation_command_str {__ROBEXT_OUTPUT_PREFIX__}{\l_tmp_output_prefix_str} \str_replace_all:Nnx \l__robExt_current_compilation_command_str {__ROBEXT_SOURCE_FILE__}{\l_tmp_output_prefix_str .tex} \str_replace_all:Nnx \l__robExt_current_compilation_command_str {__ROBEXT_OUTPUT_PDF__}{\l_tmp_output_prefix_str .pdf} \str_replace_all:Nnx \l__robExt_current_compilation_command_str {__ROBEXT_WAY_BACK__}{\robExtCacheFolderWayBack} \str_replace_all:Nnx \l__robExt_current_compilation_command_str {__ROBEXT_CACHE_FOLDER__}{\robExtCacheFolder} % Make sure this command is run from the cache folder \ifdefined\robExtCacheFolder \str_put_left:Nx \l__robExt_current_compilation_command_str {cd ~ \robExtCacheFolder \space && ~ } \fi% %%% We enable manual mode if we enabled "compile in parallel after=N" and we compiled more than N elements % Check if we want to run stuff in parallel \ifdefined\robExt@compile@parallel@after % TODO: **** \int_gset:Nn \g__robExt_number_figures_just_compiled_or_to_compile {\g__robExt_number_figures_just_compiled_or_to_compile + 1} % We check if the number of figures that we already compiled is large enough to start parallel compilation \int_compare:nNnTF { \g__robExt_number_figures_just_compiled_or_to_compile} > {\robExt@compile@parallel@after } { \def\robExtManualMode } {} \fi \ifdefined\robExtManualMode \message{[robExt] Manual mode enabled: please, manually compile the images using \l__robExt_current_compilation_command_str or run 'bash \jobnameNoQuotes-\robExtAddPrefixName{compile-missing-figures.sh}'.} % Avoid writing the same command multiple times (might occur in environments that execute the % same command multiple times like align etc) \cs_if_exist:cTF {l__robExt_missing_figure_\l__robExt_current_compilation_command_str :}{}{ \ifdefined\robExtDoNotRedirectOutputInManuallyCompileMissingFigures% \iow_now:Nx \g__robExt_write_manually_compile_all_missing_figures_iow {\l__robExt_current_compilation_command_str}% a new line is automatically added \else \iow_now:Nx \g__robExt_write_manually_compile_all_missing_figures_iow {(~\l__robExt_current_compilation_command_str~)~>~\robExtAddCachePathAndName{\robExtFinalHash-compilation.log}~2>&1}% a new line is automatically added \fi \cs_gset:cn {l__robExt_missing_figure_\l__robExt_current_compilation_command_str :} {} \ifdefined\robExt@compile@parallel@after \gdef\robExt@compile@parallel@must@compile{} \seq_gput_right:Nx \g__robExt_files_compiled_in_parallel {\robExtAddCachePathAndName{\robExtFinalHash}} % Gets the line with the error for easier debugging \cs_gset:cx {g__robExt_lines_error_\robExtAddCachePathAndName{\robExtFinalHash}:} {\msg_line_number:} \ifdefined\robExtPrintWholeFile \cs_gset:cx {g__robExt_print_whole_file_\robExtAddCachePathAndName{\robExtFinalHash}:} {} \fi \cs_gset:cx {g__robExt_compilation_command_\robExtAddCachePathAndName{\robExtFinalHash}:} {\l__robExt_current_compilation_command_str} \fi } \else% \bool_if:nTF { \sys_if_shell_unrestricted_p: || \cs_if_exist_p:N \robExtForceCompilation } { % For a better debugging, we create a new file: \ifdefined\robExtDoNotRedirectOutput% \sys_shell_now:x {\robExtPrefixAllCompilationCommands \l__robExt_current_compilation_command_str}% \else \sys_shell_now:x {\robExtPrefixAllCompilationCommands (~\l__robExt_current_compilation_command_str~)~>~\robExtAddCachePathAndName{\robExtFinalHash-compilation.log}~2>&1}% \fi \message{[robExt] We ~ will ~ start ~ the ~ compilation ~ using: ~ \l__robExt_current_compilation_command_str.}% %% We notify that we are running the compilation command, this way it is possible to check later %% if the file is not present because of some compilation error or because we are in manual mode %% or fallback \cs_set:Nn \l__robExt_I_just_ran_a_compilation_command: {} }{ \ifdefined\robExtFallbackManualMode % Avoid writing the same command multiple times (might occur in environments that execute the % same command multiple times like align etc) \cs_if_exist:cTF {l__robExt_missing_figure_\l__robExt_current_compilation_command_str :}{}{ \message{[robExt] Fallback to manual mode: please, manually compile the images using \l__robExt_current_compilation_command_str or run 'bash \jobnameNoQuotes-\robExtAddPrefixName{compile-missing-figures.sh}'.} \ifdefined\robExtDoNotRedirectOutputInManuallyCompileMissingFigures% \iow_now:Nx \g__robExt_write_manually_compile_all_missing_figures_iow {\l__robExt_current_compilation_command_str}% a new line is automatically added \else \iow_now:Nx \g__robExt_write_manually_compile_all_missing_figures_iow {(~\l__robExt_current_compilation_command_str~)~>~\robExtAddCachePathAndName{\robExtFinalHash-compilation.log}~2>&1}% a new line is automatically added \fi \cs_gset:cn {l__robExt_missing_figure_\l__robExt_current_compilation_command_str :} {} \ifdefined\robExt@compile@parallel@after \gdef\robExt@compile@parallel@must@compile{} \seq_gput_right:Nx \g__robExt_files_compiled_in_parallel {\robExtAddCachePathAndName{\robExtFinalHash}} \fi } \else \ifdefined\robExtPrintSourceWhenSaving \typeout{Compilation~command~that~we~would~have~run:^^J\l__robExt_current_compilation_command_str^^J} \fi \msg_error:nn{robExt}{need shell escape} \fi } \fi } } \robExtSetPlaceholder*{__ROBEXT_INCLUDE_COMMAND__}{% \ifdefined\robExtDepth% \raisebox{-\robExtDepth}{% \includegraphics[__ROBEXT_INCLUDEGRAPHICS_OPTIONS__]{% __ROBEXT_INCLUDEGRAPHICS_FILE__% }}% \else% \includegraphics[__ROBEXT_INCLUDEGRAPHICS_OPTIONS__]{% %\robExtAddCachePathAndName{\robExtFinalHash.pdf}% __ROBEXT_INCLUDEGRAPHICS_FILE__% }% \fi } % These special placeholders are not loaded before for efficiency reasons. \NewDocumentCommand{\robExtLoadSpecialPlaceholders}{}{ \robExtPlaceholderFromString {__ROBEXT_OUTPUT_PREFIX__}{\l_tmp_output_prefix_str} \robExtPlaceholderFromStringExpanded {__ROBEXT_SOURCE_FILE__}{\l_tmp_output_prefix_str .tex} \robExtPlaceholderFromStringExpanded {__ROBEXT_OUTPUT_PDF__}{\l_tmp_output_prefix_str .pdf} \robExtPlaceholderFromStringExpanded {__ROBEXT_WAY_BACK__}{\robExtCacheFolderWayBack} \robExtPlaceholderFromStringExpanded {__ROBEXT_CACHE_FOLDER__}{\robExtCacheFolder} } \def\robExtIncludeGraphicsArgs{} %%% This command is not meant to be called by the end user. It will be called after the compilation to include %%% the compiled file back into the original file. \NewDocumentCommand{\robExtIncludeFile}{m}{% % We can disable this loading for efficiency reasons \ifdefined\robExtDoNotLoadSpecialPlaceholders\else\robExtLoadSpecialPlaceholders\fi% \ifdefined\robExtIncludeCommandAdvanced% \robExtIncludeCommandAdvanced% \else% {% \file_if_exist:xTF{\robExtAddCachePathAndName{\robExtFinalHash.pdf}}{% \file_if_exist:xTF{\robExtAddCachePathAndName{\robExtFinalHash-out.tex}}{% \kern0pt%Without the kern, the next unskip would eat spaces before... and we don't want that. See also % https://tex.stackexchange.com/questions/104034/when-is-it-good-practice-to-use-unskip \input{\robExtAddCachePathAndName{\robExtFinalHash-out.tex}}\unskip% Otherwise if the file contains space it will be added here. }{}% %\robExtConfigure{run ~ before ~ include ~ command}% \ifdefined\robExtIncludeCommand% \robExtIncludeCommand% \else% \robExtEvalPlaceholderStartFromList{__ROBEXT_INCLUDE_COMMAND__,__ROBEXT_INCLUDEGRAPHICS_OPTIONS__,__ROBEXT_INCLUDEGRAPHICS_FILE__,__ROBEXT_LATEX_TRIM_LENGTH__}{__ROBEXT_INCLUDE_COMMAND__}% \fi% %\robExtConfigure{run ~ after ~ include ~ command}% }{ % Check if we tried to run a compilation command at the previous step: \cs_if_exist:NTF \l__robExt_I_just_ran_a_compilation_command: { % We ran a compilation command but it failed: we print an error message % We first check if an error file has been produced: \file_if_exist:nTF {\robExtAddCachePathAndName{\robExtFinalHash-compilation.log}} { % We print the error twice as some editor might display only the first error while in others % it might be easier to see the last error. For instance, in emacs if we do not do that % then it first prints the error message in a file in the cache (but it forgets the % robustExternalize prefix) % But first we get the lines containing the word error: \robExt_get_errors_from_file:n {\robExtAddCachePathAndName{\robExtFinalHash-compilation.log}}% % We print the message before the log because otherwise emacs is confused and tries to open the % auxiliary cached file, and I don't want that. \ifdefined\robExtHideFirstErrorMessage\else \msg_error:nnxxxx{robExt}{missing compiled pdf parallel with log}{\robExtAddCachePathAndName{\robExtFinalHash}}{\msg_line_number:}{\l__robExt_current_compilation_command_str}{below~(you~might~need~to~press~ENTER~to~go~to~the~next~error)} \fi \message{--------~We~print~now~the~full~log~--------^^J} \robExt_write_file_in_log:n {\robExtAddCachePathAndName{\robExtFinalHash-compilation.log}}% \message{--------~End~of~the~full~log~--------^^J} \msg_error:nnxxxx{robExt}{missing compiled pdf parallel with log}{\robExtAddCachePathAndName{\robExtFinalHash}}{\msg_line_number:}{\l__robExt_current_compilation_command_str}{above} }{ \msg_error:nnxxx{robExt}{missing compiled pdf parallel}{\robExtAddCachePathAndName{\robExtFinalHash}}{\msg_line_number:}{\l__robExt_current_compilation_command_str} }% } { % We ran no compilation command: we print instead a placeholder depending on the context: \cs_if_exist:NTF \robExt@compile@parallel@must@compile { % we are supposed to compile the current picture in parallel, let us check if it will be possible: \bool_if:nTF { \sys_if_shell_unrestricted_p: || \cs_if_exist_p:N \robExtForceCompilation} { \robExtImagePlaceholderIfParallelCompilation }{ \robExtImagePlaceholderIfManualMode } }{ \cs_if_exist:NTF \robExtManualMode { \robExtImagePlaceholderIfManualMode \message{[robExt] ~ You ~ are ~ in ~ manual ~ mode: ~ please ~ compile ~ yourself ~ \robExtAddCachePathAndName{\robExtFinalHash.tex} ~ or ~ use ~ the ~ bash ~ \jobnameNoQuotes-\robExtAddPrefixName{compile-missing-figures.sh}} }{ \robExtImagePlaceholderIfFallbackMode \message{[robExt] ~ You ~ are ~ falling ~ back ~ to ~ manual ~ mode: ~ please ~ compile ~ yourself ~ \robExtAddCachePathAndName{\robExtFinalHash.tex} ~ or ~ use ~ the ~ bash ~ \jobnameNoQuotes-\robExtAddPrefixName{compile-missing-figures.sh}} } } } }% }% \fi% } %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Disabling externalization %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % It seems that when we disable externalization on \tikz, \tikz internally call \tikzpicture in a % weird way (certainly some tikz magic), and as a result it does not manage to grab the end of the CacheMe % environment. For this reason, by default, |disable externalization| will disable **all** commands \seq_clear_new:N \l_commands_to_reset_seq % List of commands to reset by default when disable externalization is set \NewDocumentCommand{\robExtAddToCommandResetList}{m}{ \seq_put_right:Nn \l_commands_to_reset_seq {#1} } \NewDocumentCommand{\robExtSetCommandResetList}{m}{ \seq_set_from_clist:Nn \l_commands_to_reset_seq {#1} } \seq_clear_new:N \l_environments_to_reset_seq % List of environments to reset by default when disable externalization is set \NewDocumentCommand{\robExtAddToEnvironmentResetList}{m}{ \seq_put_right:Nn \l_environments_to_reset_seq {#1} } \NewDocumentCommand{\robExtSetEnvironmentResetList}{m}{ \seq_set_from_clist:Nn \l_environments_to_reset_seq {#1} } \NewDocumentCommand{\robExtDisableTikzpictureOverwrite}{}{% \ifdefined\robExtTikzPictureOrig% \let\tikzpicture\robExtTikzPictureOrig% \let\endtikzpicture\endrobExtTikzPictureOrig% \fi% \ifdefined\robExtEnvironmentOrigName% \expanded{\noexpand\DeclareEnvironmentCopy{\robExtEnvironmentOrigName}{robExtEnvironmentOrig\robExtEnvironmentOrigName}}% \fi% \ifdefined\robExtCommandOrigName% \expandafter\DeclareCommandCopy\csname \robExtCommandOrigName\expandafter\endcsname\csname robExtCommandOrig\robExtCommandOrigName\endcsname% \fi% \ifdefined\robExtDoNotResetAllCommands\else% \seq_map_inline:Nn \l_commands_to_reset_seq { \cs_if_exist:cTF { robExtCommandOrig##1 } { \expandafter\DeclareCommandCopy\csname ##1\expandafter\endcsname\csname robExtCommandOrig##1\endcsname% } {} } \seq_map_inline:Nn \l_environments_to_reset_seq { \cs_if_exist:cTF { robExtEnvironmentOrig##1 } { \expanded{\noexpand\DeclareEnvironmentCopy{##1}{robExtEnvironmentOrig##1}} } {} } \fi% } %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Automatically forward elements %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % \robExtSetCodeToRunIfMacroPresent{\hello}{some code} will execute "some code" if \hello is present in % the main content to build and if "auto forward" is enabled. \NewDocumentCommand{\robExtSetCodeToRunIfMacroPresent}{mm}{ % we need to remove the leading space % \str_set:Nx \l__robExt_tmp_str {\cs_to_str:N #1} \expandafter\def \csname l__robExt_execute_if_macro_present\string#1\endcsname {#2}% } % \robExtRunCodeToRunForMacroPresent{\hello} will run the code that was configured when calling % \robExtSetCodeToRunIfMacroPresent{\hello}{some code} \NewDocumentCommand{\robExtRunCodeToRunForMacroPresent}{m}{ % \str_set:Nx \l__robExt_tmp_str {\cs_to_str:N ##1} \cs_if_exist_use:c {l__robExt_execute_if_macro_present\string#1} } % \autoForwardMacro{\hello}[\firstMacroDeps,\secondMacroDeps] will be used to tell to "auto forward" to forward \hello, \firstMacroDeps, and \secondMacroDeps if \hello is present in the main content to build. \NewDocumentCommand{\robExtConfigIfMacroPresent}{mm}{% \robExtSetCodeToRunIfMacroPresent{#1}{% \pgfkeysalso{#2}% }% } \let\configIfMacroPresent\robExtConfigIfMacroPresent \NewDocumentCommand{\robExtAutoForwardMacro}{mO{}}{% \robExtConfigIfMacroPresent{#1}{ forward=#1,#2 }%a } \let\autoForwardMacro\robExtAutoForwardMacro \NewDocumentCommand{\robExtLoadAutoForwardMacroConfig}{+m}{ %\str_set:Nx \l__robExt_tmp_str {\cs_to_str:N #1} \cs_if_exist:cTF {l__robExt_execute_if_macro_present\string#1}{ % if the macro is present twice, we want to run \forward only once \cs_if_exist:cTF {l__robExt_execute_if_macro_present_already_forwarded\string#1 :}{}{ \use:c {l__robExt_execute_if_macro_present\string#1} % define it so that we do not import twice next time \cs_set:cx {l__robExt_execute_if_macro_present_already_forwarded\string#1 :} {} } }{} } %% Old version: too inneficient % \regex_const:Nn \l__robExt_macro_regex { \\[A-Za-z]+ } % \NewDocumentCommand{\robExtAutoForward}{}{ % \seq_clear_new:N \l__robExt_matches_seq % \regex_extract_all:NVN \l__robExt_macro_regex \l__robExt_placeholder___ROBEXT_MAIN_CONTENT_ORIG___str \l__robExt_matches_seq% % \seq_map_inline:Nn \l__robExt_matches_seq { % \robExtLoadAutoForwardMacroConfig{##1} % } % } %% "auto forward" \NewDocumentCommand{\robExtAutoForward}{}{ % some code will automatically add auto forward, but we don't want to run it twice. \robExtDebugInfo{Running auto forward.} \ifdefined\robExt@already@ran@autoforward\robExtDebugInfo{Auto forward already ran}\else \ifdefined\robExtUserInputCacheMe\else \msg_warning:nn{robExt}{auto forward not in cachemecode} \tl_set_rescan:NnV \robExtUserInputCacheMe {} \l__robExt_placeholder___ROBEXT_MAIN_CONTENT_ORIG___str \fi \robExt@normalBraces\robExtUserInputCacheMe \expandafter\robExt@getfromstringA\robExtUserInputCacheMe\robExt@getfromstringA%\robExt@getfromstringA is added to know when to stop \def\robExt@already@ran@autoforward{}% \fi } % https://tex.stackexchange.com/questions/700834/efficiently-program-search-of-macro-in-string/700844 \long\def\robExt@getfromstringA#1{\ifx#1\robExt@getfromstringA \else %\ifcsname L:\string#1\endcsname \addto\listmacros{#1}\fi \robExtLoadAutoForwardMacroConfig{#1}% \expandafter\robExt@getfromstringA\fi } \def\robExt@normalBraces#1{{\catcode`{=12 \catcode`}=12 \everyeof={\endfile}% \expandafter\robExt@normalBracesA\expandafter#1\scantokens\expandafter{#1}}} \long\def\robExt@normalBracesA#1#2\endfile{\gdef#1{#2}} %%% To automatically define and forward \NewDocumentCommand{\newcommandAutoForward}{moomO{}}{ \IfNoValueTF{#2}{ \IfNoValueTF{#3}{ \newcommand{#1}{#4} }{ \newcommand{#1}[#3]{#4} } }{ \IfNoValueTF{#3}{ \newcommand{#1}[#2]{#4} }{ \newcommand{#1}[#2][#3]{#4} } } \robExtAutoForwardMacro{#1}[#5] } %%% To automatically define and forward \NewDocumentCommand{\renewcommandAutoForward}{moomO{}}{ \IfNoValueTF{#2}{ \IfNoValueTF{#3}{ \renewcommand{#1}{#4} }{ \renewcommand{#1}[#3]{#4} } }{ \IfNoValueTF{#3}{ \renewcommand{#1}[#2]{#4} }{ \renewcommand{#1}[#2][#3]{#4} } } \robExtAutoForwardMacro{#1}[#5] } %%% To automatically define and forward \NewDocumentCommand{\providecommandAutoForward}{moomO{}}{ \IfNoValueTF{#2}{ \IfNoValueTF{#3}{ \providecommand{#1}{#4} }{ \providecommand{#1}[#3]{#4} } }{ \IfNoValueTF{#3}{ \providecommand{#1}[#2]{#4} }{ \providecommand{#1}[#2][#3]{#4} } } \robExtAutoForwardMacro{#1}[#5] } \NewDocumentCommand{\NewDocumentCommandAutoForward}{mmO{}m}{ \NewDocumentCommand{#1}{#2}{#4} \robExtAutoForwardMacro{#1}[#3] } \NewDocumentCommand{\RenewDocumentCommandAutoForward}{mmO{}m}{ \RenewDocumentCommand{#1}{#2}{#4} \robExtAutoForwardMacro{#1}[#3] } \NewDocumentCommand{\ProvideDocumentCommandAutoForward}{mmO{}m}{ \ProvideDocumentCommand{#1}{#2}{#4} \robExtAutoForwardMacro{#1}[#3] } \NewDocumentCommand{\DeclareDocumentCommandAutoForward}{mmO{}m}{ \DeclareDocumentCommand{#1}{#2}{#4} \robExtAutoForwardMacro{#1}[#3] } \NewDocumentCommand{\NewExpandableDocumentCommandAutoForward}{mmO{}m}{ \NewExpandableDocumentCommand{#1}{#2}{#4} \robExtAutoForwardMacro{#1}[#3] } \NewDocumentCommand{\RenewExpandableDocumentCommandAutoForward}{mmO{}m}{ \RenewExpandableDocumentCommand{#1}{#2}{#4} \robExtAutoForwardMacro{#1}[#3] } \NewDocumentCommand{\ProvideExpandableDocumentCommandAutoForward}{mmO{}m}{ \ProvideExpandableDocumentCommand{#1}{#2}{#4} \robExtAutoForwardMacro{#1}[#3] } \NewDocumentCommand{\DeclareExpandableDocumentCommandAutoForward}{mmO{}m}{ \DeclareExpandableDocumentCommand{#1}{#2}{#4} \robExtAutoForwardMacro{#1}[#3] } \NewDocumentCommand{\defAutoForward}{mO{}mO{}}{ \def#1#2{#3}% \robExtAutoForwardMacro{#1}[#4]% } % \robExtGenericAutoForward[tikz][namespace]{string to match}[additional style to run]{ % code to run in cached file if string matches, and to always run in current folder if not using % the star version % } \ExplSyntaxOff \NewDocumentCommand{\definecolorAutoForward}{mmmO{}}{% \definecolor{#1}{#2}{#3}% \robExtConfigure{if matches word={#1}{forward color=#1,#4}}% } \NewDocumentCommand{\colorletAutoForward}{mmO{}}{% \colorlet{#1}{#2}% \robExtConfigure{if matches word={#1}{forward color=#1,#3}}% } \NewDocumentCommand{\robExtRunHereAndInPreambleOfCachedFiles}{O{latex}m}{% #2% \robExtConfigure{% add to preset={#1}{% add to preamble={#2}, },% }% } \let\runHereAndInPreambleOfCachedFiles\robExtRunHereAndInPreambleOfCachedFiles \NewDocumentCommand{\robExtGenericAutoForward}{sO{latex}O{}mO{}m}{% \IfBooleanTF{#1}{}{#6}% We run the code right now \robExt@set@hash@robust\zxTmpMacro{#6}% \expanded{% \noexpand\robExtConfigure{% register word with namespace={#3}{#4}{% /utils/exec={% \noexpand\robExtAddBeforePlaceholder*{__ROBEXT_MAIN_CONTENT__}{% \expandonce{\zxTmpMacro}% }% },% #5% },% add to preset={#2}{% auto forward words namespace={#3},% }% }% }% } \let\genericAutoForward\robExtGenericAutoForward % uses "if matches" instead of "if matches word" \NewDocumentCommand{\robExtGenericAutoForwardStringMatch}{sO{latex}mO{}m}{% \IfBooleanTF{#1}{}{#5}% We run the code right now \robExt@set@hash@robust\zxTmpMacro{#5}% \robExtConfigure{% add to preset={#2}{% if matches={#3}{ run command if externalization={ \expanded{% \noexpand\robExtAddBeforePlaceholder*{__ROBEXT_MAIN_CONTENT__}{% \expandonce{\zxTmpMacro}% % #1%% }% }% },#4% }% }% }% } \let\genericAutoForwardStringMatch\robExtGenericAutoForwardStringMatch \ExplSyntaxOn %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Automatically forward (regex-based) %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % called by style "if matches". % \robExtRegexMatches{my regex}{my style} will apply "my style" if "__ROBEXT_MAIN_CONTENT_ORIG__" matches % "my regex". \NewDocumentCommand{\robExtIfMatchesRegex}{mm}{ \regex_match:nVTF {#1} \l__robExt_placeholder___ROBEXT_MAIN_CONTENT_ORIG___str {\pgfkeysalso{#2}} {} } \NewDocumentCommand{\robExtIfMatchesString}{mm}{ \str_if_in:NnTF \l__robExt_placeholder___ROBEXT_MAIN_CONTENT_ORIG___str {#1} {\pgfkeysalso{#2}} {} } % \robExtRegisterWord {namespace} {word} {style} \NewDocumentCommand{\robExtRegisterWord}{mmm}{ % \robExt_register_match_word:nnn {#1} {#2} {\pgfkeysalso{#3}} \robExtRegisterWordCode{#1}{#2}{\pgfkeysalso{#3}}% } %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Automatically forward (word-based) %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % For efficiency reasons, we first extract in a single pass all words in the text (a word is basically [A-Za-z]+) % and run a function on these words %%% I tried this, but it is really slow (500x slower than if_string_matches, overall 20% increase of %%% compilation time) % % \__robExt_auto_forward_words:N \commandToRunOnEachWord \stringToSearchOn % \cs_set:Nn \__robExt_auto_forward_words:NN { % % \l_tmpa_str will contain the current word read so far % \str_set:Nn \l_tmpa_str {}% % \str_map_inline:Nn #2 { % % \token_case_charcode:NnTF ##1 {} {} {} % \__robExt_if_letter:nTF {##1} { % \str_put_right:Nn \l_tmpa_str {##1} % }{ % \str_if_empty:NTF \l_tmpa_str { } { % % if the string is empty, we run the command on the string % #1 \l_tmpa_str% % \str_set:Nn \l_tmpa_str {}% we reset its value % } % } % } % } % %% \__robExt_if_letter:nTF {char} {true} {false} tests if an element is a letter % %% https://tex.stackexchange.com/a/700864/116348 % \prg_new_conditional:Npnn \__robExt_if_letter:n #1 { TF } % { % \bool_lazy_or:nnTF % { % \bool_lazy_and_p:nn % { \int_compare_p:nNn { `#1 } > { `a - 1 } } % { \int_compare_p:nNn { `#1 } < { `z + 1 } } % } % { % \bool_lazy_and_p:nn % { \int_compare_p:nNn { `#1 } > { `A - 1 } } % { \int_compare_p:nNn { `#1 } < { `Z + 1 } } % } % \prg_return_true: % \prg_return_false: % } % % \robExt_register_match_word {namespace that defaults to empty} {word} {code to run if word is present} % \cs_set:Nn \robExt_register_match_word:nnn { % \cs_set:cn {l__robExt_execute_if_word_present_#1_#2:} {#3} % } % % \robExt_try_to_execute_if_match_word:nn {namespace} {word} % \cs_set:Nn \robExt_try_to_execute_if_match_word:nn { % \cs_if_exist:cTF {l__robExt_execute_if_word_present_#1_#2:} {% % \cs_if_exist:cTF {l__robExt_execute_if_word_present_#1_#2__already_forwarded:}{\message{Already forwarded}}{ % \use:c {l__robExt_execute_if_word_present_#1_#2:}% % % define it so that we do not import twice next time % \cs_set:cx {l__robExt_execute_if_word_present_#1_#2__already_forwarded:} {} % } % } { } % } % \cs_generate_variant:Nn \robExt_try_to_execute_if_match_word:nn { nV } % \NewDocumentCommand{\robExtAutoForwardWords}{O{}}{ % \cs_set:Nn \__robExt_tmp_fct:N { % \robExt_try_to_execute_if_match_word:nV {#1} ##1 % } % \__robExt_auto_forward_words:NN \__robExt_tmp_fct:N \l__robExt_placeholder___ROBEXT_MAIN_CONTENT_ORIG___str % } % \robExtAutoForwardWords[namespace] \NewDocumentCommand{\robExtAutoForwardWords}{m}{% % needed or the original string will be changed \let\robExt@tmpString\l__robExt_placeholder___ROBEXT_MAIN_CONTENT_ORIG___str% \robExt@scanmacro@find@word{#1}\robExt@tmpString% } \ExplSyntaxOff % Thanks a lot wipet for this optimized version!! % https://tex.stackexchange.com/questions/701351/more-efficient-string-extraction-of-words/701441#701441 \def\robExtWordSeparators{{ };:."?!@+-/*,=\{\}[]\\'()&|~_^<>} % Not for user, use \robExtAutoForwardWords % \robExt@scanmacro@find@word{namespace}\stringToParse \def\robExt@scanmacro@find@word#1#2{% \def\robExt@namespace{#1}% % we will change the lccode (basically ascii code) of some characters like +- to delimit what a word is % supposed to be. Since we do not want to affect other stuff, we put it in a bgroup. \bgroup \expandafter\robExt@set@to@comma\robExtWordSeparators\relax% \ifdefined\robExtAdditionalCodeInScanMacroFindWord\robExtAdditionalCodeInScanMacroFindWord\fi% % This seems to set the string in lower case, so fill or Fill or get triggered. But it is not what we want here. % \lowercase\expandafter{\expandafter\gdef\expandafter#2\expandafter{#2}}% \lowercase\expandafter{\expandafter\gdef\expandafter#2\expandafter{#2}}% \edef#2{\detokenize\expandafter{#2}}% % \message{\string#2: \meaning#2} % prints the modified format of the scanned macro \expandafter\egroup% \expandafter\robExt@wordscan@aux#2,\relax,% } \def\robExt@set@to@comma #1{\ifx\relax#1\else \lccode`#1=`, \expandafter\robExt@set@to@comma\fi} \def\robExt@wordscan@aux#1,{\ifx\relax#1\empty\else% %\message{{#1}} % prints each scanned "word" \ifcsname robExt@action@to@run@on@word:\robExt@namespace:#1\endcsname \csname robExt@action@to@run@on@word:\robExt@namespace:#1\endcsname \fi% \expandafter\robExt@wordscan@aux\fi% } %\robExtRegisterWordCode{namespace}{word}{code} \def\robExtRegisterWordCode#1#2#3{\expandafter\gdef\csname robExt@action@to@run@on@word:\string#1:\string#2\endcsname{#3}} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Interface %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % We create interface into pgfkeys in order to allow easier creation of content via style \pgfkeys{% /robExt/.cd,% % We create a default style that will be loaded (mostly for the user) default style/.style={},% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Code to create new styles %%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % The advantage of this over .append style is that you do not need to double the number of hashes % don't know if there is a better solution. % https://tex.stackexchange.com/questions/695432/latex3-latex-doubles-the-number-of-hashes-when-storing-them-in-string/695461 add to preset/.code 2 args={% \robExtStrSetDoubleHash{\robExtTmpStr}{#2}% % Sadly, \expanded{\noexpand } does not work, as I get extra {} around the def, creating a group % so the simpler seems to use this library ^^ \robExtPlaceholderFromString{__ROBEXT_TMP__}{\robExtTmpStr}% \robExtEvalPlaceholderReplaceFromList{__ROBEXT_TMP__}{% \pgfkeys{% /robExt/.cd, #1/.append style={% % Some styles run differently if inside a preset or not, % like if matches word. This macro helps with detecting it. /utils/exec={\def\robExtCurrentlyDefiningPreset{}},% __ROBEXT_TMP__% },% }% }% \robExtRemovePlaceholder{__ROBEXT_TMP__}% let us clean our variables \let\robExtCurrentlyDefiningPreset\undefined% }, add before preset/.code 2 args={% \robExtStrSetDoubleHash{\robExtTmpStr}{#2}% % Sadly, \expanded{\noexpand } does not work, as I get extra {} around the def, creating a group % so the simpler seems to use this library ^^ \robExtPlaceholderFromString{__ROBEXT_TMP__}{\robExtTmpStr}% \robExtEvalPlaceholderReplaceFromList{__ROBEXT_TMP__}{% \pgfkeys{% /robExt/.cd, #1/.prefix style={% % Some styles run differently if inside a preset or not, % like if matches word. This macro helps with detecting it. /utils/exec={\def\robExtCurrentlyDefiningPreset{}},% __ROBEXT_TMP__% },% }% }% \robExtRemovePlaceholder{__ROBEXT_TMP__}% let us clean our variables \let\robExtCurrentlyDefiningPreset\undefined% }, new preset/.code 2 args={% \robExtStrSetDoubleHash{\robExtTmpStr}{#2}% % Sadly, \expanded{\noexpand } does not work, as I get extra {} around the def, creating a group % so the simpler seems to use this library ^^ \robExtPlaceholderFromString{__ROBEXT_TMP__}{\robExtTmpStr}% % \robExtShowPlaceholder*{__ROBEXT_TMP__} \robExtEvalPlaceholderReplaceFromList{__ROBEXT_TMP__}{% \pgfkeys{% /robExt/.cd,% #1/.style={% % Some styles run differently if inside a preset or not, % like if matches word. This macro helps with detecting it. /utils/exec={\def\robExtCurrentlyDefiningPreset{}},% __ROBEXT_TMP__% },% }% }% \robExtRemovePlaceholder{__ROBEXT_TMP__}% let us clean our variables \let\robExtCurrentlyDefiningPreset\undefined% }, % new compiled preset={latex compiled}{ % code to compile in order to produce the expected strings % }{ % code to initialize % } compile latex template/.style={ % to allow later addition to the preamble, we add a dummy value that we replace in a few lines % same for main content and content orig that we do not want to replace. add to placeholder={__ROBEXT_LATEX_PREAMBLE__}{__ROBEXT_LATEX_PREAMBLE_DUMMY__}, set placeholder={__ROBEXT_MAIN_CONTENT__}{__ROBEXT_MAIN_CONTENT_DUMMY__}, set placeholder={__ROBEXT_MAIN_CONTENT_ORIG__}{__ROBEXT_MAIN_CONTENT_ORIG_DUMMY__}, /utils/exec={% \robExtGetNearlyFinalValueTemplateAndCompilationCommand% \robExtSetBackCompilationCommandAndTemplate% }, set placeholder={__ROBEXT_LATEX_PREAMBLE_DUMMY__}{__ROBEXT_LATEX_PREAMBLE__}, set placeholder={__ROBEXT_MAIN_CONTENT_DUMMY__}{__ROBEXT_MAIN_CONTENT__}, set placeholder={__ROBEXT_MAIN_CONTENT_ORIG_DUMMY__}{__ROBEXT_MAIN_CONTENT_ORIG__}, set placeholder rec replace from list={__ROBEXT_TEMPLATE__,__ROBEXT_LATEX_PREAMBLE_DUMMY__,__ROBEXT_MAIN_CONTENT_DUMMY__,__ROBEXT_MAIN_CONTENT_ORIG_DUMMY__}{__ROBEXT_TEMPLATE__}{__ROBEXT_TEMPLATE__}, % For the include command set placeholder rec replace from list={__ROBEXT_INCLUDE_COMMAND__,__ROBEXT_INCLUDEGRAPHICS_OPTIONS__,__ROBEXT_INCLUDEGRAPHICS_FILE__,__ROBEXT_LATEX_TRIM_LENGTH__}{__ROBEXT_INCLUDE_COMMAND__}{__ROBEXT_INCLUDE_COMMAND__}, }, keep placeholder after group/.code={\robExtKeepPlaceholderAfterGroup{#1}}, new compiled preset/.code n args={3}{ \robExtStrSetDoubleHash{\robExtCodeToCompileStr}{#2}% \robExtStrSetDoubleHash{\robExtCodeToRunStr}{#3}% % Sadly, \expanded{\noexpand } does not work, as I get extra {} around the def, creating a group % so the simpler seems to use this library ^^ \robExtPlaceholderFromString{__ROBEXT_TMP_CODE_TO_COMPILE__}{\robExtCodeToCompileStr}% \robExtPlaceholderFromString{__ROBEXT_TMP_CODE_TO_RUN__}{\robExtCodeToRunStr}% \robExtPlaceholderFromString{__ROBEXT_NAME_TEMPLATE_PREFIX__}{\robExtCodeToRunStr}% % \robExtShowPlaceholder*{__ROBEXT_TMP__} \robExtEvalPlaceholderReplaceFromList{__ROBEXT_TMP_CODE_TO_COMPILE__,__ROBEXT_TMP_CODE_TO_RUN__}{% \robExtConfigure{% #1-recompile/.code={% \begingroup% \robExtConfigure{ __ROBEXT_TMP_CODE_TO_COMPILE__, }% \robExtCopyPlaceholder*{__ROBEXT_COMPILED_#1_TEMPLATE__}{__ROBEXT_TEMPLATE__}% \robExtKeepPlaceholderAfterGroup{__ROBEXT_COMPILED_#1_TEMPLATE__}% \robExtCopyPlaceholder*{__ROBEXT_COMPILED_#1_COMPILATION_COMMAND__}{__ROBEXT_COMPILATION_COMMAND__}% \robExtKeepPlaceholderAfterGroup{__ROBEXT_COMPILED_#1_COMPILATION_COMMAND__}% \robExtCopyPlaceholder*{__ROBEXT_COMPILED_#1_INCLUDE_COMMAND__}{__ROBEXT_INCLUDE_COMMAND__}% \robExtKeepPlaceholderAfterGroup{__ROBEXT_COMPILED_#1_INCLUDE_COMMAND__}% \robExtRescanPlaceholderInVariableNoReplacement{robExtCompiledIncludeCommand#1}{__ROBEXT_INCLUDE_COMMAND__}% \robExtKeepaftergroup{robExtCompiledIncludeCommand#1}% \robExtKeepaftergroup{l__robExt_placeholder___ROBEXT_COMPILED_#1_INCLUDE_COMMAND___str}% \robExtKeepaftergroup{l__robExt_placeholder___ROBEXT_COMPILED_#1_TEMPLATE___str}% \robExtSaveDependencies{robExtCompiledDependencies#1}% \endgroup% }, #1-recompile, #1/.style={ disable placeholders, set placeholder={__ROBEXT_LATEX_PREAMBLE__}{}, add to preamble/.style={ add to placeholder={__ROBEXT_LATEX_PREAMBLE__}{####1}, }, /utils/exec={\robExtRestoreDependencies{robExtCompiledDependencies#1}}, custom include command={\csname robExtCompiledIncludeCommand#1\endcsname}, copy placeholder no import={__ROBEXT_TEMPLATE__}{__ROBEXT_COMPILED_#1_TEMPLATE__}, copy placeholder no import={__ROBEXT_COMPILATION_COMMAND__}{__ROBEXT_COMPILED_#1_COMPILATION_COMMAND__}, __ROBEXT_TMP_CODE_TO_RUN__ },% }% }% \robExtRemovePlaceholder{__ROBEXT_TMP_CODE_TO_RUN__}% let us clean our variables \robExtRemovePlaceholder{__ROBEXT_TMP_CODE_TO_COMPILE__}% let us clean our variables }, in command/.style={ set placeholder={__ROBEXT_MAIN_CONTENT__}{__ROBEXT_MAIN_CONTENT_ORIG__}, }, %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Interface to change placeholders %%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% remove placeholder/.code={\robExtRemovePlaceholder{#1}}, remove placeholders/.style={ remove placeholder/.list={#1}, }, set main content/.style={ set placeholder={__ROBEXT_MAIN_CONTENT_ORIG__}{#1} }, copy placeholder/.code 2 args={\robExtCopyPlaceholder{#1}{#2}}, copy placeholder no import/.code 2 args={\robExtCopyPlaceholder*{#1}{#2}}, set placeholder/.code 2 args={\robExtSetPlaceholder{#1}{#2}}, % do not import the placeholder (useful for efficiency reasons) set placeholder no import/.code 2 args={\robExtSetPlaceholder*{#1}{#2}}, set placeholder first/.code 2 args={\robExtSetPlaceholderFirst*{#1}{#2}}, set placeholder rec/.code 2 args={\robExtSetPlaceholderRec{#1}{#2}}, set placeholder rec keep after group/.code 2 args={\robExtSetPlaceholderRec{#1}{#2}\robExtKeepPlaceholderAfterGroup{#1}}, set placeholder rec no import/.code 2 args={\robExtSetPlaceholderRec*{#1}{#2}}, set placeholder rec replace from list/.code n args={3}{\robExtSetPlaceholderRecReplaceFromList{#1}{#2}{#3}}, set placeholder rec replace from list no import/.code n args={3}{\robExtSetPlaceholderRecReplaceFromList*{#1}{#2}{#3}}, set placeholder eval/.code 2 args={\robExtSetPlaceholderRec{#1}{#2}\robExtEvalPlaceholderInplace{#1}}, set placeholder eval no import/.code 2 args={\robExtSetPlaceholderRec*{#1}{#2}\robExtEvalPlaceholderInplace{#1}}, set placeholder eval replace from list/.code n args={3}{\robExtSetPlaceholderRecReplaceFromList{#1}{#2}{#3}\robExtEvalPlaceholderInplace{#2}}, set placeholder eval replace from list no import/.code n args={3}{\robExtSetPlaceholderRecReplaceFromList*{#1}{#2}{#3}\robExtEvalPlaceholderInplace{#2}}, eval placeholder/.code={\robExtEvalPlaceholder{#1}}, eval placeholder replace from list/.code 2 args={\robExtEvalPlaceholderReplaceFromList{#1}{#2}}, set placeholder from content/.code 2 args={\robExtPlaceholderFromContent{#1}{#2}}, set placeholder from content no import/.code 2 args={\robExtPlaceholderFromContent*{#1}{#2}}, add to placeholder/.code 2 args={\robExtAddToPlaceholder{#1}{#2}}, add to placeholder no import/.code 2 args={\robExtAddToPlaceholderNoImport{#1}{#2}}, add to placeholder no space/.code 2 args={\robExtAddToPlaceholder*{#1}{#2}}, add to placeholder no space no import/.code 2 args={\robExtAddToPlaceholderNoImport*{#1}{#2}}, add before placeholder/.code 2 args={\robExtAddBeforePlaceholder{#1}{#2}}, add before placeholder no space/.code 2 args={\robExtAddBeforePlaceholder*{#1}{#2}}, add before placeholder no import/.code 2 args={\robExtAddBeforePlaceholderNoImport{#1}{#2}}, add before placeholder no space no import/.code 2 args={\robExtAddBeforePlaceholderNoImport*{#1}{#2}}, set placeholder path from filename/.code 2 args={\robExtPlaceholderPathFromFilename{#1}{#2}}, set placeholder path from filename no import/.code 2 args={\robExtPlaceholderPathFromFilename*{#1}{#2}}, set placeholder from file content/.code 2 args={\robExtPlaceholderFromFileContent{#1}{#2}}, set placeholder from file content no import/.code 2 args={\robExtPlaceholderFromFileContent*{#1}{#2}}, set placeholder path from content/.code n args={3}{\robExtPlaceholderPathFromContent{#1}[#3]{#2}}, set placeholder path from content no import/.code n args={3}{\robExtPlaceholderPathFromContent*{#1}[#3]{#2}}, eval placeholder in place/.code={\robExtEvalPlaceholderInplace{#1}}, placeholder halve number hashes in place/.code={\robExtPlaceholderHalveNumberHashesInplace{#1}}, placeholder double number hashes in place/.code={\robExtPlaceholderDoubleNumberHashesInplace{#1}}, placeholder replace in place/.code n args={3}{\robExtPlaceholderReplaceInplace{#1}{#2}{#3}}, placeholder replace in place eval/.code n args={3}{\robExtPlaceholderReplaceInplaceEval{#1}{#2}{#3}}, placeholder prepend all lines/.code 2 args={\robExtPlaceholderPrependAllLines{#1}{#2}}, prepend all lines/.style={ placeholder prepend all lines={\robExtCurrentPlaceholderName}{#1}, }, placeholder remove spaces until/.code 2 args={\robExtPlaceholderRemoveSpacesUntil{#1}{#2}}, remove spaces until/.style={ placeholder remove spaces until={\robExtCurrentPlaceholderName}{#1}, }, placeholder strictly remove spaces until/.code 2 args={\robExtPlaceholderRemoveSpacesUntil{#1}[0]{#2}}, strictly remove spaces until/.style={ placeholder remove spaces until nospace={\robExtCurrentPlaceholderName}{#1}, }, placeholder remove leading spaces/.code={\robExtPlaceholderRemoveLeadingSpaces{#1}}, remove leading spaces/.code={\robExtPlaceholderRemoveLeadingSpaces{\robExtCurrentPlaceholderName}}, remove leading spaces if not disabled/.code={\ifdefined\robExtDoNotRemoveLeadingSpaces\else\robExtPlaceholderRemoveLeadingSpaces{\robExtCurrentPlaceholderName}\fi}, do not remove leading spaces/.code={\def\robExtDoNotRemoveLeadingSpaces{}}, % Interface to set template set template/.style={ set placeholder first={__ROBEXT_TEMPLATE__}{#1}, }, %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Interface for optimization %%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % It is important to create an efficient code (otherwise the library can becomes pointless) % and it can help a lot if we give some advices to the system that evaluates placeholders, notably on % the replacement order it should follow: %%%% "only placeholders" says "use only these placeholders in this template/compilation command/... in that order %%%% while "first placeholders" says "start with these placeholders, if it is not enough, continue as usual only placeholders in compilation command/.code={\robExtOnlyPlaceholdersInCompilationCommand{#1}}, first placeholders in compilation command/.code={\robExtFirstPlaceholdersInCompilationCommand{#1}}, only placeholders in template/.code={\robExtOnlyPlaceholdersInTemplate{#1}}, first placeholders in template/.code={\robExtFirstPlaceholdersInTemplate{#1}}, only placeholders in include command/.code={\robExtOnlyPlaceholdersInIncludeCommand{#1}}, first placeholders in include command/.code={\robExtFirstPlaceholdersInIncludeCommand{#1}}, %%%%%%%%%%%%% %%% Debug %%% %%%%%%%%%%%%% more logs/.code={\def\robExtDebugMessage##1{\message{^^J[robExt] ##1}}\def\robExtDebugInfo##1{^^J[robExt] ##1}}, less logs/.code={\def\robExtDebugMessage##1{}\def\robExtDebugInfo##1{}}, show placeholder/.code={\robExtShowPlaceholder{#1}}, show placeholders/.code={\robExtShowPlaceholders}, show placeholders contents/.code={\robExtShowPlaceholdersContents}, print imported placeholders except default/.code={\robExtPrintAllPlaceholdersExceptDefaults}, print imported placeholders/.code={\robExtPrintAllPlaceholders}, %%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Optimizations %%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%% do not load special placeholders/.code={\let\robExtDoNotLoadSpecialPlaceholders\undefined}, load special placeholders/.code={\def\robExtDoNotLoadSpecialPlaceholders{}}, do not load special placeholders, %% Replace very little placeholders, basically only __ROBEXT_MAIN_CONTENT__, __ROBEXT_MAIN_CONTENT_ORIG__, %% and the default (prefix, source, output, wayback, cache folder) disable placeholders/.code={\def\robExtDisablePlaceholders{}}, enable placeholders/.code={\let\robExtDisablePlaceholders\undefined}, %% If all placeholders have __, this option will speed up the compilation time all placeholders have underscores/.code={\def\robExtPlaceholderOnlyWithUnderscores{}}, not all placeholders have underscores/.code={\let\robExtPlaceholderOnlyWithUnderscores\undefined}, %% not sure if I want users to disable import mechanism as they might use dirty things enable import mechanism/.code={\def\robExtEnableImportMechanism{}}, disable import mechanism/.code={\let\robExtEnableImportMechanism\undefined}, disable optimizations/.style={ not all placeholders have underscores, disable import mechanism, }, enable optimizations/.style={ all placeholders have underscores, enable import mechanism, }, enable optimizations, %%%%%%%%%%%%%% %%% Groups %%% %%%%%%%%%%%%%% clear imported placeholders/.code={\robExtClearImportedPlaceholders}, remove imported placeholder/.code={\robExtRemoveImportedPlaceholder{#1}}, remove imported placeholders/.style={ remove imported placeholder/.list={#1}, }, print group placeholders/.code={\robExtPrintGroupPlaceholders{#1}}, new group placeholders/.code={\robExtRegisterGroupPlaceholders{#1}}, add placeholder to group/.code 2 args={\robExtAddPlaceholdersToGroup{#1}{#2}}, add placeholders to group/.code 2 args={\robExtAddPlaceholdersToGroup{#1}{#2}}, remove placeholder from group/.code 2 args={\robExtRemovePlaceholdersFromGroup{#1}{#2}}, remove placeholders from group/.code 2 args={\robExtRemovePlaceholdersFromGroup{#1}{#2}}, copy group placeholders/.code 2 args={\robExtCopyGroupPlaceholders{#1}{#2}}, append group placeholders/.code 2 args={\robExtAppendGroupPlaceholders{#1}{#2}}, append before group placeholders/.code 2 args={\robExtAppendGroupPlaceholders{#1}{#2}}, import placeholders from group/.code={\robExtImportPlaceholdersFromGroup{#1}}, import all placeholders/.code={\robExtImportAllPlaceholders}, % ok this is a different kind of group, here latex group { } import placeholder/.code={\robExtImportPlaceholder{#1}}, import all placeholders/.code={\robExtImportAllPlaceholders}, import placeholders/.style={ import placeholder/.list={#1}, }, import placeholder first/.code={\robExtImportPlaceholderFirst{#1}}, import placeholders first/.code={\robExtAddPlaceholdersToListFirst{#1}}, import placeholders from group/.code={\robExtImportPlaceholdersFromGroup{#1}}, print all registered groups/.code={\robExtPrintAllRegisteredGroups}, print all registered groups and placeholders/.code={\robExtPrintAllRegisteredGroupsAndPlaceholders}, show all registered groups/.code={\robExtShowAllRegisteredGroupsAndPlaceholders}, show all registered groups and placeholders/.code={\robExtShowAllRegisteredGroupsAndPlaceholders}, %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Configure dependencies %%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Auxiliary command: dependenciesList/.code={\robExtAddDependency{#1}}, % Usage like: dependencies={input_externalize.tex,input_b.tex} % They should be relative to the main file when using the subfolder option. dependencies/.style={ /utils/exec={\robExtResetDependencies{}}, dependenciesList/.list={#1} }, add dependencies/.style={ dependenciesList/.list={#1} }, reset dependencies/.code={\robExtResetDependencies{}}, %%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Compilation command %%% %%%%%%%%%%%%%%%%%%%%%%%%%%% % People might want to force the compilation even if shell_unrestricted is false, notably if they % allow commands like mkdir/cd/pdflatex to run in restricted mode. force compilation/.code={\def\robExtForceCompilation{}}, do not force compilation/.code={\let\robExtForceCompilation\undefined}, % Recompile the file even if it has already been compiled (we do NOT clean the file as we do not feel % safe to remove files from this code, but this can also be done by the user hooking into recompile/.code={\def\robExtForceRecompilation{}}, do not recompile/.code={\let\robExtForceRecompilation\undefined}, set compilation command/.code={\robExtSetCompilationCommand{#1}}, change source extension/.style={ add before placeholder/.expanded={__ROBEXT_COMPILATION_COMMAND__}{\robExtCp\space "__ROBEXT_SOURCE_FILE__" "__ROBEXT_OUTPUT_PREFIX__.#1" &&}, }, % like "set compilation command" but moves the "__ROBEXT_OUTPUT_PDF__-tmp" to "__ROBEXT_OUTPUT_PDF__" if % there is no error. set compilation command move if no error/.style={ set compilation command/.expanded={#1 && \robExtMv\space"__ROBEXT_OUTPUT_PDF__-tmp" "__ROBEXT_OUTPUT_PDF__"} }, set compilation command create if no error/.code={\robExtSetCompilationCommand{#1 && echo "" > __ROBEXT_OUTPUT_PDF__}}, do not redirect compilation output/.code={\def\robExtDoNotRedirectOutput{}\def\robExtDoNotRedirectOutputInManuallyCompileMissingFigures{}}, print whole file in error message/.code={\def\robExtPrintWholeFile{}}, do not print whole file in error message/.code={\let\robExtPrintWholeFile\undefined}, prefix log message with/.code={\def\robExtPrefixLogMessage{#1}}, spacing between lines in log/.code={\def\robExtMessageWithPrefixNumberLines{#1}}, % This adds a ! in front of the code if \robExtPrintWholeFile is defined logs should show as errors/.style={prefix log message with={!\space}}, texstudio/.style={ logs should show as errors, spacing between lines in log={^^J^^J(sorry, these 4 empty lines are ugly but needed for texstudio to consider them as separate errors, customize me "with spacing between lines in log")^^J}, }, remove line number/.code={\def\robExtRemoveLineNumber{}}, % Removes the l. if the log error line starts with l.42 or it will disturb emacs. do not remove line number/.code={\let\robExtRemoveLineNumber\undefined}, remove line number, nb lines after error to show/.code={\def\robExtLinesAfterError{#1}}, add argument to compilation command/.code={\robExtAddArgumentToCompilationCommand{#1}}, add arguments to compilation command/.style={ add argument to compilation command/.list={#1} }, % This adds arguments like add key value to compilation command={mykey=myvalue} will add to the % compilation command two arguments: "mykey" "myvalue" % This is useful for scripts that are called like myscript key1 arg1 key2 arg2 key3 arg3, which is a % simple way to pass multiple arguments to a script like a python script add key value argument to compilation command/.code args={#1=#2}{\robExtAddArgumentToCompilationCommand{#1}\robExtAddArgumentToCompilationCommand{#2}}, add key and file argument to compilation command aux/.style args={#1=#2}{ add key value argument to compilation command={{#1}={\ifdefined\robExtCacheFolderWayBack\robExtCacheFolderWayBack\fi#2}}, }, add key and file argument to compilation command/.style={ add key and file argument to compilation command aux/.list={#1}, add dependencies={#1}, }, %%%%%%%%%%%%%%%%%%%%%%%%% %%% Inclusion command %%% %%%%%%%%%%%%%%%%%%%%%%%%% %%% Configure the command to include the compiled file back into the main file % By default, include command does a bit of logic before running the actual command, notably to % input the -out.tex file in order to pass information from the compiled file to the current file. % If you want to do everything by yourself, use: custom include command advanced/.code={\def\robExtIncludeCommandAdvanced{#1}}, % The default include command includes the pdf, making sure it is raised depending on its depth, % but you can override it: custom include command/.code={\def\robExtIncludeCommand{#1}}, include command is input/.style={ custom include command={#1\input{\robExtAddCachePathAndName{\robExtFinalHash.pdf}}}, }, include command is input/.default={}, % Sometimes (gnuplot), we need to include an image located in the cache folder: % https://tex.stackexchange.com/questions/171587/append-entries-to-an-existing-graphicspath add cache to graphicspath/.code={% \appto\Ginput@path{\robExtCacheFolder}% },% %% This is needed notably if the cached elements are nested, like the include command uses itself a tikz picture %% etc cached via \cacheTikz... It it hard to reset everything efficiently (like we might not want to reset %% all compilation commands etc), so you can add here stuff that might need to be restored later. reset/.code={% \let\robExtIncludeCommandAdvanced\undefined% \let\robExtIncludeCommand\undefined% \let\robExtPrintWholeFile\undefined% \setPlaceholder{__ROBEXT_INCLUDEGRAPHICS_FILE__}{\robExtAddCachePathAndName{\robExtFinalHash.pdf}}% }, %% Use this when we do not want to include anything (e.g. the video will be processed later in the chain): do not include pdf/.style={ custom include command={}% }, %% If you do or do not want to ask latex to run the compilation commands itself (for instance for security %% reasons, you can use these commands and run the command manually later): enable manual mode/.code={\def\robExtManualMode{}}, disable manual mode/.code={\let\robExtManualMode\undefined}, enable fallback to manual mode/.code={\def\robExtFallbackManualMode{}}, disable fallback to manual mode/.code={\let\robExtFallbackManualMode\undefined}, % For the arxiv, we need to rename the source .tex into .tex-backup or it will be removed backup source for arxiv/.code={\def\robExtEnableBackupSource{}}, do not backup source for arxiv/.code={\let\robExtEnableBackupSource\undefined}, rename backup files for arxiv/.code={\robExtRenameBackupFilesForArxiv{#1}}, rename backup files for arxiv/.default={robExt-arxiv-files-to-rename.txt}, % print in the log the file. Useful to debug in arxiv where we do not have any access to the logs print source when saving/.code={\def\robExtPrintSourceWhenSaving{}}, copy file to cache/.code={\robExtCopyFileToCache{#1}}, %% Arguments to include graphics include graphics args/.code={\def\robExtIncludeGraphicsArgs{#1}}, %% The role of this command is to set \l_robExt_result_str, that will contain the final string. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Configuration of the cache %%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % For support of --output-directory set output directory/.code={\def\robExtOutputDirectory{#1}}, %% Configure the prefix (default to "robExt-") set filename prefix/.code={\def\robExtPrefixFilename{#1}}, % first argument is subfolder, second is how to get from subfolder to the folder containing the source: % set subfolder and way back={robustExternal/}{../} % synonyme, "cache folder" is prefered over "" set subfolder and way back/.code 2 args={\def\robExtCacheFolder{#1}\def\robExtCacheFolderWayBack{#2}}, set cache folder and way back/.code 2 args={\def\robExtCacheFolder{#1}\def\robExtCacheFolderWayBack{#2}}, no cache folder/.code={\let\robExtCacheFolder\undefined\def\robExtCacheFolderWayBack{}}, % By default we put everything in robustExternalize % Change this before starting to cache any library, and if you change it mid-document, be aware % that you will not be able to refer to elements in the old folder. set subfolder and way back={robustExternalize/}{../}, %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Enable externalization for tikz %%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% cache tikz/.code={\robExtExternalizeAllTikzpictures{}}, do not cache tikz/.code={\robExtDoNotExternalizeAllTikzpictures{}}, cache tikz 2 args/.code 2 args={\robExtExternalizeAllTikzpictures[#1][#2]{}}, cache tikz 3 args/.code n args={3}{\robExtExternalizeAllTikzpictures[#1][#2][3]{}}, %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Disable externalization %%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Note: this does not work reliably for now %% TODO: fix this! disable externalization/.code={\def\robExtDisableExternalization{}}, de/.style={disable externalization}, disable externalization now/.code={\robExtDisableTikzpictureOverwrite\def\robExtDisableExternalization{}}, enable externalization/.code={\let\robExtDisableExternalization\undefined}, % Useful to wrap, for instance, text command if no externalization/.code={}, command if no externalization/.code={\robExtDisableTikzpictureOverwrite\evalPlaceholder{__ROBEXT_MAIN_CONTENT__}}, print verbatim if no externalization/.style={ command if no externalization/.code={% \robExtPrintPlaceholder{__ROBEXT_MAIN_CONTENT__}% }, }, %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Forward macros if externalization is enabled %%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% command if externalization/.code={\def\robExt@we@are@already@in@command@if@externalization{}}, % Doing "command if externalization/.append code={}" is not always good since we don't always know if the style % will always be run before running "command if externalization". In particular this arrives when using % something like "if matches word={mypink}{forward color=mypink}" run command if externalization/.code={ \ifdefined\robExt@we@are@already@in@command@if@externalization% % we are already running command if externalization #1% \else% \pgfkeysalso{command if externalization/.append code={#1}}% \fi% }, fw/.style={forward=#1}, run code before main content if externalization enabled/.code={}, % run code before main content if externalization enabled/.code={ % \message{aaa #1} % \def\zx@tmp{#1}% % \message{xxx} % \show\zx@tmp% % \scantokens{% % \pgfkeysalso{ % command if externalization/.append code={% % \expanded{% % \noexpand\robExtAddBeforePlaceholder*{__ROBEXT_MAIN_CONTENT__}{% % % \expandonce{\zx@tmp}% % #1%% % }% % }% % }% % }% % }, % }, forward/.style={% run command if externalization={% \robExtGetCommandDefinitionInMacro{#1}% \expanded{% \noexpand\robExtAddBeforePlaceholder*{__ROBEXT_MAIN_CONTENT__}{% \expandonce{\robExtDefinitionCommand}% }% }% }, }, forward at letter/.style={% run command if externalization={% \robExtGetCommandDefinitionInMacro{#1}% \expanded{% \noexpand\robExtAddBeforePlaceholder*{__ROBEXT_MAIN_CONTENT__}{% \noexpand\makeatletter% \expandonce{\robExtDefinitionCommand}% \noexpand\makeatother% }% }% }, }, forward eval/.style={% run command if externalization={% \expanded{% \noexpand\robExtAddBeforePlaceholder*{__ROBEXT_MAIN_CONTENT__}{% \noexpand\def\noexpand#1{#1}% }% }% },% }, forward counter/.style={% run command if externalization={% \expanded{% \noexpand\robExtAddBeforePlaceholder*{__ROBEXT_MAIN_CONTENT__}{% \noexpand\makeatletter% % Make sure the counter exists: \noexpand\ifcsname c@#1\noexpand\endcsname\noexpand\else\noexpand\newcounter{#1}\noexpand\fi% \noexpand\setcounter{#1}{\the\value{#1}}% \noexpand\makeatother }% }% },% },% % Useful when we want to force the value of a forwarded counter. Useful for arXiv, e.g. % if the page number is not the good one. forward counter force value/.style 2 args={% run command if externalization={% \expanded{% \noexpand\robExtAddBeforePlaceholder*{__ROBEXT_MAIN_CONTENT__}{% \noexpand\makeatletter% % Make sure the counter exists: \noexpand\ifcsname c@#1\noexpand\endcsname\noexpand\else\noexpand\newcounter{#1}\noexpand\fi% \noexpand\setcounter{#1}{#2}% \noexpand\makeatother }% }% },% },% forward color/.style={ run command if externalization={% \extractcolorspecs{#1}{\zx@tmp@model}{\zx@tmp@cmd}% \expanded{% \noexpand\robExtAddBeforePlaceholder*{__ROBEXT_MAIN_CONTENT__}{% \noexpand\definecolor{#1}{\zx@tmp@model}{\zx@tmp@cmd}% }% }% },% },% %% This will try to automatically forward some macros. For this, you must first define %% auto forward only macros/.code={\robExtAutoForward}, auto forward/.code={\robExtAutoForward\pgfkeysalso{auto forward words}}, auto forward color/.style 2 args={% add to preset={#1}{% if matches word={#2}{forward color=#2}, }, }, load auto forward macro config/.code={\robExtLoadAutoForwardMacroConfig{#1}}, %% Auto forward words auto forward words namespace/.code={% \ifdefined\robExt@autoforward@enabled\else% \pgfkeysalso{command if externalization/.append code={% \ifdefined\robExt@autoforward@enabled% \expanded{\noexpand\robExtAutoForwardWords{\robExt@autoforward@enabled}}% \fi% }% }% \fi% \def\robExt@autoforward@enabled{#1}% },%%% todo: finish auto forward words/.style={auto forward words namespace={}}, %% This will if matches regex/.code 2 args={\robExtIfMatchesRegex{#1}{#2}}, if matches/.code 2 args={\robExtIfMatchesString{#1}{#2}}, if matches word/.code 2 args={% \robExtRegisterWord{}{#1}{#2}% % If ran inside a preset, we want to enable it, otherwise we enable it on the latex preset \ifdefined\robExtCurrentlyDefiningPreset% \pgfkeysalso{auto forward words}% \else% \pgfkeysalso{/robExt/latex/.append style={auto forward words}}% \fi% }, % more efficient since we can register the words before the preset, but make sure to call `auto forward words namespace`. % You can use an empty namespace. register word with namespace/.code n args={3}{% \robExtRegisterWord{#1}{#2}{#3}% }, register word/.style 2 args={% register word with namespace={}{#1}{#2},% }, %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Run code before/after inclusion %%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% todo: make sure that commands can be added instead of replaced execute before each externalization/.code={\def\robExtExecuteBefore{#1}}, execute after each externalization/.code={\def\robExtExecuteAfter{#1}}, %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Get the name of the produced file for later use %%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Here, we provide a way to put the prefixed name into a new global macro %%% Use like 'name output=VideoA'. This creates a few macros like: %%% \blenderpointNamedOutputFilenameVideoA containing thehashthatisusedforthename %%% \blenderpointNamedOutputPrVideoA containing thehashthatisusedforthename %% See \robExtGetNamedOutputFilename to get them with \robExtGetNamedOutputFullPath name output/.code={% \def\robExtExecuteNamedOutput{% \expandafter\xdef\csname #1\endcsname{\robExtPrefixFilename\robExtFinalHash}% \expandafter\xdef\csname #1InCache\endcsname{\robExtAddCachePathAndName{\robExtFinalHash}}% }% }, %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Compile in parallel automatically %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% compile in parallel/.style={compile in parallel after=#1}, compile in parallel/.default=0, compile in parallel after/.code={% \gdef\robExt@compile@parallel@after{#1}% }, compile in parallel after/.default=0, disable compile in parallel/.code={\let\robExt@compile@parallel@after\undefined}, if windows/.code={% \robExtIfWindowsTF{\pgfkeysalso{zx@tmp@key/.estyle={\unexpanded{#1}},zx@tmp@key}}{}% }, if unix/.code={% \robExtIfWindowsTF{}{\pgfkeysalso{zx@tmp@key/.estyle={\unexpanded{#1}},zx@tmp@key}}% }, compile in parallel command/.store in=\robExt@compile@parallel@command, % Different backends to compile in parallel compile in parallel with gnu parallel/.style={ compile in parallel command={parallel --jobs #1 :::: '\jobnameNoQuotes-\robExtAddPrefixName{compile-missing-figures.sh}'}, }, compile in parallel with gnu parallel/.default={200\%}, compile in parallel with xargs/.style={ % Use " instead of ' as Windows does not consider ' as a valid surrounding for strings compile in parallel command={xargs -t -I "{}" -P #1 \robExtParallelShell\space "{}" < "\jobnameNoQuotes-\robExtAddPrefixName{compile-missing-figures.sh}"}, }, % With 0 it spawns as many shells at possible, i.e. it compiles all 200 pictures at the same time, making the % system a laggy, and it is actually *slower* (2:05mn vs 1:15mn) than when using 16 threads. compile in parallel with xargs/.default={16}, % By default, we compile with xargs and 16 processes: compile in parallel with xargs, } \ExplSyntaxOn \int_new:N \g__robExt_number_figures_just_compiled_or_to_compile \int_set:Nn \g__robExt_number_figures_just_compiled_or_to_compile {0} % Will hold the list of input files to compile in parallel. This is needed to % check at the end to find the processes that failed to build. \seq_new:N \g__robExt_files_compiled_in_parallel \AfterLastShipout{ % Check if we want to run stuff in parallel \ifdefined\robExt@compile@parallel@after % Check if we must compile \ifdefined\robExt@compile@parallel@must@compile % Check if we can compile \bool_if:nTF { \sys_if_shell_unrestricted_p: || \cs_if_exist_p:N \robExtForceCompilation} { % Check if a command was provided \ifdefined\robExtParallelShell\else \sys_if_platform_windows:TF { \def\robExtParallelShell{cmd~/C} }{ \def\robExtParallelShell{sh~-c} } \fi % We close the file or reading it is empty \iow_close:N \g__robExt_write_manually_compile_all_missing_figures_iow \msg_warning:nnx{robExt}{rerun because parallel}{\robExt@compile@parallel@command} \message{\robExt@compile@parallel@command} % Note that we do not save this into a file because it might help to see in real time where we are in % the compilation process. This also means that we get poorer error message if this specific % command fails in texstudio since it only prints the log file and not the whole terminal... % Since this should anyway be quite rare (user has not xargs installed basically), this should be fine. \sys_shell_now:x {\robExtPrefixAllCompilationCommands\robExt@compile@parallel@command} % We check if some files failed to compile \seq_map_inline:Nn \g__robExt_files_compiled_in_parallel { \file_if_exist:xTF{#1.pdf}{}{ \file_if_exist:nTF {#1-compilation.log} { % We print the error twice as some editor might display only the first error while in others % it might be easier to see the last error. For instance, in emacs if we do not do that % then it first prints the error message in a file in the cache (but it forgets the % robustExternalize prefix) % But first we get the lines containing the word error: \let\robExtPrintWholeFile\undefined \cs_if_exist:cTF {g__robExt_print_whole_file_#1:} {\def\robExtPrintWholeFile{}} {} \robExt_get_errors_from_file:n {#1-compilation.log}% % We print the message before the log because otherwise emacs is confused and tries to open the % auxiliary cached file, and I don't want that. \ifdefined\robExtHideFirstErrorMessage\else \msg_error:nnxxxx{robExt}{missing compiled pdf parallel with log}{#1}{\use:c {g__robExt_lines_error_#1:}}{\use:c {g__robExt_compilation_command_#1:}}{below~(you~might~need~to~press~ENTER~to~go~to~the~next~error)} \fi \message{--------~We~print~now~the~full~log~--------^^J} \robExt_write_file_in_log:n {#1-compilation.log}% \message{--------~End~of~the~full~log~--------^^J} \msg_error:nnxxxx{robExt}{missing compiled pdf parallel with log}{#1}{\use:c {g__robExt_lines_error_#1:}}{\use:c {g__robExt_compilation_command_#1:}}{above} \let\robExtPrintWholeFile\undefined }{ \msg_error:nnxxx{robExt}{missing compiled pdf parallel}{#1}{\use:c {g__robExt_lines_error_#1:}}{\use:c {g__robExt_compilation_command_#1:}} } } } } { \ifdefined\robExtFallbackManualMode \msg_warning:nn{robExt}{enabled parallel no shell escape} \else \msg_error:nn{robExt}{need shell escape parallel} \fi } \fi \fi % \robExtBackupSource moves the source .tex into .tex-backup, needed for the arXiv website that removes the % .pdf if there is a .tex file. % We need to run it at the very end, as if we move it right after a cacheMe, we cannot refer anymore to the % input file of this block later. \iow_close:N \g__robExt_write_list_all_figures_iow \ifdefined\robExtEnableBackupSource% \ior_open:Nn \g__robExt_read_ior {\jobnameNoQuotes-\robExtAddPrefixName{all-figures.txt}}% \ior_str_map_inline:Nn \g__robExt_read_ior {% \robExtBackupSource{#1}% }% \ior_close:N \g__robExt_read_ior \fi% } \ExplSyntaxOff % Not really made for the end user % It assumes that __ROBEXT_COMPILATION_COMMAND__ and __ROBEXT_TEMPLATE__ is set \NewDocumentCommand{\robExtEvaluateCompileAndInclude}{}{% \ifdefined\robExtDisableExternalization% \pgfkeys{% /robExt/.cd, command if no externalization, }% \else% \pgfkeys{% /robExt/.cd, command if externalization, }% \ifdefined\robExtExecuteBefore\robExtExecuteBefore\fi% \robExtWriteFile{}% \robExtCompileFile{}% \robExtIncludeFile{}% \ifdefined\robExtExecuteNamedOutput\robExtExecuteNamedOutput\fi% \ifdefined\robExtExecuteAfter\robExtExecuteAfter\fi% \fi% \robExtDebugInfo{Finished to include the file.}% } %% #1: Arguments, #2: content to externalize \NewDocumentCommand{\robExtCacheMe}{O{}+m}{% {% Group %% We store the input in a non-string element for efficiently implementing "auto forward" \edef\robExtUserInputCacheMe{\unexpanded{#2}}% \unexpanded is needed if the macro contains a #1 \pgfkeys{% /robExt/.cd,% %% This is needed notably if the cached elements are nested, like the include command uses itself a tikz %% picture etc cached via \cacheTikz... It it hard to reset everything efficiently (like we might not %% want to reset all compilation commands etc), so you can add here stuff that might need to be restored %% later. reset, set placeholder={__ROBEXT_MAIN_CONTENT_ORIG__}{#2},% default style,% #1, }% \robExtEvaluateCompileAndInclude% }% } \let\cacheMe\robExtCacheMe \ExplSyntaxOn %% #1: Arguments, #2: content to externalize \str_new:N \__robExt_tmp_contain_code_str \tl_new:N \__robExt_tmp_contain_code_tl \NewDocumentCommand{\robExtCacheMeCode}{O{}+v}{% {% Group %% We store the input in a non-string element for efficiently implementing "auto forward" \edef\robExtUserInputCacheMe{\unexpanded{#2}}% \tl_set:Nn \__robExt_tmp_contain_code_tl {#2} \tl_replace_all:Nen \__robExt_tmp_contain_code_tl {\char_generate:nn{13}{12}} {^^J} \str_set:Ne \__robExt_tmp_contain_code_str {\tl_to_str:e {\__robExt_tmp_contain_code_tl}} %\str_show:N \__robExt_tmp_contain_code_str %\tl_replace_all:Nnn \__robExt_tmp_contain_code_str {^^M} {b} %\str_show:N \__robExt_tmp_contain_code_str \pgfkeys{% /robExt/.cd,% %% This is needed notably if the cached elements are nested, like the include command uses itself a tikz %% picture etc cached via \cacheTikz... It it hard to reset everything efficiently (like we might not %% want to reset all compilation commands etc), so you can add here stuff that might need to be restored %% later. reset, /utils/exec={\robExtPlaceholderFromString{__ROBEXT_MAIN_CONTENT_ORIG__}{\__robExt_tmp_contain_code_str}}, default~style,% defaultPlaceholderFromCodeStyle, #1, }% \robExtEvaluateCompileAndInclude% }% } \let\cacheMeCode\robExtCacheMeCode \ExplSyntaxOff %% #1: Arguments, #2: content to externalize \NewDocumentEnvironment{RobExtCacheMe}{m+b}{% \robExtCacheMe[#1]{#2}% }{} \let\CacheMe\RobExtCacheMe \let\endCacheMe\endRobExtCacheMe \NewDocumentEnvironment{RobExtCacheMeCode}{m}{% \RobExtPlaceholderFromCode{__ROBEXT_MAIN_CONTENT_ORIG__}% }{% \endRobExtPlaceholderFromCode% \pgfkeys{% /robExt/.cd,% #1,% }% \robExtEvaluateCompileAndInclude% } \let\CacheMeCode\RobExtCacheMeCode \let\endCacheMeCode\endRobExtCacheMeCode \NewDocumentEnvironment{RobExtCacheMeNoContent}{+b}{% \robExtCacheMe[#1]{}% }{} \let\CacheMeNoContent\RobExtCacheMeNoContent \let\endCacheMeNoContent\endRobExtCacheMeNoContent %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%% Forward a macro %%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% In this section, we provide commands to forward a macro, like %% \def\myMacro{stuff} %% \cacheMe %% Note that this will only be forwarded if externalization is enabled (otherwise the macro already exists) %% I published this code in this answer, thanks to egreg for some pointers %% https://tex.stackexchange.com/questions/697120/how-to-extract-the-definition-of-a-macro-to-write-in-a-file-for-instance \ExplSyntaxOn \cs_new:Nn \__robExt_getCommandDefinitionFromXparse:N { \str_clear_new:N \l__robExt_definition_str \GetDocumentCommandArgSpec { #1 } \str_set:Nx \l__robExt_definition_str { \c_backslash_str DeclareDocumentCommand \c_left_brace_str \c_backslash_str \cs_to_str:N #1 \c_right_brace_str \c_left_brace_str \ArgumentSpecification \c_right_brace_str \c_left_brace_str \cs_replacement_spec:c { \cs_to_str:N #1 ~ code } \c_right_brace_str }% } \cs_new:Nn \__robExt_getCommandDefinitionFromDef:N { \str_clear_new:N \l__robExt_definition_str \str_set:Nx \l__robExt_definition_str { \c_backslash_str def \c_backslash_str \cs_to_str:N #1 \cs_argument_spec:c { \cs_to_str:N #1 } \c_left_brace_str \cs_replacement_spec:c { \cs_to_str:N #1 } \c_right_brace_str }% } \def\robExt@extractDefaultNewcommand#1#2#3#4{ #4% }% \cs_new:Nn \__robExt_getCommandDefinitionFromNewcommand:N { \str_clear_new:N \l__robExt_definition_str \str_clear_new:N \l__robExt_tmp %% \l__robExt_tmp_str will look like [#1]#2#3#4#5#6#7: \str_set:Nx \l__robExt_tmp_str {\cs_argument_spec:c { \c_backslash_str \cs_to_str:N #1 }}% %% We remove the brackets: \str_replace_all:Nnn \l__robExt_tmp_str {[} {} \str_replace_all:Nnn \l__robExt_tmp_str {]} {} \str_set:Nx \l__robExt_definition_str { %% Make sure the command does not exist \c_backslash_str providecommand \c_backslash_str \cs_to_str:N #1 \c_left_brace_str \c_right_brace_str %% this line will be like "\newdocumentcommand{\mymacro}" \c_backslash_str renewcommand \c_left_brace_str \c_backslash_str \cs_to_str:N #1 \c_right_brace_str %% It must have at least one argument, since elements without optional arguments are turned into \def %% Moreover, macros can't have more than 1 argument, so it will be easier to parse, we just need the last digit [\str_range:Nnn \l__robExt_tmp_str {-1} {-1}] %% <- this is the number of mandatory arguments [\expandafter \robExt@extractDefaultNewcommand #1 ] %% <- this is the value of the default argument \c_left_brace_str \cs_replacement_spec:c { \c_backslash_str \cs_to_str:N #1 } \c_right_brace_str }% } % \robExtGetCommandDefinitionInMacro{\myMacro} will populate % \l__robExt_definition_str and \robExtDefinitionCommand % with a string to execute to write it properly. \NewDocumentCommand{\robExtGetCommandDefinitionInMacro}{m}{ \cs_if_exist:NTF #1 { \cs_if_exist:cTF {\cs_to_str:N #1 ~ code}{ % xparse-based definition \__robExt_getCommandDefinitionFromXparse:N #1% } { \cs_if_exist:cTF {\c_backslash_str \cs_to_str:N #1}{ % \newcommand-based definition \__robExt_getCommandDefinitionFromNewcommand:N #1 }{ % \def-based definition \__robExt_getCommandDefinitionFromDef:N #1 } } }{ \msg_error:nn{robExt}{The~macro~#1~does~not~exist} } \let\robExtDefinitionCommand\l__robExt_definition_str } \NewDocumentCommand{\robExtGetCommandDefinition}{m}{% \robExtGetCommandDefinitionInMacro{#1}% \l__robExt_definition_str% } \ExplSyntaxOff %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%% Default presets %%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% We create here a few presets and placeholders useful later %%%%%% %%%%%% Group "special characters" %%%%%% %%%% Generic placeholders, practical to escape stuff \robExtClearGroupPlaceholders{main} \ExplSyntaxOn \robExtPlaceholderFromString{__ROBEXT_LEFT_BRACE__}{\c_left_brace_str} \robExtPlaceholderFromString{__ROBEXT_RIGHT_BRACE__}{\c_right_brace_str} \robExtPlaceholderFromString{__ROBEXT_BACKSLASH__}{\c_backslash_str} \robExtPlaceholderFromString{__ROBEXT_HASH__}{\c_hash_str} \robExtPlaceholderFromString{__ROBEXT_PERCENT__}{\c_percent_str} \robExtPlaceholderFromString{__ROBEXT_UNDERSCORE__}{\c_underscore_str} \ExplSyntaxOff \robExtCopyGroupPlaceholders{special characters}{main} \robExtRegisterGroupPlaceholders{special characters} %%%%%% %%%%%% Group "latex" %%%%%% \robExtClearGroupPlaceholders{main} \begin{RobExtPlaceholderFromCode}{__ROBEXT_LATEX__} \documentclass[__ROBEXT_LATEX_OPTIONS__]{__ROBEXT_LATEX_DOCUMENT_CLASS__} __ROBEXT_LATEX_PREAMBLE__ % most packages must be loaded before hyperref % so we typically want to load hyperref here __ROBEXT_LATEX_PREAMBLE_HYPERREF__ % some packages must be loaded after hyperref __ROBEXT_LATEX_PREAMBLE_AFTER_HYPERREF__ \begin{document}% __ROBEXT_LATEX_MAIN_CONTENT_WRAPPED__ \end{document} \end{RobExtPlaceholderFromCode} \robExtSetPlaceholder{__ROBEXT_LATEX_OPTIONS__}{} \robExtSetPlaceholder{__ROBEXT_LATEX_DOCUMENT_CLASS__}{standalone} \robExtSetPlaceholder{__ROBEXT_LATEX_PREAMBLE__}{} \robExtSetPlaceholder{__ROBEXT_LATEX_PREAMBLE_HYPERREF__}{} \robExtSetPlaceholder{__ROBEXT_LATEX_PREAMBLE_AFTER_HYPERREF__}{} \robExtSetPlaceholder{__ROBEXT_LATEX_TRIM_LENGTH__}{30cm} \begin{RobExtPlaceholderFromCode}{__ROBEXT_LATEX_MAIN_CONTENT_WRAPPED__} __ROBEXT_LATEX_CREATE_OUT_FILE__% \newsavebox\boxRobExt% \begin{lrbox}{\boxRobExt}% __ROBEXT_MAIN_CONTENT__% \end{lrbox}% \usebox{\boxRobExt}% __ROBEXT_LATEX_WRITE_DEPTH_TO_OUT_FILE__% \end{RobExtPlaceholderFromCode} \begin{RobExtPlaceholderFromCode}{__ROBEXT_LATEX_CREATE_OUT_FILE__} %% We save the height/depth of the content by using a savebox: \newwrite\writeRobExt% \immediate\openout\writeRobExt=\jobname-out.tex% \end{RobExtPlaceholderFromCode} \begin{RobExtPlaceholderFromCode}{__ROBEXT_LATEX_WRITE_DEPTH_TO_OUT_FILE__} \immediate\write\writeRobExt{% \string\def\string\robExtWidth{\the\wd\boxRobExt}% \string\def\string\robExtHeight{\the\ht\boxRobExt}% \string\def\string\robExtDepth{\the\dp\boxRobExt}% }% \end{RobExtPlaceholderFromCode} %% Compilation commands \robExtSetPlaceholder{__ROBEXT_LATEX_COMPILATION_COMMAND__}{__ROBEXT_LATEX_ENGINE__ __ROBEXT_LATEX_COMPILATION_COMMAND_OPTIONS__ "__ROBEXT_SOURCE_FILE__"} \robExtSetPlaceholder{__ROBEXT_LATEX_COMPILATION_COMMAND_OPTIONS__}{-halt-on-error} \robExtSetPlaceholder{__ROBEXT_LATEX_ENGINE__}{pdflatex} \robExtConfigure{ %%%% Allow users to specify which command to try (or try first) when replacing placeholders. Order matters. %%%% it looks ugly, but it is mostly for performance improvements. first placeholders latex/.style={ first placeholders in compilation command={__ROBEXT_LATEX_COMPILATION_COMMAND__,__ROBEXT_LATEX_ENGINE__,__ROBEXT_LATEX_COMPILATION_COMMAND_OPTIONS__}, first placeholders in template={__ROBEXT_LATEX__,__ROBEXT_LATEX_OPTIONS__,__ROBEXT_LATEX_DOCUMENT_CLASS__,__ROBEXT_LATEX_PREAMBLE__,__ROBEXT_LATEX_PREAMBLE_HYPERREF__,__ROBEXT_LATEX_PREAMBLE_AFTER_HYPERREF__,__ROBEXT_LATEX_MAIN_CONTENT_WRAPPED__,__ROBEXT_LATEX_TRIM_LENGTH__,__ROBEXT_LATEX_CREATE_OUT_FILE__,__ROBEXT_LATEX_WRITE_DEPTH_TO_OUT_FILE__,__ROBEXT_MAIN_CONTENT__,__ROBEXT_MAIN_CONTENT_ORIG__}, }, only placeholders latex/.style={ only placeholders in compilation command={__ROBEXT_LATEX_COMPILATION_COMMAND__,__ROBEXT_LATEX_ENGINE__,__ROBEXT_LATEX_COMPILATION_COMMAND_OPTIONS__}, only placeholders in template={__ROBEXT_LATEX__,__ROBEXT_LATEX_OPTIONS__,__ROBEXT_LATEX_DOCUMENT_CLASS__,__ROBEXT_LATEX_PREAMBLE__,__ROBEXT_LATEX_PREAMBLE_HYPERREF__,__ROBEXT_LATEX_PREAMBLE_AFTER_HYPERREF__,__ROBEXT_LATEX_MAIN_CONTENT_WRAPPED__,__ROBEXT_LATEX_TRIM_LENGTH__,__ROBEXT_LATEX_CREATE_OUT_FILE__,__ROBEXT_LATEX_WRITE_DEPTH_TO_OUT_FILE__,__ROBEXT_MAIN_CONTENT__,__ROBEXT_MAIN_CONTENT_ORIG__}, }, % some useful presets latex/.style={ enable placeholders, % This way it appears before orig in the list: more efficient import placeholders from group={latex}, %/utils/exec={\message{I am in the latex style!!!!!!}}, set placeholder={__ROBEXT_MAIN_CONTENT__}{__ROBEXT_MAIN_CONTENT_ORIG__}, import placeholder={__ROBEXT_MAIN_CONTENT_ORIG__}, first placeholders latex, set template={__ROBEXT_LATEX__}, set compilation command={__ROBEXT_LATEX_COMPILATION_COMMAND__}, %% Configure the latex compilation engine add to compilation command options/.style={ add to placeholder={__ROBEXT_LATEX_COMPILATION_COMMAND_OPTIONS__}{#1}, }, use latexmk/.style={ set placeholder={__ROBEXT_LATEX_ENGINE__}{latexmk}, }, use lualatex/.style={ set placeholder={__ROBEXT_LATEX_ENGINE__}{lualatex}, }, use xelatex/.style={ set placeholder={__ROBEXT_LATEX_ENGINE__}{xelatex}, }, set latex options/.style={ set placeholder={__ROBEXT_LATEX_OPTIONS__}{##1}, }, add to latex options/.style={ add to placeholder no space={__ROBEXT_LATEX_OPTIONS__}{,##1}, }, set documentclass/.style={ set placeholder={__ROBEXT_LATEX_DOCUMENT_CLASS__}{##1}, }, set preamble/.style={ set placeholder={__ROBEXT_LATEX_PREAMBLE__}{##1}, }, add to preamble/.style={ add to placeholder={__ROBEXT_LATEX_PREAMBLE__}{##1}, }, add before main content/.style={ add before placeholder no space={__ROBEXT_MAIN_CONTENT__}{##1}, }, add before preamble/.style={ add before placeholder={__ROBEXT_LATEX_PREAMBLE__}{##1}, }, set preamble hyperref/.style={ set placeholder={__ROBEXT_LATEX_PREAMBLE_HYPERREF__}{##1}, }, add to preamble hyperref/.style={ add to placeholder={__ROBEXT_LATEX_PREAMBLE_HYPERREF__}{##1}, }, set preamble after hyperref/.style={ set placeholder={__ROBEXT_LATEX_PREAMBLE_AFTER_HYPERREF__}{##1}, }, add to preamble after hyperref/.style={ add to placeholder={__ROBEXT_LATEX_PREAMBLE_AFTER_HYPERREF__}{##1}, }, do not wrap code/.style={ set placeholder={__ROBEXT_LATEX_MAIN_CONTENT_WRAPPED__}{__ROBEXT_MAIN_CONTENT__}, }, add to includegraphics options={trim=__ROBEXT_LATEX_TRIM_LENGTH__ __ROBEXT_LATEX_TRIM_LENGTH__ __ROBEXT_LATEX_TRIM_LENGTH__ __ROBEXT_LATEX_TRIM_LENGTH__}, add to latex options={margin=__ROBEXT_LATEX_TRIM_LENGTH__}, do not add margins/.style={ set placeholder={__ROBEXT_LATEX_TRIM_LENGTH__}{0cm} }, }, % U tikz/.style={ latex, add to preamble={\usepackage{tikz}}, }, tikzpicture/.style={ tikz, set placeholder no import={__ROBEXT_MAIN_CONTENT__}{\begin{tikzpicture}__ROBEXT_MAIN_CONTENT_ORIG__\end{tikzpicture}}, }, } \robExtCopyGroupPlaceholders{latex}{main} \robExtRegisterGroupPlaceholders{latex} %%%%%% %%%%%% Group "python" %%%%%% \robExtClearGroupPlaceholders{main} \begin{RobExtPlaceholderFromCode}{__ROBEXT_PYTHON__} __ROBEXT_PYTHON_IMPORT__ __ROBEXT_PYTHON_MAIN_CONTENT_WRAPPED__ \end{RobExtPlaceholderFromCode} \robExtSetPlaceholder{__ROBEXT_PYTHON_IMPORT__}{} \begin{RobExtPlaceholderFromCode}{__ROBEXT_PYTHON_MAIN_CONTENT_WRAPPED__} # This file will be loaded in latex. Useful to pass data to the main document f_out_write = open("__ROBEXT_OUTPUT_PREFIX__-out.tex", "w") import os import sys def write_to_out(text): """Write to the -out.tex file that is loaded by default""" f_out_write.write(text) def parse_args(): args = {} if len(sys.argv) % 2 == 0: print("Error: the number of arguments must be even, as tuples of name and value") exit(1) for i in range(0,len(sys.argv)-1,2): args[sys.argv[i+1]] = sys.argv[i+2] return args def get_cache_folder(): ''' Path of the cache folder. Warning: this works only when the python script is located in this cache folder (that should be true when it's called from LaTeX) ''' return os.path.abspath(os.path.dirname(sys.argv[0])) def get_file_base(): ''' Outputs the base of the files (i.e. something like robExt-somehash, without any extension) ''' return os.path.splitext(os.path.basename(sys.argv[0]))[0] # __file__ does not work as it refers to the library def get_current_script(): ''' Outputs the path of the current script ''' return os.path.abspath(sys.argv[0]) # __file__ does not work as it refers to the library def get_filename_from_extension(extension): ''' If you want to create a file with extension 'extension' (with the appropriate base name), this command is for you. For instance get_filename_from_extension(".mp4") would return something like robExt-somehash.mp4 the extension can also be like get_filename_from_extension("-out.tex") etc. ''' return os.path.join(get_cache_folder(), get_file_base() + extension) def get_verbatim_output(): '''Returns the path to -out.txt that is read by verbatim output''' return get_filename_from_extension("-out.txt") def get_pdf_output(): '''Returns the path to -out.txt that is read by verbatim output''' return get_filename_from_extension(".pdf") def finished_with_no_error(): ''' Call this at the end of your script. This creates the path of the final pdf file that should be created (otherwise robust-externalize will think that the compilation failed) ''' if not os.path.exists(get_filename_from_extension(".pdf")): # we create a nearly empty pdf (not empty or arxiv will remove it) with open(get_filename_from_extension(".pdf"), 'w') as f: f.write("ok") ### Starting main content __ROBEXT_MAIN_CONTENT__ ### Ending main content __ROBEXT_PYTHON_FINISHED_WITH_NO_ERROR__ f_out_write.close() \end{RobExtPlaceholderFromCode} % It is annoying to manually call finished_with_no_error(), but it is handy to be able to disable it. \begin{RobExtPlaceholderFromCode}{__ROBEXT_PYTHON_FINISHED_WITH_NO_ERROR__} finished_with_no_error() \end{RobExtPlaceholderFromCode} %% On windows, python3 does not exist, and python points to python3. On linux, it seems to depend, at least on %% my system it points to python3 as well. \robExtSetPlaceholder{__ROBEXT_PYTHON_EXEC__}{python} \robExtConfigure{ python/.style={ enable placeholders, print whole file in error message, import placeholders={__ROBEXT_PYTHON__,__ROBEXT_PYTHON_IMPORT__,__ROBEXT_PYTHON_MAIN_CONTENT_WRAPPED__,__ROBEXT_PYTHON_FINISHED_WITH_NO_ERROR__,__ROBEXT_PYTHON_EXEC__}, set compilation command={__ROBEXT_PYTHON_EXEC__ "__ROBEXT_SOURCE_FILE__"}, set template={__ROBEXT_PYTHON__}, print verbatim if no externalization, force python3/.style={ set placeholder={__ROBEXT_PYTHON_EXEC__}{python3} }, add import/.style={ add to placeholder no space={__ROBEXT_PYTHON_IMPORT__}{##1^^J}, }, remove leading spaces if not disabled, } } \robExtCopyGroupPlaceholders{python}{main} \robExtRegisterGroupPlaceholders{python} %%%%%% %%%%%% Group "python print code result" %%%%%% \robExtClearGroupPlaceholders{main} %% A style to print both the code and the result: \begin{RobExtPlaceholderFromCode}{__ROBEXT_PYTHON_PRINT_CODE_RESULT_TEMPLATE_BEFORE__} # File where print("bla") should be redirected # get_filename_from_extension("-foo.txt") will give you the path of the file # in the cache that looks like robExt-somehash-foo.txt print_file = open(get_filename_from_extension("-print.txt"), "w") sys.stdout = print_file # This code will read the current code, and extract the lines between # that starts with "### CODESTARTSHERE" and "### CODESTOPSHERE", and will write # it into the *-code.text (we do not want to print all these functions in # the final code) with open(get_filename_from_extension("-code.txt"), "w") as f: # The current script has extension .tex with open(get_current_script(), "r") as script: should_write = False for line in script: if line.startswith("### CODESTARTSHERE"): should_write = True elif line.startswith("### CODESTOPSHERE"): should_write = False elif "HIDEME" in line: pass else: if should_write: f.write(line) ### CODESTARTSHERE \end{RobExtPlaceholderFromCode} \begin{RobExtPlaceholderFromCode}{__ROBEXT_PYTHON_PRINT_CODE_RESULT_TEMPLATE_AFTER__} ### CODESTOPSHERE print_file.close() \end{RobExtPlaceholderFromCode} \robExtSetPlaceholder{__ROBEXT_PYTHON_TCOLORBOX_PROPS__}{colback=red!5!white,colframe=red!75!black} \robExtSetPlaceholder{__ROBEXT_PYTHON_CODE_MESSAGE__}{} \robExtSetPlaceholder{__ROBEXT_PYTHON_RESULT_MESSAGE__}{Output:} \robExtSetPlaceholder{__ROBEXT_PYTHON_LSTINPUT_STYLE__}{frame=single, breakindent=.5\textwidth, frame=single, breaklines=true, style=mypython} \robExtConfigure{ python print code and result/.style={ python, import placeholders={__ROBEXT_PYTHON_PRINT_CODE_RESULT_TEMPLATE_BEFORE__,__ROBEXT_PYTHON_PRINT_CODE_RESULT_TEMPLATE_AFTER__,__ROBEXT_PYTHON_TCOLORBOX_PROPS__,__ROBEXT_PYTHON_CODE_MESSAGE__,__ROBEXT_PYTHON_RESULT_MESSAGE__,__ROBEXT_PYTHON_LSTINPUT_STYLE__}, add before placeholder no space={__ROBEXT_MAIN_CONTENT__}{__ROBEXT_PYTHON_PRINT_CODE_RESULT_TEMPLATE_BEFORE__}, add to placeholder no space={__ROBEXT_MAIN_CONTENT__}{__ROBEXT_PYTHON_PRINT_CODE_RESULT_TEMPLATE_AFTER__}, set title/.style={ set placeholder={__MY_TITLE__}{##1}, }, set title={Python code}, custom include command={ % Useful to replace __MY_TITLE__: \evalPlaceholder{ \begin{tcolorbox}[title=__MY_TITLE__,__ROBEXT_PYTHON_TCOLORBOX_PROPS__] __ROBEXT_PYTHON_CODE_MESSAGE__% \lstinputlisting[__ROBEXT_PYTHON_LSTINPUT_STYLE__]{\robExtAddCachePathAndName{\robExtFinalHash-code.txt}} __ROBEXT_PYTHON_RESULT_MESSAGE__% \verbatiminput{\robExtAddCachePathAndName{\robExtFinalHash-print.txt}} \end{tcolorbox} } }, }, } \robExtCopyGroupPlaceholders{python print code result}{main} \robExtRegisterGroupPlaceholders{python print code result} %%%%%% %%%%%% Group "python exec" %%%%%% \robExtClearGroupPlaceholders{main} % if we set res = XXX or res[""] = XXX it will print it by default, if we do instead res[42] = XXX % it will not load it. To make this easy to program, we create an element for the empty string, % this way we can always \res{} in the include command. \begin{RobExtPlaceholderFromCode}[remove spaces until=>]{__ROBEXT_PYTHON_EXEC_TEMPLATE__} > def toMacro(x): > if hasattr(x, '__toMacro'): > return x.__toMacro() > else: > return str(x) > > __ROBEXT_PYTHON_EXEC_LIBRARY_CHANGES__ > __ROBEXT_PYTHON_EXEC_CUSTOM_TO_MACRO_DEF__ > # We create a special class extending dict to check if the user just did res[42] = xxx > # or res = foo, as in that case we will just print the result directly. > class DictToExportToMacros(dict): > pass > res = DictToExportToMacros() > res[""] = "" > __ROBEXT_PYTHON_EXEC_RES_EQUALITY____ROBEXT_MAIN_CONTENT_ORIG__ > if not isinstance(res, DictToExportToMacros): > write_to_out(r"\gdef\robExtResMacro{" + toMacro(res) + r"}") > else: > for k in res: > # We create a macro with csname to allow numbers in the name etc and parallel compilation > write_to_out(r"\expandafter\gdef\csname robExtResMacro" + str(k) + r"\endcsname{" + toMacro(res[k]) + r"}") \end{RobExtPlaceholderFromCode} \robExtSetPlaceholder{__ROBEXT_PYTHON_EXEC_RES_EQUALITY__}{} \robExtSetPlaceholder{__ROBEXT_PYTHON_EXEC_LIBRARY_CHANGES__}{} \begin{RobExtPlaceholderFromCode}{__ROBEXT_PYTHON_EXEC_CUSTOM_TO_MACRO_DEF__} \end{RobExtPlaceholderFromCode} % Some macro always expect a number to work, and must be expandable \NewDocumentCommand{\robExtResForceNumber}{m}{% \ifcsname robExtResMacro#1\endcsname% \csname robExtResMacro#1\endcsname% \else% 404% \fi% } \def\robExtNoResult#1{% \textbf{??}% } \NewDocumentCommand{\robExtRes}{m}{% \ifcsname robExtResMacro#1\endcsname% \csname robExtResMacro#1\endcsname% \else% \msg_warning:nnx{robExt}{warning res not defined}{#1}%% \robExtNoRes{#1}% \fi% } \let\res\robExtRes \robExtConfigure{ new preset={python exec res}{ python, % we do that so that custom include command does not pick the result from previous runs locally /utils/exec={\let\robExtResMacro\undefined}, import placeholders={__ROBEXT_PYTHON_EXEC_CUSTOM_TO_MACRO_DEF__,__ROBEXT_PYTHON_EXEC_RES_EQUALITY__,__ROBEXT_PYTHON_EXEC_TEMPLATE__,__ROBEXT_PYTHON_EXEC_LIBRARY_CHANGES__}, custom include command={\robExtRes{}}, set placeholder={__ROBEXT_MAIN_CONTENT__}{__ROBEXT_PYTHON_EXEC_TEMPLATE__}, }, new preset={python exec}{ python exec res, set placeholder={__ROBEXT_PYTHON_EXEC_RES_EQUALITY__}{res =}, }, } \robExtCopyGroupPlaceholders{python exec}{main} \robExtRegisterGroupPlaceholders{python exec} %%%%%% %%%%%% Group "sage" and "sage res" %%%%%% \robExtClearGroupPlaceholders{main} \robExtSetPlaceholder{__ROBEXT_SAGE_EXEC__}{sage} % We need to overwrite some definitions as it adds a .sage in front automatically by default \begin{RobExtPlaceholderFromCode}[]{__ROBEXT_SAGE_EXEC_LIBRARY_CHANGES__} def get_filename_from_extension(extension): ''' If you want to create a file with extension 'extension' (with the appropriate base name), this command is for you. For instance get_filename_from_extension(".mp4") would return something like robExt-somehash.mp4 the extension can also be like get_filename_from_extension("-out.tex") etc. ''' return os.path.join(get_cache_folder(), "__ROBEXT_OUTPUT_PREFIX__" + extension) ROBEXT_PLOT_SAVE_FORMAT = "png" ROBEXT_PLOT_SAVE_OPTIONS = {} ROBEXT_PLOT_ID = 0 def __plot_graphics_to_macro(self): global ROBEXT_PLOT_SAVE_FORMAT global ROBEXT_PLOT_ID global ROBEXT_PLOT_SAVE_OPTIONS filename = get_file_base() + f"plot-{ROBEXT_PLOT_ID}.{ROBEXT_PLOT_SAVE_FORMAT}" self.save(filename, **ROBEXT_PLOT_SAVE_OPTIONS) ROBEXT_PLOT_ID += 1 return filename sage.plot.graphics.Graphics.__toMacro = __plot_graphics_to_macro \end{RobExtPlaceholderFromCode} \robExtConfigure{ new preset={sage res}{ python exec res, import placeholders={__ROBEXT_SAGE_EXEC__,__ROBEXT_SAGE_EXEC_LIBRARY_CHANGES__}, set placeholder={__ROBEXT_PYTHON_EXEC_LIBRARY_CHANGES__}{__ROBEXT_SAGE_EXEC_LIBRARY_CHANGES__}, % Sage expects the extension .sage set compilation command={__ROBEXT_SAGE_EXEC__ "__ROBEXT_OUTPUT_PREFIX__.sage"}, change source extension=sage, }, new preset={sage}{ sage res, set placeholder={__ROBEXT_PYTHON_EXEC_RES_EQUALITY__}{res =}, }, } \robExtCopyGroupPlaceholders{python exec}{main} \robExtRegisterGroupPlaceholders{python exec} %%%%%% %%%%%% Group "verbatim" %%%%%% \robExtClearGroupPlaceholders{main} %% Inspired by %% https://tex.stackexchange.com/questions/259247/rescaling-gnuplottex-to-fit-in-subfigure/259271 \ExplSyntaxOn \DeclareExpandableDocumentCommand{\robExtLenToCm}{ O{cm} m } { \dim_to_decimal_in_unit:nn { #2 } { 1 #1 } #1 } \DeclareExpandableDocumentCommand{\robExtLenToCmNoUnit}{ O{cm} m } { \dim_to_decimal_in_unit:nn { #2 } { 1 #1 } } \ExplSyntaxOff \let\lenToCm\robExtLenToCm \let\lenToCmNoUnit\robExtLenToCmNoUnit \robExtConfigure{ verbatim text/.style={ enable placeholders, set template={__ROBEXT_MAIN_CONTENT__}, custom include command={\evalPlaceholder{\verbatiminput{\robExtAddCachePathAndName{\robExtFinalHash.tex}}}}, %% Apparently this works on windows as well https://stackoverflow.com/questions/1702762/how-can-i-create-an-empty-file-at-the-command-line-in-windows %% We do not want it to be empty or arXiv will remove the file. set compilation command={echo "ok" > __ROBEXT_OUTPUT_PDF__}, }, verbatim text no include/.style={ verbatim text, custom include command={\evalPlaceholder{% \xdef\robExtPathToInput{\robExtAddCachePathAndName{\robExtFinalHash.tex}}% }% }% }, } \robExtCopyGroupPlaceholders{verbatim}{main} \robExtRegisterGroupPlaceholders{verbatim} %%%%%% %%%%%% Group "gnuplot" %%%%%% % We clean the "main" group, that we will copy later to gnuplot. \robExtClearGroupPlaceholders{main} \begin{PlaceholderFromCode}*{__GNUPLOT_TEMPLATE__} set terminal __ROBEXT_GNUPLOT_TERMINAL__ set output "__ROBEXT_GNUPLOT_OUTPUTFILE__" set loadpath "__ROBEXT_WAY_BACK__" __ROBEXT_GNUPLOT_PRELUDE__ __ROBEXT_MAIN_CONTENT__ \end{PlaceholderFromCode} \begin{PlaceholderFromCode}{__ROBEXT_GNUPLOT_PRELUDE__} \end{PlaceholderFromCode} \robExtCopyGroupPlaceholders{gnuplot}{main} \robExtRegisterGroupPlaceholders{gnuplot} \robExtConfigure{ new preset={gnuplot}{ enable placeholders, print whole file in error message, import placeholders from group={gnuplot}, set compilation command={gnuplot -c "__ROBEXT_SOURCE_FILE__" && echo "ok" > "__ROBEXT_OUTPUT_PDF__"}, set terminal/.style={ set placeholder={__ROBEXT_GNUPLOT_TERMINAL__}{#1}, }, % The extension is important. Notably, the cairolatex expects a .tex. set filename/.style={ set placeholder={__ROBEXT_GNUPLOT_OUTPUTFILE__}{__ROBEXT_OUTPUT_PREFIX__-gnuplot-#1}, set placeholder={__ROBEXT_INCLUDEGRAPHICS_FILE__}{\robExtAddCachePathAndName{\robExtFinalHash-gnuplot-#1}}, }, pdf terminal/.style={ set terminal={pdf #1}, set filename={.pdf}, }, pdf terminal/.default={}, pdf terminal, is tex output/.style={ set filename={.tex}, add cache to graphicspath, % At least in cairolatex, we first need to input a file that includegraphics an image in the cache. custom include command={\message{We will input the file \robExtAddCachePathAndName{\robExtFinalHash-gnuplot-.tex}}\input{\robExtAddCachePathAndName{\robExtFinalHash-gnuplot-.tex}}}, }, tikz terminal/.style={ set terminal={tikz #1}, is tex output, }, tikz terminal/.default={}, % Warning: you cannot define this option with the name "tikz" if you try to nest elements, for instance % if the include command calls an \input that calls a tikz picture, that is cached by \cacheTikz and therefore % call the "tikz" preset that is now changed. cairolatex terminal/.style={ set terminal={cairolatex #1}, is tex output, }, cairolatex terminal/.default={}, set template={__GNUPLOT_TEMPLATE__}, print verbatim if no externalization, }, } % Like \gpgetvar but do not produce an error if the variable does not exist yet % Fix https://github.com/leo-colisson/robust-externalize/issues/17 \ExplSyntaxOn \NewDocumentCommand{\robExtGpgetvar}{m}{% \ifcsname gp@var@#1\endcsname% \gpgetvar{#1}% \else% \msg_warning:nnx{robExt}{gpgetvar recompilation needed}{#1}% \emph{Please~recompile~to~load~ variable:~}% \texttt{\tl_to_str:n {#1}}% \fi% } % Is useful if you want a number even if there is an error https://github.com/leo-colisson/robust-externalize/issues/17#issuecomment-1862556771 % It must be expandable. \NewExpandableDocumentCommand{\robExtGpgetvarNb}{sO{404}m}{% \ifcsname gp@var@#3\endcsname% \gpgetvar{#3}% \else% We cannot print a waring or siunit will complain. \sys_if_engine_luatex:TF{\directlua{texio.write_nl('Warning:~the~variable~#3~used~in~robExtGpgetvarNb~does~not~exist~yet.~Please~recompile.')}}{}#2% \fi% } \ExplSyntaxOff %%%%%% %%%%%% Group "bash" %%%%%% \robExtClearGroupPlaceholders{main} \begin{RobExtPlaceholderFromCode}{__ROBEXT_BASH_TEMPLATE__} # Quit if there is an error set -e outputTxt="__ROBEXT_OUTPUT_PREFIX__-out.txt" outputTex="__ROBEXT_OUTPUT_PREFIX__-out.tex" outputPdf="__ROBEXT_OUTPUT_PDF__" __ROBEXT_MAIN_CONTENT__ # Create the pdf file to certify that no compilation error occured echo "ok" > "${outputPdf}" \end{RobExtPlaceholderFromCode} \robExtSetPlaceholder{__ROBEXT_BASH_SHELL__}{bash} \robExtConfigure{ bash/.style={ enable placeholders, print whole file in error message, import placeholders={__ROBEXT_BASH_TEMPLATE__,__ROBEXT_BASH_SHELL__}, set compilation command={__ROBEXT_BASH_SHELL__ "__ROBEXT_SOURCE_FILE__"}, set template={__ROBEXT_BASH_TEMPLATE__}, print verbatim if no externalization, } } \robExtCopyGroupPlaceholders{bash}{main} \robExtRegisterGroupPlaceholders{bash} %%%%%% %%%%%% Group "web image", to download images automatically %%%%%% \robExtClearGroupPlaceholders{main} % \begin{RobExtPlaceholderFromCode}{__ROBEXT_BASH_TEMPLATE__} % # Quit if there is an error % set -e % outputTxt="__ROBEXT_OUTPUT_PREFIX__-out.txt" % outputTex="__ROBEXT_OUTPUT_PREFIX__-out.tex" % outputPdf="__ROBEXT_OUTPUT_PDF__" % __ROBEXT_MAIN_CONTENT__ % # Create the pdf file to certify that no compilation error occured % echo "ok" > "${outputPdf}" % \end{RobExtPlaceholderFromCode} \robExtConfigure{ web image/.style={ enable placeholders, print whole file in error message, wget/.style={ set compilation command move if no error={wget "__ROBEXT_MAIN_CONTENT__" -O "__ROBEXT_OUTPUT_PDF__-tmp"}, }, curl/.style={ set compilation command move if no error={curl "__ROBEXT_MAIN_CONTENT__" -L -o "__ROBEXT_OUTPUT_PDF__-tmp"}, }, if unix={ wget }, if windows={ curl }, set template={}, } } \NewDocumentCommand{\robExtIncludegraphicsWeb}{D<>{}O{}m}{\robExtCacheMe[web image,set includegraphics options={#2},#1]{#3}} \let\includegraphicsWeb\robExtIncludegraphicsWeb \robExtCopyGroupPlaceholders{web image}{main} \robExtRegisterGroupPlaceholders{web image} %%%%%% %%%%%% Group "default", available in all styles %%%%%% \robExtClearGroupPlaceholders{main} % This additional level of indirection is made to allow an easier wrapping % of \begin{tikzpicture} ... \end{tikzpicture} for instance. % The original behavior (modify __ROBEXT_MAIN_CONTENT__ directly) % was not really practical as if you use both |tikz| and |\cacheCommand|, it would wrap % the environment twice. \robExtSetPlaceholder{__ROBEXT_MAIN_CONTENT__}{__ROBEXT_MAIN_CONTENT_ORIG__} \setPlaceholder{__ROBEXT_INCLUDEGRAPHICS_OPTIONS__}{} \setPlaceholder{__ROBEXT_INCLUDEGRAPHICS_FILE__}{\robExtAddCachePathAndName{\robExtFinalHash.pdf}} \robExtConfigure{ % Allow nice optimizations all placeholders have underscores, set includegraphics options/.style={ set placeholder no import={__ROBEXT_INCLUDEGRAPHICS_OPTIONS__}{#1}, }, add to includegraphics options/.style={ add to placeholder no space no import={__ROBEXT_INCLUDEGRAPHICS_OPTIONS__}{,#1}, }, set placeholder no import={__ROBEXT_VERBATIM_COMMAND__}{\verbatiminput}, % We expect the program to write in __ROBEXT_OUTPUT_PREFIX__-out.txt verbatim output/.style={ import placeholders={__ROBEXT_VERBATIM_COMMAND__}, custom include command={% \evalPlaceholder{% __ROBEXT_VERBATIM_COMMAND__{__ROBEXT_CACHE_FOLDER____ROBEXT_OUTPUT_PREFIX__-out.txt}% }% }, }, % Mostly for debugging purpose print command and source/.style={ enable manual mode, custom include command advanced={% \evalPlaceholder{% Command: (run in folder \texttt{__ROBEXT_CACHE_FOLDER__}) \robExtPrintPlaceholder{__ROBEXT_COMPILATION_COMMAND__} Dependencies: \verbatiminput{__ROBEXT_CACHE_FOLDER____ROBEXT_OUTPUT_PREFIX__.deps}% Source (in \texttt{__ROBEXT_CACHE_FOLDER____ROBEXT_OUTPUT_PREFIX__.tex}): \verbatiminput{__ROBEXT_CACHE_FOLDER____ROBEXT_OUTPUT_PREFIX__.tex}% }% }, }, debug/.style={ print command and source }, } \robExtCopyGroupPlaceholders{default}{main} % we do not reset it as it will be the default imported group \robExtRegisterGroupPlaceholders{default} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%% Automatically cache environments, tikzpicture etc %%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%% %%%%%% Parse tikz %%%%%% %% The parser is mostly taken from %% https://github.com/sasozivanovic/memoize/blob/master/memoize-tikz.tex \newtoks\robExt@temptoks \def\robExt@apptotoks#1#2{\expandafter#1\expandafter{\the#1#2}} \def\robExt@tikz{% \robExt@temptoks={}% \robExt@tikz@anim% } \def\robExt@tikz@anim{% \pgfutil@ifnextchar[{\robExt@tikz@opt}{% \pgfutil@ifnextchar:{\robExt@tikz@anim@a}{% \robExt@tikz@code}}%] }% \def\robExt@tikz@anim@a#1=#2{% \robExt@apptotoks\robExt@temptoks{#1={#2}}% \robExt@tikz@anim% }% \def\robExt@tikz@opt[#1]{% \robExt@apptotoks\robExt@temptoks{[#1]}% \robExt@tikz@code% } \def\robExt@tikz@code{% \pgfutil@ifnextchar\bgroup\robExt@tikz@braced\robExt@tikz@single% } \def\robExt@tikz@braced#1{\robExt@apptotoks\robExt@temptoks{{#1}}\robExt@tikz@done} \def\robExt@tikz@single#1;{\robExt@apptotoks\robExt@temptoks{#1;}\robExt@tikz@done} \def\robExt@tikz@done{% \edef\robExt@marshal{% \noexpand\robExtRescanHashRobust{\noexpand\robExtCacheMe[\expandonce{\robExt@tmp@orig@args}, \expandonce{\robExt@tmp@args}]{% \noexpand\tikz\the\robExt@temptoks% }% }% }% \robExt@marshal% } \NewDocumentCommand{\robExtExternalizeAllTikzpictures}{O{tikz}O{tikzpicture}O{<>}}{% \robExtDoNotExternalizeAllTikzpictures% \robExtCacheEnvironment[#3]{tikzpicture}{#2}% % Tikz has a more complicated parsing system \DeclareCommandCopy{\robExtCommandOrigtikz}{\tikz}% we save the function for later reset \robExtAddToCommandResetList{tikz}% we add it to the list of functions to reset \DeclareDocumentCommand{\tikz}{D#3{}}{% \def\robExtCommandOrigName{tikz}% we specify the name of the current environment \edef\robExt@tmp@args{\detokenize{##1}}% we specify the name of the current environment \edef\robExt@tmp@orig@args{\detokenize{#1}}% we specify the name of the current environment % we start the parsing \robExt@tikz% }% } \let\cacheTikz\robExtExternalizeAllTikzpictures \NewDocumentCommand{\robExtDoNotExternalizeAllTikzpictures}{}{% \ifdefined\robExtCommandOrigtikz% \DeclareCommandCopy{\tikz}{\robExtCommandOrigtikz}% \let\robExtCommandOrigtikz\undefined \fi% \ifdefined\robExtEnvironmentOrigtikzpicture% \DeclareEnvironmentCopy{tikzpicture}{robExtEnvironmentOrigtikzpicture}% \let\robExtEnvironmentOrigtikzpicture\undefined \fi% } \let\doNotCacheTikz\robExtDoNotExternalizeAllTikzpictures %% The cached version \DeclareDocumentEnvironment{tikzpictureC}{D<>{}O{}O{}}{% \begin{CacheMe}{tikzpicture,#1}[#2]% }{\end{CacheMe}}% %%%%%% %%%%%% Cache tikzit %%%%%% \NewDocumentCommand{\robExtCacheTikzit}{O{tikzit}}{% \ifdefined\robExtCommandOrigtikzfig\else% \DeclareCommandCopy{\robExtCommandOrigtikzfig}{\tikzfig}% \fi% \robExtAddToCommandResetList{tikzfig}% \DeclareDocumentCommand{\tikzfig}{D<>{}m}{% % tikzit tries first ##2.tikz and fallsback to figures/##2.tikz otherwise. \IfFileExists{##2.tikz}{\def\robExtTikzfigFile{##2}}{\def\robExtTikzfigFile{figures/##2}}% \expanded{\noexpand\cacheMe[ #1, add dependencies/.expanded={\robExtTikzfigFile.tikz}, ##1, command if no externalization/.code={\noexpand\robExtDisableTikzpictureOverwrite\noexpand\robExtCommandOrigtikzfig{##2}}, ]{\noexpand\tikzfig{__ROBEXT_WAY_BACK__\robExtTikzfigFile}}}% }% } \let\cacheTikzit\robExtCacheTikzit \NewDocumentCommand{\robExtCacheTikzitWithStyle}{O{tikzit}m}{% \robExtCacheTikzit[#1]% \robExtConfigure{ % First, we copy the tikzit-related files to the cache: copy file to cache={tikzit.sty}, copy file to cache={#2}, % We create the tikzit preset loaded by default: new preset={#1}{ latex, add to preamble={ \usepackage{tikzit} \input{#2} }, }, }% } \let\cacheTikzitWithStyle\robExtCacheTikzitWithStyle %%%%%% %%%%%% The generic functions %%%%%% %% Usage: \robExtCacheEnvironment{myenv} \NewDocumentCommand{\robExtCacheEnvironment}{O{<>}mm}{% %% We need to save the original environment to avoid infinite recursion if we disable externalization %% https://tex.stackexchange.com/questions/695391/why-isnt-my-environment-restored/695398 \ifcsname robExtEnvironmentOrig#2\endcsname\PackageWarning{Your are trying to cache an environment #2 that seems to be already cached... Expect weird things to happen.}{}\fi \DeclareEnvironmentCopy{robExtEnvironmentOrig#2}{#2}% \robExtAddToEnvironmentResetList{#2}% \DeclareDocumentEnvironment{#2}{D#1{}}{% \def\robExtEnvironmentOrigName{#2}% \CacheMe{% #3,% set placeholder no import={__ROBEXT_MAIN_CONTENT__}{\begin{#2}__ROBEXT_MAIN_CONTENT_ORIG__\end{#2}},% ##1% }% }{\endCacheMe}% } \let\cacheEnvironment\robExtCacheEnvironment % \robExtShowPlaceholder{__ROBEXT_MAIN_CONTENT__} % \robExtShowPlaceholdersContents %%%% Borrowed and adapted from https://github.com/sasozivanovic/memoize/blob/master/xparse-arglist.sty %%%% see also https://tex.stackexchange.com/questions/695662/automatically-wrap-a-macro/695734 %% The idea of the library is that it builds a string like %% [#2]<#3>{#4} %% in order to generate something like %% \NewDocumentCommand{\myfunction}{D<>{}O{coucou}D<>{yes}m} %% { %% \cacheMe[#1]{\myfunction[#2]<#3>{#4}} %% } %%%% _________________________________________________________________ \def\robExtArgumentList{% \expandafter\robExt@arglist\expandafter0\ArgumentSpecification.% } \def\robExt@arglist#1#2{% \ifcsname robExt@arglist@#2\endcsname \csname robExt@arglist@#2\expandafter\expandafter\expandafter\endcsname \else \expandafter\robExt@arglist@error \fi \expandafter{\the\numexpr#1+1\relax}% } % \robExt@arglist@...: #1 = the argument number \def\robExt@arglist@m#1{\noexpand\unexpanded{{#####1}}\robExt@arglist{#1}} \def\robExt@arglist@r#1#2#3{\noexpand\unexpanded{#2#####1#3}\robExt@arglist{#1}} \def\robExt@arglist@R#1#2#3#4{\noexpand\unexpanded{#2#####1#3}\robExt@arglist{#1}} \def\robExt@arglist@v#1{{Handled commands with verbatim arguments are not supported}\robExt@arglist{#1}} % error \def\robExt@arglist@b#1{{This is not the way to handle environment}\robExt@arglist{#1}} % error \def\robExt@arglist@o#1{\noexpand\unexpanded{[#####1]}\robExt@arglist{#1}} \def\robExt@arglist@d#1#2#3{\noexpand\unexpanded{#2#####1#3}\robExt@arglist{#1}} \def\robExt@arglist@O#1#2{\noexpand\unexpanded{[#####1]}\robExt@arglist{#1}} % \def\robExt@arglist@O#1#2{\noexpand\unexpanded{[#####1]}\robExt@arglist{#1}} \def\robExt@arglist@D#1#2#3#4{\noexpand\unexpanded{#2#####1#3}\robExt@arglist{#1}} \def\robExt@arglist@s#1{\noexpand\IfBooleanT{#####1}{*}\robExt@arglist{#1}} \def\robExt@arglist@t#1#2{\noexpand\IfBooleanT{#####1}{#2}\robExt@arglist{#1}} \csdef{robExt@arglist@+}#1{\expandafter\robExt@arglist\expandafter{\the\numexpr#1-1\relax}}% \csdef{robExt@arglist@.}#1{} % e,E: Embellishments are not supported. % > Argument processors are not supported. And how could they be? \def\robExt@arglist@error#1.{{Unknown argument type}} %%%% _________________________________________________________________ %% Since the very first occurrence of D is not forwarded to the function %% we discard it: %% The first argument of \robExt@arglist@D is the number of the argument %% so #2#####1#3 reads as #2 = <, #### = #, #1 = > #3 = default value that can be discarded since it is already part of the argument spec. \def\robExt@arglist@D#1#2#3#4{% \noexpand\unexpanded{% \ifnum #1=1 % If it is the first argument D, then we do not add anything to the argument string \else #2#####1#3\fi}\robExt@arglist{#1}% } %% Usage: \robExtCacheCommand[delimiters]{name command}[argument specification]{style} %% Inspired and modified from %% https://github.com/sasozivanovic/memoize/blob/81d960aa547148bdb38fea89917eda1476c9bace/memoize.sty#L744 \NewDocumentCommand{\robExtCacheCommand}{O{<>}mom}{% %% We get the specification of the command, like "O{}mm" \IfNoValueTF{#3}{% \expandafter\GetDocumentCommandArgSpec\csname #2\endcsname% }{% \def\ArgumentSpecification{#3}% }% \edef\ArgumentSpecification{D#1{}\ArgumentSpecification}% \robExtAddToCommandResetList{#2}% % We copy the original definition for later (if externalization is disabled) \expandafter\DeclareCommandCopy\csname robExtCommandOrig#2\expandafter\endcsname\csname #2\endcsname% \edef\robExt@marshal{% \noexpand\DeclareDocumentCommand% \expandonce{\csname #2\endcsname}% {\expandonce{\ArgumentSpecification}}% {% \noexpand\def\noexpand\robExtCommandOrigName{#2}% % todo: add a hook for users setup; prevent user from changing \MemoizeWrapper? \edef\noexpand\robExt@marshal{% \noexpand\noexpand\noexpand\robExtRescanHashRobust{\noexpand\noexpand\noexpand\robExtCacheMe[\detokenize{#4}, \noexpand\detokenize{####1}]{% \noexpand\noexpand\expandonce{\csname #2\endcsname}% \robExtArgumentList% }% }% }% % For debug %\noexpand\show\noexpand\robExt@marshal% \noexpand\robExt@marshal% }% }% % for debug %\show\robExt@marshal% \robExt@marshal% } \let\cacheCommand\robExtCacheCommand