# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/16_latex.convert.ipynb.

# %% auto 0
__all__ = ['DEFAULT_NUMBERED_ENVIRONMENTS', 'DEFAULT_ENVIRONMENTS_TO_NOT_DIVIDE_ALONG', 'UNTITLED_SECTION_TITLE',
           'COMMON_SECTION_TITLES', 'remove_comments', 'divide_preamble', 'NoDocumentNodeError', 'find_document_node',
           'environment_names_used', 'numbered_newtheorems_counters_in_preamble', 'numberwithins_in_preamble',
           'display_names_of_environments', 'get_node_from_simple_text', 'swap_numbers_invoked', 'divide_latex_text',
           'section_and_subsection_titles_from_latex_parts', 'custom_commands', 'regex_pattern_detecting_command',
           'replace_command_in_text', 'replace_commands_in_text', 'replace_commands_in_latex_document',
           'adjust_common_syntax_to_markdown', 'setup_reference_from_latex_parts']

# %% ../../nbs/16_latex.convert.ipynb 3
from collections import OrderedDict
from itertools import product
import os
from os import PathLike
from pathlib import Path
import re
from typing import Optional, Union

from pathvalidate import sanitize_filename
from pylatexenc import latexwalker, latex2text
from pylatexenc.latexwalker import (
    LatexWalker, LatexEnvironmentNode, get_default_latex_context_db,
    LatexNode, LatexSpecialsNode, LatexMathNode, LatexMacroNode, LatexCharsNode,
    LatexGroupNode, LatexCommentNode
)
from pylatexenc.latex2text import (
    MacroTextSpec, EnvironmentTextSpec)
from pylatexenc.macrospec import (
    MacroSpec, LatexContextDb, EnvironmentSpec
)
import regex

from trouver.helper import (
    find_regex_in_text, dict_with_keys_topologically_sorted,
    containing_string_priority, replace_string_by_indices, text_from_file
)
from trouver.markdown.markdown.file import (
    MarkdownFile, MarkdownLineEnum
)
from ..markdown.obsidian.links import ObsidianLink

from ..markdown.obsidian.vault import VaultNote
from trouver.markdown.obsidian.personal.index_notes import (
    correspond_headings_with_folder, convert_title_to_folder_name
)
from ..markdown.obsidian.personal.reference import setup_folder_for_new_reference
from ..markdown.obsidian.vault import VaultNote
import warnings

# %% ../../nbs/16_latex.convert.ipynb 4
DEFAULT_NUMBERED_ENVIRONMENTS = ['theorem', 'corollary', 'lemma', 'proposition',
                                 'definition', 'conjecture', 'remark', 'example',
                                 'question']

# %% ../../nbs/16_latex.convert.ipynb 14
def remove_comments(text: str) -> str:
    # Find all occurrences of the comment pattern %[^\n]*
    return re.sub(r"%[^\n]*", "", text)

# %% ../../nbs/16_latex.convert.ipynb 21
def divide_preamble(
        text: str, # LaTeX document
        document_environment_name: str = "document"
        ) -> tuple[str, str]:
    """Divide the preamble from the rest of a LaTeX document.
    """
    begin_environment_str = rf'\begin{{{document_environment_name}}}'
    pattern = re.compile(re.escape(begin_environment_str))
    match = re.search(pattern, text) 
    start_match, end_match = match.span()
    return text[:start_match], text[start_match:]

    

# %% ../../nbs/16_latex.convert.ipynb 24
class NoDocumentNodeError(Exception):
    """Exception raised when a LatexEnvironmentNode corresponding to the document 
    environment is expected in a LaTeX string, but no such node exists.
    
    **Attributes**
    - text - str
        - The text in which the document environment is not found.
    """
    
    def __init__(self, text):
        self.text = text
        super().__init__(
            f"The following text does not contain a document environment:\n{text}")



# %% ../../nbs/16_latex.convert.ipynb 25
def find_document_node(
        text: str, # LaTeX str
        document_environment_name: str = "document" # The name of the document environment.
        ) -> LatexEnvironmentNode:
    """Find the `LatexNode` object for the main document in `text`.
    
    **Raises**
    - NoDocumentNodeError
        - If document environment node is not detected.
    """
    w = LatexWalker(text)
    nodelist, _, _ = w.get_latex_nodes(pos=0)
    for node in nodelist:
        if node.isNodeType(LatexEnvironmentNode)\
                and node.environmentname == document_environment_name:
            return node
    raise NoDocumentNodeError(text)

# %% ../../nbs/16_latex.convert.ipynb 36
def environment_names_used(
        text: str # LaTeX document
        ) -> set[str]: # The set of all environment names used in the main document.
    """Return the set of all environment names used in the main document
    of the latex code.
    """
    document_node = find_document_node(text)
    return {node.environmentname for node in document_node.nodelist
            if node.isNodeType(LatexEnvironmentNode)}        

# %% ../../nbs/16_latex.convert.ipynb 45
def _search_counters_by_pattern(
        preamble: str,
        newtheorem_regex: re.Pattern, # This is supposed to be a regex that detects and captures parameters of `\newtheorem` commands.
        counter_group: int # This depends on which `newtheorem_regex` is used, and is either 3 or 4. 
        ) -> dict[str, str]: # The 
    """
    Capture the newly defined theorem-like environment names as well as the
    counters that they belong to
    
    This is a helper function for `numbered_newtheorems_counters_in_preamble`.
    
    """
    counters = {}
    for match in newtheorem_regex.finditer(preamble):
        env_name = match.group(1)
        counter = match.group(counter_group)
        # If no counter was specified, use the environment name as the counter
        if counter is None:
            counter = env_name
        counters[env_name] = counter
    return counters

