helper.latex.macros_and_commands

Latex functions for identifying macros and commands (to replace)
from fastcore.test import *

Identify macros and commands (to replace)

The following functions were originally written for latex.formatting, but were moved here.


source

custom_commands

 custom_commands (preamble:str)

*Return a dict mapping commands (and math operators) defined in preamble to the number of arguments display text of the commands.

Assumes that the newcommands only have at most one default parameter (newcommands with multiple default parameters are not valid in LaTeX).

Ignores all comented newcommands.*

Type Details
preamble str The preamble of a LaTeX document.
Returns list Each tuple consists of 1. the name of the custom command 2. the number of parameters 3. The default argument if specified or None otherwise, and 4. the display text of the command.
# Basic
text_1 = r'\newcommand{\con}{\mathcal{C}}'
test_eq(custom_commands(text_1), [('con', 0, None, r'\mathcal{C}')])

# With a parameter
text_2 = r'\newcommand{\field}[1]{\mathbb{#1}}'
test_eq(custom_commands(text_2), [('field', 1, None, r'\mathbb{#1}')]) 

# With multiple parameters, the first of which has a default value of `2`
text_3 = r'\newcommand{\plusbinomial}[3][2]{(#2 + #3)^#1}'
test_eq(custom_commands(text_3), [('plusbinomial', 3, '2', r'(#2 + #3)^#1')])

# The display text has backslashes `\` and curly brances `{}``
text_4 = r'\newcommand{\beq}{\begin{displaymath}}'
test_eq(custom_commands(text_4), [('beq', 0, None, '\\begin{displaymath}')])


# Basic with spaces in the newcommand declaration
text_6 = r'\newcommand {\con}  {\mathcal{C}}'
test_eq(custom_commands(text_6), [('con', 0, None, r'\mathcal{C}')])

# With a parameter and spaces in the newcommand declaration
text_7 = r'\newcommand   {\field}   [1] {\mathbb{#1}}'
test_eq(custom_commands(text_7), [('field', 1, None, r'\mathbb{#1}')])

# With multiple parameters, a default value, and spaces in the newcommand declaration
text_8 = r'\newcommand {\plusbinomial} [3] [2] {(#2 + #3)^#1}'
test_eq(custom_commands(text_8), [('plusbinomial', 3, '2', r'(#2 + #3)^#1')]) 

# With a comment `%'; commented out command declarations should not be detected.
text_9 = r'% \newcommand{\con}{\mathcal{C}}'
test_eq(custom_commands(text_9), [])


# Spanning multiple lines
text_10 = r'''\newcommand{\mat}[4]{\left[\begin{array}{cc}#1 & #2 \\
                                         #3 & #4\end{array}\right]}'''
test_eq(
    custom_commands(text_10),
    [('mat', 4, None,
             '\\left[\\begin{array}{cc}#1 & #2 \\\\\n                                         #3 & #4\\end{array}\\right]')])

# Math operator
text_11 = r'\DeclareMathOperator{\Hom}{Hom}'
test_eq(custom_commands(text_11), [('Hom', 0, None, 'Hom')])

text_12 = r'\DeclareMathOperator{\tConf}{\widetilde{Conf}}'
test_eq(custom_commands(text_12), [('tConf', 0, None, r'\widetilde{Conf}')])

# `\def` commands
# \def is a bit complicated because arguments can either be provided with []
# or can be provided with {}.
text_13 = r'\def\A{{\cO_{K}}}'
test_eq(custom_commands(text_13), [('A', 0, None, r'{\cO_{K}}')])

# newcommand and renewcommand don't require {} for the
# command name, cf. https://arxiv.org/abs/1703.05365
text_14 = r'\newcommand\A{{\mathbb A}}'
test_eq(custom_commands(text_14), [('A', 0, None, r'{\mathbb A}')])

# A test for https://arxiv.org/abs/0902.4637
text_15 = r'\newcommand{\til}[1]{{\widetilde{#1}}}'
test_eq(custom_commands(text_15), [('til', 1, None, '{\\widetilde{#1}}')])

source