# %% ../../nbs/16_latex.convert.ipynb 48
def numbered_newtheorems_counters_in_preamble(
        document: str # The LaTeX document
        ) -> dict[str, tuple[str, Union[str, None]]]: # The keys are the command names of the environments.  The value a key is a tuple `(<counter>, <reset_by_counter>)`, where `<counter>`` is the counter that the environment belongs to, which can be custom defined or predefined in LaTeX, and `<reset_by_counter>` is a counter whose incrementation resets the # counter of the environment, if available. 
    r"""Return the dict specifying the numbered `\newtheorem` command invocations

    Assumes that

    - invocations of the `\newtheorem` command are exclusively in the
    preamble of the LaTeX document.
    - theorem-like environments are defined using the `\newtheorem` command.
    - no environments of the same name are defined twice.

    This function does not take into account `numberwithins` being used.

    This function uses two separate regex patterns, one to detect the invocations
    of `\newtheorem` in which the optional parameter is the second parameter and
    one to detect those in which the optional parameter is the third parameter.


    """
    preamble, _ = divide_preamble(document)
    preamble = remove_comments(preamble)
    # TODO: maybe use the `regex` package instead of `re` with a recursive
    # balanced-curly braces detecting regex.

    # matches `\newtheorem{theorem}{Theorem}`, `\newtheorem{proposition}[theorem]{Proposition}`
    # does not match `\newtheorem{theorem}{Theorem}[Section]`
    second_parameter_pattern = re.compile(
        # In this case, the optional parameter (if any) should not follow the newtheorem.
        r'\\newtheorem\s*\{\s*(\w+)\s*\}\s*(\[\s*(\w+)\s*\])?\s*\{\s*(.*)\s*\}(?!\s*\[\s*(\w+)\s*\])')
    # matches `\newtheorem{theorem}{Theorem}`, `\newtheorem{theorem}{Theorem}[Section]`,
    # does not match `\newtheorem{proposition}[theorem]{Proposition}`
    third_parameter_pattern = re.compile(
        r'\\newtheorem\s*\{\s*(\w+)\s*\}\s*\{\s*(.*)\s*\}\s*(\[\s*(\w+)\s*\])?')

    # TODO: return a dict whose values are tuples.
    second_results = _search_counters_by_pattern(preamble, second_parameter_pattern, 3)
    third_results = _search_counters_by_pattern(preamble, third_parameter_pattern, 4)
    to_return = {}
    for environment_name, counter in second_results.items():
        to_return[environment_name] = (counter, None)
    for environment_name, reset_counter in third_results.items():
        if environment_name in to_return:
            continue
        to_return[environment_name] = (environment_name, reset_counter)
    return to_return        

# %% ../../nbs/16_latex.convert.ipynb 59
def numberwithins_in_preamble(
        document: str # The LaTeX document
    ) -> dict[str, str]: # The keys are the first arguments of `numberwithin` invocations and the values ar ethe second arguments of `numberwithin` invocations.
    r"""Return the dict describing `numberwithin` commands invoked
    in the preamble of `document`."""
    preamble, _ = divide_preamble(document)
    preamble = remove_comments(preamble)
    pattern = regex.compile(r'\\numberwithin\s*\{\s*(\w+)\s*\}\s*\{\s*(.*)\s*\}')
    numberwithins = {}

    for match in pattern.finditer(preamble):
        environment_to_number = match.group(1)
        environment_to_count = match.group(2)
        numberwithins[environment_to_number] = environment_to_count

    

    return numberwithins

# %% ../../nbs/16_latex.convert.ipynb 63
def display_names_of_environments(
        document: str # The LaTeX document
        ) -> dict[str, str]:  
    r"""Return the dict specifying the display names for each theorem-like
    environment.

    This function uses two separate regex patterns, one to detect the invocations
    of `\newtheorem`
    in which the optional parameter is the second parameter and one to detect
    those in which the optional parameter is the third parameter.

    Assumes that
    - invocations of the `\newtheorem` command are exclusively in the
    preamble of the LaTeX document.
    - theorem-like environments are defined using the `\newtheorem` command.
    - no environments of the same name are defined twice.

    """
    preamble, _ = divide_preamble(document)
    # matches `\newtheorem{theorem}{Theorem}`, `\newtheorem{proposition}[theorem]{Proposition}`
    # does not match `\newtheorem{theorem}{Theorem}[Section]`
    second_parameter_pattern = re.compile(
        # In this case, the optional parameter (if any) should not follow the newtheorem.
        r'\\newtheorem\*?\s*\{\s*(\w+\*?)\s*\}\s*(\[\s*(\w+)\s*\])?\s*\{\s*(.*)\s*\}(?!\s*\[\s*(\w+)\s*\])')
    # matches `\newtheorem{theorem}{Theorem}`, `\newtheorem{theorem}{Theorem}[Section]`,
    # does not match `\newtheorem{proposition}[theorem]{Proposition}`
    third_parameter_pattern = re.compile(
        r'\\newtheorem\*?\s*\{\s*(\w+\*?)\s*\}\s*\{\s*(.*)\s*\}\s*(\[\s*(\w+)\s*\])?')
    second_results = _search_display_names_by_pattern(preamble, second_parameter_pattern, 4)
    third_results = _search_display_names_by_pattern(preamble, third_parameter_pattern, 2)
    return second_results | third_results
    

def _search_display_names_by_pattern(
        preamble: str,
        newtheorem_regex: re.Pattern,
        display_name_group: int # This depends on which `newtheorem_regex` is used, and is either 3 or 4. 
        ) -> dict[str, str]:
    """
    Capture the newly defined theorem-like environment names as well as the
    counters that they belong to"""
    display_names = {}
    for match in newtheorem_regex.finditer(preamble):
        env_name = match.group(1)
        display_name = match.group(display_name_group)
        display_names[env_name] = display_name
    return display_names

# %% ../../nbs/16_latex.convert.ipynb 67
def _setup_counters(
        numbertheorem_counters: dict[str, tuple[str, Union[str, None]]], # An output of `numbered_newtheorems_counters_in_preamble`
        ) -> dict[str, int]:
    r"""
    Return a dict whose keys are of counters in the LaTeX document and whose
    values are all `0`. These key-value pairs are used to keep track of
    the numberings of `parts`.

    One special key is the key of the empty string `''`, which counters the
    parts which do not get a numbering, i.e. for most text that lie outside
    of (numbered) environments

    """
    # TODO: replace enumerated environments with markdown enumerated lists
    # and itemizes with markdown bulleted lists

    # cf. https://www.overleaf.com/learn/latex/Counters#Default_counters_in_LaTeX
    predefined_counters = [
        'part', # Incremented each time the `\part` command is used. It is not reset automatically and casn only be reset by the user
        'chapter', # Incremeneted each time the `\chapter` command is used.
        'section', # Incremented whenever a new `\section` command is encountered
        'subsection', # Incremented whenever a new `\subsection` command is encountered, reset whenever a new `\section` command is encountered
        'subsubsection', # Incremented whenever a new `\subsubsection` command is encounted, reset whenever a new `\subsection` or `\section` command is encountered
        'paragraph', # Incremeneted whenever a new paragraph is started. Reset whenever a new `\subsubsection`, `\subsection`, or `\section` command is encounted
        'subparagraph', # Incremented each time the `\subparagraph` command is used and reset at the beginning of a new
        'page', # Incremented each time a new page is started in the document
        'equation', # Incremeneted whenever the `\begin{equation}` environment is used. 
        'figure', # Incremented whenever a new `figure` environment is encountered
        'table', # Incremeneted whenever a new `taable` environment is encountered`
        'footnote', 
        'mpfootnote',
        'enumi',
        'enumii',
        'enumiii',
        'enumiv']

    counters = {counter: 0 for _, (counter, reset_counter) in numbertheorem_counters.items()}
    for counter in predefined_counters:
        counters[counter] = 0

    counters[''] = 0
    return counters

# %% ../../nbs/16_latex.convert.ipynb 69
def _setup_numberwithins(
        explicit_numberwithins: dict[str, str],
        numbertheorem_counters: dict[str, tuple[str, Union[str, None]]], # An output of `numbered_newtheorems_counters_in_preamble`.
        ) -> dict[str, str]: # The keys are counters and the values are all counters that the key is immediately numbered within.
    """
    Extracts information of counters that are reset when other counters are
    incremented.

    This is a helper function of `_setup_all_numberwithins` as well as
    `divide_latex_text`.
    """
    builtin_numberwithins = {
        'subsection': 'section',
        'subsubsection': 'subsection',
        'paragraph': 'subsubsection',
        'subparagraph': 'paragraph',
        'enumii': 'enumi',
        'enumiii': 'enumii',
        'enumiv': 'enumiii',
        'part': 'chapter',
        'appendix': 'chapter'
    }
    numberwithins = explicit_numberwithins | builtin_numberwithins

    for environmentname, (counter, reset_by_counter) in numbertheorem_counters.items():
        if reset_by_counter is None:
            continue
        numberwithins[environmentname] = reset_by_counter
    return numberwithins

    

def _setup_all_numberwithins(
        explicit_numberwithins: dict[str, str],
        numbertheorem_counters: dict[str, tuple[str, Union[str, None]]], # An output of `numbered_newtheorems_counters_in_preamble`.
        ) -> dict[str, list[str]]: # The keys are counters and the values are all counters that the key is numbered within.
    """
    This is a helper function of `divide_latex_text`.
    """
    numberwithins = _setup_numberwithins(explicit_numberwithins, numbertheorem_counters)
    all_counters = set()
    for key, value in numberwithins.items():
        all_counters.add(key)
        all_counters.add(value)
    all_numbered_withins = {counter: [] for counter in all_counters}
    for counter_1, counter_2 in product(all_counters, all_counters):
        if _is_numberedwithin(counter_1, counter_2, numberwithins):
            all_numbered_withins[counter_1].append(counter_2)
    return all_numbered_withins


def _is_numberedwithin(
        counter_1, counter_2, numberwithins: dict[str, str]
        ) -> bool:
    """Return `True` if `counter_1` is numbered within `counter_2""" 
    if counter_1 not in numberwithins:
        return False
    elif numberwithins[counter_1] == counter_2:
        return True
    return _is_numberedwithin(
        numberwithins[counter_1], counter_2, numberwithins)


# %% ../../nbs/16_latex.convert.ipynb 71
def _unnumbered_environments(
        numbertheorem_counters: dict[str, tuple[str, Union[str, None]]], # An output of `numbered_newtheorems_counters_in_preamble`
        display_names: dict[str, str]) -> set[str]:
    r"""Return the set of unnumbered theorem-like environments defined by
    `\newtheorem`.

    This is a helper function of `divide_latex_text`.
    """
    return {environment for environment in display_names
            if environment not in numbertheorem_counters}

    

# %% ../../nbs/16_latex.convert.ipynb 73
def _section_title(
        text: str
        ) -> tuple[bool, str]: # The bool is `True` if the section/subsection is numbered (i.e. is `section` or `subsection` as opposed to `section*` or `subsection*`). The `str` is the title of the section or subsection
    """Return the title of a section or subsection from a latex str
    and whether or not the section/subsection is numbered"""

    # Note that the `section` command has the optional argument `toc-title` which appears
    # in the table of contents, cf.
    # http://latexref.xyz/_005csection.html
    pattern = regex.compile(
        r'\\(?:section|subsection)\s*(?:\[.*\])?(\*)?\s*'
        r'\{((?>[^{}]+|\{(?2)\})*)\}',
        regex.MULTILINE
    )
    regex_search = regex.search(pattern, text)
    is_numbered = regex_search.group(1) is None
    title = regex_search.group(2)
    return is_numbered, title


# %% ../../nbs/16_latex.convert.ipynb 75
def _is_section_node(node):
    return (node.isNodeType(LatexMacroNode)
            and node.macroname == 'section')

def _is_subsection_node(node):
    return (node.isNodeType(LatexMacroNode)
            and node.macroname == 'subsection')

def _is_environment_node(node):
    return node.isNodeType(LatexEnvironmentNode)

# %% ../../nbs/16_latex.convert.ipynb 77
def _is_numbered(
        node: LatexNode,
        numbertheorem_counters: dict[str, str]
        ) -> bool:
    if _is_section_node(node) or _is_subsection_node(node):
        is_numbered, _ = _section_title(node.latex_verbatim())
        return is_numbered
    elif _is_environment_node(node):
        return node.environmentname in numbertheorem_counters
    else:
        return False

# %% ../../nbs/16_latex.convert.ipynb 79
def _change_counters(
        node,
        counters,
        numbertheorem_counters: dict[str, str],
        all_numberwithins: dict[str, list[str]]
        ):
    # identify which counter to change
    # TODO
    # Take into consideration unnumbered non-environment node
    # Take into consideration unnumbered environment node
    if _is_environment_node(node):
        if node.environmentname in numbertheorem_counters:
           counter = numbertheorem_counters[node.environmentname][0]
        else:
            counter = None
    elif _is_section_node(node):
        counter = 'section'
    elif _is_subsection_node(node):
        counter = 'subsection'
    else:
        counter = None

    # Section counters seem to only reset subsection counters
    # When the section is numbered, etc., cf. `numbering_example_4...`
    # and `numbering_example_5...` in `nbs\_tests\latex_examples`
    is_numbered = _is_numbered(node, numbertheorem_counters)
    # e.g. `\numberwithin{equation}{section}`` means that `equation` is
    # numbered within `section`, i.e. `equation` is reset whenever
    # `section` is incremeneted

    # if counter is None and not _is_environment_node(node):
    #     counters[''] += 1 
    #     return

    if is_numbered:
        counters[counter] += 1
    for numbered_counter, within_counter in all_numberwithins.items():
        if counter is not None and counter in within_counter:
            counters[numbered_counter] = 0