regex_pattern_detecting_command

 regex_pattern_detecting_command
                                  (command_tuple:tuple[str,int,typing.Opti
                                  onal[str],str])

*Return a regex.pattern object (not a re.pattern object) detecting the command with the specified number of parameters, optional argument, and display text.

Assumes that the curly braces used to write the invocations of the commands are balanced and properly nested. Assumes that there are no two commands of the same name.*

Type Details
command_tuple tuple Consists of 1. the name of the custom command 2. the number of parameters 3. The default argument if specified or None otherwise, and 4. the display text of the command.
Returns Pattern
# Basic
pattern = regex_pattern_detecting_command(('Sur', 0, None, r'\mathrm{Sur}'))
text = r'The number of element of $\Sur(\operatorname{Cl} \mathcal{O}_L, A)$ is ...'
match = pattern.search(text)
start, end = match.span()
test_eq(text[start:end], r'\Sur')

pattern = regex_pattern_detecting_command(('frac', 2, None, r'\mathrm{Sur}'))
text = r'\frac{\frac{2}{5}}{7}'
match = pattern.search(text)
start, end = match.span()
test_eq(text[start:end], text)

pattern = regex_pattern_detecting_command(('frac', 2, None, r'\mathrm{Sur}'))
text = r'\frac{error}{7'
match = pattern.search(text)
test_is(match, None)
# start, end = match.span()
# test_eq(text[start:end], text)

pattern = regex_pattern_detecting_command(('frac', 2, None, r'\mathrm{Sur}'))
text = r'\frac{\frac{2}{5}}{7'
match = pattern.search(text)
start, end = match.span()
test_eq(text[start:end], r'\frac{2}{5}')

# One parameter
pattern = regex_pattern_detecting_command(('field', 1, None, r'\mathbb{#1}'))
text = r'\field{Q}'
# print(pattern.pattern)
match = pattern.search(text)
start, end = match.span()
test_eq(text[start:end], text)

# Multiple parameters
pattern = regex_pattern_detecting_command(('mat', 4, None, r'\left[\begin{array}{cc}#1 & #2 \\ #3 & #4\end{array}\right]'))
text = r'\mat{{123}}{asdfasdf{}{}}{{{}}}{{asdf}{asdf}{}}' # This is a balanced str.
match = pattern.search(text)
start, end = match.span()
test_eq(text[start:end], text)
test_eq(match.group(1), r'{123}')

# Multiple parameters, one of which is optional parameter
pattern = regex_pattern_detecting_command(('plusbinomial', 3, '2', r'(#2 + #3)^#1'))
# When the optional parameter is used
text = r'\plusbinomial{x}{y}'
match = pattern.search(text)
start, end = match.span()
test_eq(text[start:end], text)

# When the optional parameter is not used
text = r'\plusbinomial[4]{x}{y}'
match = pattern.search(text)
start, end = match.span()
test_eq(text[start:end], text)

# One parameter that is optional.
pattern = regex_pattern_detecting_command(('greet', 1, 'world', r'Hello #1!'))
# When the optional parameter is used
text = r'\greet'
match = pattern.search(text)
start, end = match.span()
test_eq(text[start:end], text)

# When the optional parameter is not used
text = r'\greet[govna]'
match = pattern.search(text)
start, end = match.span()
test_eq(text[start:end], text)

# In the following example, `\del` is a command defined as `\delta`.
# Any invocation `\delta` should detected as invocations of `\del``
command_tuple = (r'del', 0, None, r'\delta')
pattern = regex_pattern_detecting_command(command_tuple)
text = r'\del should be detected.'
match = pattern.search(text)
start, end = match.span()
test_eq(text[start:end], r'\del')
text = r'\delta should not be detected.'
match = pattern.search(text)
assert match is None
# test_eq(replace_command_in_text(text, command_tuple), r'\delta should be replaced. \delta should not.')

# In the following example, the command takes one argument, but sometimes the command
# is `\del` 
command_tuple = ('til', 1, None, '{\\widetilde{#1}}')
pattern = regex_pattern_detecting_command(command_tuple)
text = r'\til \calh_g'
match = pattern.search(text)
# start, end = match.span()