# %% ../../nbs/16_latex.convert.ipynb 80
def get_node_from_simple_text(
        text: str) -> LatexNode:
    """Return the (first) `LatexNode` object from a str."""
    w = LatexWalker(text)
    nodelist, _, _ = w.get_latex_nodes(pos=0)
    return nodelist[0]

# %% ../../nbs/16_latex.convert.ipynb 83
def _node_numbering(
        node: LatexNode,
        numbertheorem_counters: dict[str, str],
        numberwithins: dict[str, str],
        counters: dict[str, int]
        ) -> str: # Just the numbering of the node, no "section/subsection" or displayname
    if _is_section_node(node):
        counter = 'section'
    elif _is_subsection_node(node):
        counter = 'subsection'
    elif _is_environment_node(node):
        counter = numbertheorem_counters[node.environmentname][0]
    return _numbering_helper('', counter, numberwithins, counters)


def _numbering_helper(
        trailing_numbering: str,
        counter: str,
        numberwithins: dict[str, str],
        counters: dict[str, int]
        ) -> str:
    """Recurisve helper function to `_node_numbering`."""
    if counter not in numberwithins and counter not in counters:
        return trailing_numbering
    if counter not in numberwithins and counter in counters and trailing_numbering:
        return f'{counters[counter]}.{trailing_numbering}'
    if counter not in numberwithins and counter in counters and not trailing_numbering:
        return f'{counters[counter]}'

    parent_counter = numberwithins[counter]
    current_count = counters[counter]
    if not trailing_numbering:
        to_pass_to_trailing_numbering = str(current_count)
    else:
        to_pass_to_trailing_numbering = f'{current_count}.{trailing_numbering}'

    return _numbering_helper(
        to_pass_to_trailing_numbering,
        parent_counter,
        numberwithins,
        counters)
    

# %% ../../nbs/16_latex.convert.ipynb 85
def _title(
        node: LatexNode,
        numbertheorem_counters: dict[str, str],
        numberwithins: dict[str, str], # An output of _setup_numberwithins
        all_numberwithins: dict[str, list[str]], # An output of all_numberwithins
        display_names: dict[str, str],
        counters: dict[str, int],
        swap_numbers: bool):
    """Return the title of a node based on the count in
    `counters`"""
    numbered = _is_numbered(node, numbertheorem_counters)
    if _is_section_node(node) and numbered:
        _, title = _section_title(node.latex_verbatim())
        return f"{counters['section']}. {title}"
    if _is_section_node(node) and not numbered:
        _, title = _section_title(node.latex_verbatim())
        return title 

    if _is_subsection_node(node) and numbered:
        _, title = _section_title(node.latex_verbatim())
        return f"{counters['section']}.{counters['subsection']}. {title}"
    if _is_subsection_node(node) and not numbered:
        _, title = _section_title(node.latex_verbatim())
        return title

    if _is_environment_node(node):
        return _title_for_environment_node(
            node, numbertheorem_counters, numberwithins,
            display_names, counters, swap_numbers)


def _title_for_environment_node(
        node: LatexNode,
        numbertheorem_counters: dict[str, str],
        numberwithins: dict[str, list[str]],
        display_names: dict[str, str],
        counters: dict[str, int],
        swap_numbers: bool):
    """Return the title of an environment node.
    If the node is not that of an theorem-like environment, then 
    
    """
    numbered = _is_numbered(node, numbertheorem_counters)
    # TODO: see what happens when environments are numbered within
    # sections vs. subsections
    if not numbered:
        numbering = None
    else:
        numbering = _node_numbering(
            node, numbertheorem_counters, numberwithins, counters)
    
    environment = node.environmentname
    if environment in display_names:
        display_name = display_names[environment]
    else:
        display_name = environment
    if not numbered:
        return display_name
    elif swap_numbers:
        return f'{numbering}. {display_name}.'
    else:
        return f'{display_name} {numbering}.'
        

# %% ../../nbs/16_latex.convert.ipynb 87
def swap_numbers_invoked(
        preamble: str
        ) -> bool: # 
    """Returns `True` if `\swapnumbers` is in the preamble.

    Assume that a mention of `\swapnumbers` is an actual invocation.
    """
    preamble = remove_comments(preamble)
    return '\swapnumbers' in preamble

# %% ../../nbs/16_latex.convert.ipynb 89
def _node_warrants_own_part(
        node, environments_to_not_divide_along: list[str],
        accumulation: str, parts: list[tuple[str, str]]) -> bool:
    """Return `True` if `node` warrants making a new part to be added in `parts`.

    This is a helper function for `_process_node`. When this function returns
    `true`, the `accumulation` should be considered for appending to `parts`
    and the node should also be appended to `parts
    and 
    """
    if _is_section_node(node) or _is_subsection_node(node):
        return True
    elif not _is_environment_node(node):
        return False
    # Is environment node from here and below.
    if len(parts) == 0 and accumulation.strip() == '':
        return True
    # elif len(parts) == 0:
    #     return node.environmentname not in environments_to_not_divide_along
    # previous_node = get_node_from_simple_text(parts[-1][1])
    # if (accumulation.strip() == ''
    #         and (_is_section_node(previous_node)
    #              or _is_subsection_node(previous_node))):
    #     return True
    return node.environmentname not in environments_to_not_divide_along

# %% ../../nbs/16_latex.convert.ipynb 91
def _node_is_proof_immediately_following_a_theorem_like_environment(
        node, accumulation, parts, display_names) -> bool:
    """Return `True` if `node` is that of a proof environment that immediately
    follows a theorem-like environment.
    
    This is a helper function for `_process_node`.
    """
    if not _is_environment_node(node):
        return False
    if not node.environmentname == 'proof':
        return False
    if not len(parts) > 0:
        return False
    if accumulation.strip() != '':
        return False
    previous_node = get_node_from_simple_text(parts[-1][1])
    if not _is_environment_node(previous_node):
        return False
    return previous_node.environmentname in display_names


# %% ../../nbs/16_latex.convert.ipynb 93
DEFAULT_ENVIRONMENTS_TO_NOT_DIVIDE_ALONG = [
    'equation', 'equation*', 'proof', 'align', 'align*', 'enumerate', 'itemize', 'label',
    'eqnarray', 'quote', 'tabular', 'table']
def divide_latex_text(
        document: str, 
        # environments_to_divide_along: list[str], # A list of the names of environments that warrant a new note
        # numbered_environments: list[str], # A list of the names of environments which are numbered in the latex code. 
        environments_to_not_divide_along: list[str] = DEFAULT_ENVIRONMENTS_TO_NOT_DIVIDE_ALONG, # A list of the names of the environemts along which to not make a new note, unless the environment starts a section (or the entire document).
        ) -> list[tuple[str, str]]: # Each tuple is of the form `(note_title, text)`, where `note_title` often encapsulates the note type (i.e. section/subsection/display text of a theorem-like environment) along with the numbering and `text` is the text of the part. Sometimes `title` is just a number, which means that `text` is not of a `\section` or `\subsection` command and not of a theorem-like environment.
    r"""Divide LaTeX text to convert into Obsidian.md notes.

    Assumes that the counters in the LaTeX document are either the
    predefined ones or specified by the `\newtheorem` command.

    This function does not divide out `\subsubsection`'s.

    Proof environments are assigned to the same parts their prcededing
    theorem-like environments, if available.

    TODO: Implement counters specified by `\newcounter`, cf. 
    https://www.overleaf.com/learn/latex/Counters#LaTeX_commands_for_working_with_counters.
    """
    numbertheorem_counters = numbered_newtheorems_counters_in_preamble(document)
    explicit_numberwithins = numberwithins_in_preamble(document)
    numberwithins = _setup_numberwithins(explicit_numberwithins, numbertheorem_counters)
    all_numberwithins = _setup_all_numberwithins(explicit_numberwithins, numbertheorem_counters)
    # environments_to_counters = counters_for_environments(document)
    display_names = display_names_of_environments(document)
    counters = _setup_counters(numbertheorem_counters)
    unnumbered_environments = _unnumbered_environments(
        numbertheorem_counters, display_names)
    # Eventually gets returned
    preamble, main_document = divide_preamble(document)
    document_node = find_document_node(main_document)
    swap_numbers = swap_numbers_invoked(preamble)
    parts = []
    # "Accumulates" a "part" until text that should comprise a new part is encountered
    accumulation = '' 
    for node in document_node.nodelist:
        accumulation = _process_node(
            node, environments_to_not_divide_along, accumulation,
            numbertheorem_counters,
            numberwithins, all_numberwithins, counters,
            display_names, swap_numbers, parts)
    _append_non_environment_accumulation_to_parts_if_non_empty(
        accumulation, counters, parts)
    return parts


def _process_node(
        node, environments_to_not_divide_along, accumulation,
        numbertheorem_counters,
        numberwithins, all_numberwithins, counters,
        display_names, swap_numbers, parts) -> str:
    """
    Update `accumulation`, `counter`, and `parts` based on the contents of `node`.

    Also return 'accumulation` to update it.

    This is a helper function for `divide_latex_text`.

    """
    # If node is a proof immediately following a theorem-like environment
    # Then add it to said theorem-like environment
    if _node_is_proof_immediately_following_a_theorem_like_environment(
            node, accumulation, parts, display_names):
        parts[-1][1] += node.latex_verbatim()
        return accumulation

    _change_counters(node, counters, numbertheorem_counters, numberwithins)
    if _node_warrants_own_part(
            node, environments_to_not_divide_along, accumulation, parts):
        accumulation =  _append_non_environment_accumulation_to_parts_if_non_empty(
            accumulation, counters, parts)
        
        title = _title(
            node, numbertheorem_counters, numberwithins, all_numberwithins,
            display_names, counters, swap_numbers).strip()
        title = title.replace('\n', '') 
        parts.append([title, node.latex_verbatim()])
    else:
        accumulation += node.latex_verbatim()
        # In _change_counters`, the '' counter is incremented by default.
        # This offsets the incorrectly incrementation.
    return accumulation


def _append_non_environment_accumulation_to_parts_if_non_empty(
        accumulation: str, counters, parts):
    """Append accumulation to `parts` if `accumulation` is nonempty
    and return the updated `accumulation` """
    if accumulation.strip() != '':
        counters[''] += 1
        parts.append([str(counters['']).strip(), accumulation.strip()])
        return ''
    else:
        return accumulation.strip()






# %% ../../nbs/16_latex.convert.ipynb 113
def _part_starts_section(
        part: tuple[str, str]):
    """
    Return `True` if `part` starts a section (explicitly),
    cf. `divide_latex_text`.
    """
    return part[1].startswith(r'\section')
    # node = get_node_from_simple_text(part[1])
    # return _is_section_node(node)


def _part_starts_subsection(
        part: tuple[str, str]):
    """Return `True` if `part` starts a subsection, cf. `divide_latex_text`."""
    return part[1].startswith(r'\subsection')
    # node = get_node_from_simple_text(part[1])
    # return _is_subsection_node(node)

# %% ../../nbs/16_latex.convert.ipynb 115
UNTITLED_SECTION_TITLE = 'Untitled Section'
def section_and_subsection_titles_from_latex_parts(
        parts: list[tuple[str, str]], # An output of `divide_latex_text`
        # verbose_sections: bool = False, # 
        # short_subsections: bool = False,
        # section_name: str = 'section',
        # subsection_name: str = 'subsection')\
        ) -> list[list[str]]: # Each list corresponds to a section. The first entry of the list is the title of the section and the other entries are the titles of the subsections. 
    """
    Return a list of lists of titles for the sections and subsections in `parts`

    Unnumbered sections get their own list. Unnumbered subsections are also included in lists.
    All the titles are striped (of leading and trailing whitespaces).
    """
    sections_and_subsections = []
    for part in parts:
       _consider_part_to_add(part, sections_and_subsections) 
    return sections_and_subsections


def _consider_part_to_add(
        part: list[tuple[str, str]],
        sections_and_subsections: list[list[str]]):
    """Add the title of `part` to `sections_and_subsections`
    if `part` starts a section or subsection."""
    title = part[0].strip()
    if _part_starts_section(part):
        sections_and_subsections.append([title])
    elif _part_starts_subsection(part):
        sections_and_subsections[-1].append(title)
    elif not sections_and_subsections:
        # If sections and subsections is empty and the very first `part`
        # does not explicitly start a section, then we are in an untitled
        # section.
        sections_and_subsections.append([UNTITLED_SECTION_TITLE])
        


# %% ../../nbs/16_latex.convert.ipynb 130
def custom_commands(
        preamble: str, # The preamble of a LaTeX document.
        ) -> list[tuple[str, int, Union[str, None], str]]: # 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.
    """
    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.
    """
    preamble = remove_comments(preamble)
    latex_commands = _commands_from_newcommand_and_declaremathoperator(preamble)
    # tex_commands = _commands_from_def(preamble)
    return latex_commands


def _commands_from_newcommand_and_declaremathoperator(
        preamble: str, # The preamble of a LaTeX document
        ) -> list[tuple[str, int, Union[str, None], str]]: # 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.
    """
    Get custom commands from invocations of `\newcommand` and `DeclareMathOperator`
    in the preamble.
    """
    # newcommand_regex = regex.compile(
    #     r'(?<!%)\s*\\(?:(?:re)?newcommand|DeclareMathOperator)\s*\{\\\s*(\w+)\s*\}\s*(?:\[(\d+)\]\s*(?:\[(\w+)\])?)?\s*\{((?>[^{}]+|\{(?4)\})*)\}', re.MULTILINE)
    newcommand_regex = regex.compile(
        r'(?<!%)\s*\\(?:(?:re)?newcommand|DeclareMathOperator)\s*(?:\{\\\s*(\w+)\s*\}|\\\s*(\w+))\s*(?:\[(\d+)\]\s*(?:\[(\w+)\])?)?\s*\{((?>[^{}]+|\{(?5)\})*)\}', re.MULTILINE)

    commands = []
    for match in newcommand_regex.finditer(preamble):
        name_surrounded_in_parentheses = match.group(1) # e.g. \newcommand{\A}
        name_without_parentheses = match.group(2) # e.g. \newcommand\A
        num_args = match.group(3)
        optional_default_arg = match.group(4)
        definition = match.group(5)

        if name_surrounded_in_parentheses is not None:
            name = name_surrounded_in_parentheses
        else:
            name = name_without_parentheses

        # Convert the number of arguments to an integer, if it was specified
        if num_args is not None:
            num_args = int(num_args)
        else:
            num_args = 0

        commands.append((name, num_args, optional_default_arg, definition))
    return commands


# def _commands_from_def(
#         preamble: str
#         ) -> list[tuple[str, int, Union[str, None], str]]: # 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.
#     """
#     """
#     def_regex = regex.compile(
#         r'(?<!%)\s*\\def\s*(\\[a-z0-9]+)'
#     )


# %% ../../nbs/16_latex.convert.ipynb 133
def regex_pattern_detecting_command(
        command_tuple: tuple[str, int, Union[None, str], str], # 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.
        ) -> regex.Pattern:
    """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.
    """
    command_name, num_parameters, optional_arg, _ = command_tuple
    backslash_name = fr"\\{command_name}"
    optional_argument_detection = fr"(?:\[(.*?)\])?" if optional_arg is not None else ""
    argument_detection = r""
    if optional_arg is not None:
        trailing_arguments = [_argument_detection(i) for i in range(2, 1+num_parameters)]
        trailing_args_pattern = "\\s*".join(trailing_arguments)
        pattern = (f"{backslash_name}\\s*{optional_argument_detection}\\s*{trailing_args_pattern}")
    elif num_parameters > 0:
        arguments = [_argument_detection(i) for i in range(1, 1+num_parameters)]
        args_pattern = "\\s*".join(arguments)
        pattern = f"{backslash_name}\\s*{args_pattern}"
    else:
        # Match the command name exactly without letters immediately following
        # (but underscores following are okay).
        pattern = f"{backslash_name}(?![^\W_])"
    return regex.compile(pattern)

def _argument_detection(group_num: int):
    return "\{((?>[^{}]+|\{(?1)\})*)\}".replace("1", str(group_num))
    

# %% ../../nbs/16_latex.convert.ipynb 135
def replace_command_in_text(
        text: str,
        command_tuple: tuple[str, int, Union[None, str], str], # 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.
    ):
    """
    Replaces all invocations of the specified command in `text` with the display text
    with the arguments used in the display text.

    Assumes that '\1', '\2', '\3', etc. are not part of the display text. 
    """
    command_name, num_parameters, optional_arg, display_text = command_tuple
    command_pattern = regex_pattern_detecting_command(command_tuple)
    replace_pattern = display_text.replace('\\', r'\\')
    # if optional_arg is not None:
    #     replace_pattern = replace_pattern.replace('#1', optional_arg)
    replace_pattern = re.sub(r'#(\d)\b', r'\\\1', replace_pattern)
    text = regex.sub(
        command_pattern,
        lambda match: _replace_command(match, command_tuple, command_pattern, replace_pattern),
        text)
    return text

def _replace_command(
        match: regex.match,
        command_tuple: tuple[str, int, Union[None, str], str],
        command_pattern: regex.Pattern,
        replace_pattern: re.Pattern) -> str:
    """Replace the matched command with the display text"""
    command_name, num_parameters, optional_arg, display_text = command_tuple
    start, end = match.span()
    matched_string_to_replace = match.string[start:end]
    if len(match.groups()) > 0 and match.group(1) is None:
        replace_pattern = replace_pattern.replace(r'\1', optional_arg)
        replaced_string = regex.sub(command_pattern, replace_pattern, matched_string_to_replace)
        return replaced_string
    else:
        return regex.sub(command_pattern, replace_pattern, matched_string_to_replace)



# %% ../../nbs/16_latex.convert.ipynb 137
def replace_commands_in_text(
        text: str, # The text in which to replace the commands. This should not include the preamble of a latex document.
        command_tuples: tuple[str, int, Union[None, str], str], # An output of `custom_commands`. 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.
    ) -> str:
    """
    Replaces all invocations of the specified commands in `text` with the
    display text with the arguments used in the display text.

    Assumes that '\1', '\2', '\3', etc. are not part of the display text. 
    """
    for command_tuple in command_tuples:
        text = replace_command_in_text(text, command_tuple)
    return text

# %% ../../nbs/16_latex.convert.ipynb 139
def replace_commands_in_latex_document(
        docment: str
        ) -> str:
    """Return the latex document (without the preamble) with invocations
    of custom commands/operators replaced with their display text.

    Assumes that all custom commands and operators are defined in the
    preamble.

    Assumes that, if commands with the same name are defined multiple times,
    only the finally defined command is used. 

    Even replaces these invocations incommented out text.
    """
    preamble, document = divide_preamble(docment)
    commands = custom_commands(preamble)
    # Note that `command_tuple[0]` is the name of the command.
    unique_commands = {command_tuple[0]: command_tuple for command_tuple in commands} 
    for _, command_tuple in unique_commands.items():
        document = replace_command_in_text(document, command_tuple)
    return document
    

# %% ../../nbs/16_latex.convert.ipynb 143
# TODO: give the option to replace quotations ``'' and `enquote`, e.g. ```unlikely intersections''` into `"unlikely intersections"`
# TODO: give the option to replace emph with `****`, e.g. ``\emph{special}``.
def adjust_common_syntax_to_markdown(
        text) -> str:
    """
    Adjust some common syntax, such as math mode delimiters and equation/align
    environments, for Markdown.

    Assumes that the tokens for math mode delimiters (e.g. `\( \)` and `\[ \]`)
    are not used otherwise.
    """
    # TODO: see if I need to add more substitutions.
    text = re.sub(r'\\\(|\\\)', '$', text)
    text = re.sub(r'\\\[|\\]', '$$', text)
    text = re.sub(r'(\\begin\{(?:align|equation|eqnarray)\*?\})', r'$$\1', text)
    text = re.sub(r'(\\end\{(?:align|equation|eqnarray)\*?\})', r'\1$$', text)
    return text

# %% ../../nbs/16_latex.convert.ipynb 146
def _replace_custom_commands_in_parts(
        parts: list[tuple[str, str]],
        custom_commands: list[tuple[str, int, Union[str, None], str]]
        ) -> list[tuple[str, str]]:
    return [(title, replace_commands_in_text(text, custom_commands))
            for title, text in parts]


def _adjust_common_syntax_to_markdown_in_parts(
        parts: list[tuple[str, str]]
        ) -> list[tuple[str, str]]:
    return [(title, adjust_common_syntax_to_markdown(text))
            for title, text in parts]

# %% ../../nbs/16_latex.convert.ipynb 148
def _adjust_common_section_titles_in_parts(
        parts: list[tuple[str, str]],
        reference_name: str):
    """Adjust common section titles in `parts`

    Common section titles include, but are not limited to,
    `Introduction`, `Notations`,
    `Conventions`, `Preliminaries`, and `Notations and Conventions`    
    
    This is a helper function for `setup_reference_from_latex_parts`.

    """
    return [(_adjusted_title(title, text, reference_name), text)
            for title, text in parts]


# TODO: also adjust title if the title is of the form
# <section_number>_<common_section_title>, e.g.
# 7_acknowledgements
# 8_references
COMMON_SECTION_TITLES = [
    'introduction', 'notations', 'conventions', 'preliminaries',
    'notations and conventions', 'definitions', 'definitions and notations',
    'references', 'acknowledgements']


def _adjusted_title(
        title: str,
        text: str,
        reference_name: str):
    """Adjust the given title"""
    node = get_node_from_simple_text(text)
    if not _is_section_node(node):
        return title
    _, section_title = _section_title(text)
    if section_title.lower() in COMMON_SECTION_TITLES:
        return f'{title}_{reference_name}'
    else:
        return title 

# %% ../../nbs/16_latex.convert.ipynb 151
def _create_notes_from_parts(
        parts: list[tuple[str, str]],
        chapters: list[list[str]],
        index_note: VaultNote, # The index note of the reference that was created.
        vault: Path,
        reference_folder: Path,
        reference_name: str,
        template_mf: MarkdownFile, # The template of the reference that was created.
        ):
    """Create notes for the vault from `parts`."""
    # headings_folder_correspondence = correspond_headings_with_folder(
    #     index_note, vault)
    title_numbering_folder_map = {
        title: convert_title_to_folder_name(title)
        for section in chapters
        for title in section}

    current_section, current_subsection = chapters[0][0], '' # section/subsection titles
    # Dict of dict of list of str. The top level keys
    # are section titles and the corresponding values are dicts whose
    # keys are subsection titles and values are lists of bulleted links texts
    # of the form `- [[linke_to_note]], Title/identifying information` to add.
    links_to_make = {current_section: {current_subsection: []}}  
    for part in parts:
        current_section, current_subsection = _create_part_or_update(
            part, title_numbering_folder_map, vault, reference_folder,
            reference_name, template_mf, current_section, current_subsection,
            links_to_make)

    _make_links_in_index_notes(
        links_to_make, title_numbering_folder_map, vault,
        reference_folder, reference_name)
    

def _make_links_in_index_notes(
        links_to_make: dict[str, dict[str, list[str]]],
        title_numbering_folder_map: dict[str, tuple[str, str]],
        vault: Path,
        reference_folder: Path,
        reference_name: str,
        ):
    for section_title, section_dict in links_to_make.items():
        section_folder = title_numbering_folder_map[section_title]
        _make_links_in_index_note_for_section(
            section_title, section_dict, section_folder,
            vault, reference_folder, reference_name)


def _make_links_in_index_note_for_section(
        section_title: str,
        section_dict: dict[str, list[str]],
        section_folder: str,
        vault: Path,
        reference_folder: Path,
        reference_name: str):
    rel_path = reference_folder / section_folder / f'_index_{section_folder}.md'
    section_index_note = VaultNote(vault, rel_path=rel_path)
    mf = MarkdownFile.from_vault_note(section_index_note)
    for subsection_title, links_to_make_in_header in section_dict.items():
        mf.add_line_in_section(
            subsection_title,
            {'type': MarkdownLineEnum.UNORDERED_LIST,
             'line': '\n'.join(links_to_make_in_header)})
    mf.write(section_index_note)


def _create_part_or_update(
        part: tuple[str, str],
        title_numbering_folder_map: dict[str, tuple[str, str]],
        vault: Path,
        reference_folder: Path,
        reference_name: str,
        template_mf: MarkdownFile,
        current_section: str,
        current_subsection: str,
        links_to_make: dict[str, dict[str, list[str]]],
        ) -> tuple[str, str]:
    """
    Consider `part` for creating a new note or for updating
    `current_section` and `current_subsection`

    Also append to `links_to_make` for each note that is created.
    """
    if _part_starts_section(part):
        current_section = part[0]
        current_subsection = ''
        links_to_make[current_section] = {'': []}
        folder = title_numbering_folder_map[current_section]
        return current_section, current_subsection
    elif _part_starts_subsection(part):
        current_subsection = part[0]
        links_to_make[current_section][current_subsection] = []
        return current_section, current_subsection

    created_note = _create_note_for_part(
        part, title_numbering_folder_map, vault, reference_folder,
        reference_name, template_mf, current_section, current_subsection)

    _update_links_to_make(
        part, current_section, current_subsection, links_to_make,
        created_note)
    return current_section, current_subsection


def _create_note_for_part(
        part: tuple[str, str],
        title_numbering_folder_map: dict[str, tuple[str, str]],
        vault: Path,
        reference_folder: Path,
        reference_name: str,
        template_mf: MarkdownFile,
        current_section: str,
        current_subsection: str
        ) -> VaultNote: # The created VaultNote.
    """Create a note for the part"""
    note_title = sanitize_filename(part[0])
    note_contents = part[1]
    mf = template_mf.copy(True)
    mf.add_line_in_section(
        'Topic[^1]', {'type': MarkdownLineEnum.DEFAULT, 'line': note_contents})
    mf.parts[-1]['line'] += note_title
    section_folder = title_numbering_folder_map[current_section]
    # TODO: Make it so that unique_note_name indicates an unnumbered
    # note as unnumbered.
    unique_note_name = VaultNote.unique_name(
        f"{reference_name}_{note_title}", vault)
    if current_subsection == '':
        rel_path = (
            reference_folder / section_folder / f"{unique_note_name}.md")
    else:
        subsection_folder = title_numbering_folder_map[current_subsection]
        rel_path = (
            reference_folder / section_folder / subsection_folder / f"{unique_note_name}.md")
    vn = VaultNote(vault, rel_path=rel_path)
    vn.create()
    mf.write(vn)
    return vn



def _update_links_to_make(
        part: tuple[str, str],
        current_section: str,
        current_subsection: str,
        links_to_make: dict[str, dict[str, list[str]]],
        created_note: VaultNote
        ) -> None:
    """Update `links_to_make` after note is created."""
    # if current_subsection is not None:
    #     current_subsection_key = current_subsection
    # else:
    #     current_subsection_key = ''
    note_title = part[0]
    links_to_make[current_section][current_subsection].append(
        f'- [[{created_note.name}]], {note_title}'
    )
    




# %% ../../nbs/16_latex.convert.ipynb 152
# TODO: test parts without a subsection.
# TODO: somehow contents before a section are not inclued. Fix this bug.
# TODO: If section titles are completely empty, e.g. https://arxiv.org/abs/math/0212208,
# Make section titles based on reference name.
# TODO: give the option to not included commented out content from latex files.
def setup_reference_from_latex_parts(
        parts: list[tuple[str, str]], # Output of `divide_latex_text`
        custom_commands: list[tuple[str, int, Union[str, None], str]], # Output of `custom_commands` applied to the preamble of the LaTeX ddocument.`
        vault: PathLike, # An Obsidian.md vault,
        location: PathLike, # The path to make the new reference folder. Relative to `vault`.
        reference_name: PathLike, # The name of the new reference.
        authors: Union[str, list[str]], # Each str is the family name of each author.
        author_folder: PathLike = '_mathematicians', # The directory where the author files are stored in. Relative to `vault`.
        references_folder: PathLike = '_references', # The directory where the references files are stored in. Relative to `vault`.
        templates_folder: PathLike = '_templates', # The directory where the template files are stored in. Relative to `vault`.
        template_file_name: str = '_template_common', # The template file from which to base the template file of the new reference.
        notation_index_template_file_name: str = '_template_notation_index', # The template file from which to base the notation index file of the new reference.
        glossary_template_file_name: str = '_template_glossary', # The template file from which to base the glossary file of the new reference.
        setup_temp_folder: bool = True, # If `True`, creates a `_temp` folder with an index file. This folder serves to house notes auto-created from LaTeX text files before moving them to their correct directories. Defaults to `True`.
        make_second_template_file_in_reference_directory: bool = True, # If `True`, creates a copy of the template note within the directory for the reference.
        copy_obsidian_configs: Optional[PathLike] = '.obsidian', # The folder relative to `vault` from which to copy obsidian configs.  If `None`, then no obsidian configs are copied to the reference folder. Defaults to `.obsidian`. 
        overwrite: Union[str, None] = None, # Specifies if and how to overwrite the reference folder if it already exists.  - If `'w'`, then deletes the contents of the existing reference folder, as well as the template and reference file before setting up the reference folder before creating the new reference folder.  - If `'a'`, then overwrites the contents of the reference folder, but does not remove existing files/folders.  - If `None`, then does not modify the existing reference folder and raises a `FileExistsError`.
        confirm_overwrite: bool = True, # Specifies whether or not to confirm the deletion of the reference folder if it already exists and if `overwrite` is `'w'`. Defaults to `True`.
        verbose: bool = False,
        replace_custom_commands: bool = True, # If `True`, replace the custom commands in the text of `parts` when making the notes.
        adjust_common_latex_syntax_to_markdown: bool = True, # If `True`, apply `adjust_common_syntax_to_markdown` to the text in `parts` when making the notes.`
        ) -> None:
    """Set up a reference folder in `vault` using an output of `divide_latex_text`, create
    notes from `parts`, and link notes in index files in the reference folder.

    Assumes that

    - `parts` is derived from a LaTeX document in which
        - all of the text belongs to sections.
        - all of the sections/subsections are uniquely named
    - The template file is has a section `# Topic`
    - The last line of the template file is a footnote indicating where the note comes from.
    - There is at most one reference folder in the vault whose name is given by
      `reference_name`.

    `parts` itself is not modified, even if `replace_custom_commands` and/or
    `adjust_common_latex_syntax_to_markdown` are set to `True`.

    cf. `setup_folder_for_new_reference` for how the reference folder is set up..

    The names for the subfolders of the reference folder are the section titles, except
    for sections with common titles such as `Introduction`, `Notations`, `Conventions`,
    `Preliminaries`, and `Notations and Conventions`. This ensures that the index
    file names for sections in different reference folders do not have the same name.

    Text/parts that precede explicitly given sections are included in the 
    first section's folder and are linked in the first section's index file.
    """
    parts = _adjust_common_section_titles_in_parts(parts, reference_name)
    chapters = section_and_subsection_titles_from_latex_parts(parts)
    if chapters[0][0] == UNTITLED_SECTION_TITLE:
        chapters[0][0] = f'{reference_name} {UNTITLED_SECTION_TITLE}'
    setup_folder_for_new_reference(
        reference_name, location, authors, vault, author_folder,
        references_folder, templates_folder, template_file_name,
        notation_index_template_file_name, 
        glossary_template_file_name, chapters, setup_temp_folder,
        make_second_template_file_in_reference_directory,
        copy_obsidian_configs, overwrite, confirm_overwrite, verbose)
    index_note = VaultNote(
        vault, rel_path=Path(location) / reference_name / f'_index_{reference_name}.md')
    template_note = VaultNote(vault, name=f'_template_{reference_name}')
    template_mf = MarkdownFile.from_vault_note(template_note)

    if replace_custom_commands:
        parts = _replace_custom_commands_in_parts(parts, custom_commands)
    if adjust_common_latex_syntax_to_markdown:
        parts = [(title, adjust_common_syntax_to_markdown(text))
                 for title, text in parts]
    
    reference_folder = Path(location) / reference_name
    _create_notes_from_parts(
        parts,
        chapters, 
        index_note,
        vault,
        reference_folder,
        reference_name,
        template_mf)
    

