import os
import inspect
import sys
import regex
import subprocess

from htmlcarnap import *
from parser_classes import *
from parser_classes_standard_latex import *

##############################################################################
# LOAD ALL PYTHON MODULES IN THE CUSTOM FOLDER
custom_module_dir = os.path.dirname(os.path.abspath(__file__)) + "/custom"
sys.path.append(custom_module_dir)
file_names = os.listdir(custom_module_dir)


# Filter out files that are Python modules (ending with .py)
python_module_files = [file_name[:-3] for file_name in file_names if file_name.endswith(".py")]

# Import all modules
for module_name in python_module_files:
    module = __import__(module_name)
    globals().update(vars(module))
# END LOAD ALL PYTHON MODULES IN THE CUSTOM FOLDER
##############################################################################

'''
This file contains the main functions that carry out the parsing process of valeptex.py

The functions are called in valeptex.py with the function parse_latex

The basis of the entire process are the classes from parser_classes.py, together with parser_classes_*

There are two main functions: 

start_parse: initializes the parsing process and returns a string
                -- deletes linebreaks
                -- removes comments
                -- replaces some special macros like "a with the respective Unicode characters (ä etc.)
                -- replaces escape sequences and \\ with regular LaTeX macros 
                -- replaces empty lines with par

parse_blocks: parses the entire string and returns the document tree in the form of a DocNode object

'''

##############################################################################
# TRANSFORMS ANY LATEX LENGTH TO A LENGTH SPECIFICATION THAT IS CSS COMPATIBLE
# N.B.: CSS does not recognize the dimensions bp, dd, and cc
def latex_length_to_css(string):
    # ATTENTION: this ignores everything after an initial length specification, i.e. rubber length and the like
    if (match := regex.match(r'(\d*)[\,\.]?(\d*)((?:cm|mm|in|pt|bp|pc|dd|cc|em|ex){1})',string)):
        nmb = float(f'{match.group(1)}.{match.group(2)}')
        if match.group(3) == 'bp' or match.group(3) == 'dd': return str(nmb) + 'pt'
        elif match.group(3) == 'cc': return str(nmb*12) + 'pt'
        else: return str(nmb) +  match.group(3)
    # returns none if no valid length is to be found at the beginning of the string
    else:
        return None


# returns a string with the content of file "path" or None if the file cannot be opened
def replace_string_with_file(path):
    try:
        with open(path, 'r', encoding='utf-8') as f:
            string = f.read()
    except: 
        print(f'ERROR: File {path} could not be opened for parsing!')
        string = None
    return string


## get all subclasses of a class "classname"
def get_subclasses(cls):
    subclasses = []
    # Recursively search for subclasses in all modules
    for name, obj in globals().items():
        if inspect.isclass(obj) and issubclass(obj, cls) and obj != cls:
            subclasses.append((name, obj))
    return subclasses

# calculates the length of a Verbatim environment or command, returns none if this fails
def match_verbatim(strg):
    verbenvs = get_subclasses(Verbatim)
    for name, obj in verbenvs:
        if regex.match(fr'\\begin{{{name}}}([\s\S]*?)\\end{{{name}}}',strg): return name, obj
        elif regex.match(fr'\\{name}{{([\s\S]*?)}}', strg): return name, obj
    return None

#############################################################
# loads the preamble string for a given fileopening
def load_preamble_string(stack, fileopening):
    # Remove comments
    fileopening = regex.sub(r'%.*\n','', fileopening)

    packagestring = fileopening
    while regex.search(r'\\(RequirePackage|usepackage|documentclass)\{(.*?)\}', fileopening):
        match = regex.search(r'\\(RequirePackage|usepackage|documentclass)\{(.*?)\}', fileopening)
        package = match.group(2)
        if not package in stack.packages:
            stack.packages.add(package)
            if match.group(1) == 'documentclass':
                package += '.cls'
            else:
                package += '.sty'
            # try to find package (and ignore it if this fails)    
            try:
                result = subprocess.run(['kpsewhich', package], capture_output=True, text=True, check=True)
                style_path = result.stdout.strip()
                replacement = replace_string_with_file(style_path)
                if replacement: 
                    print(f'Load package {package} ...')
                    packagestring += f'\n\n{replacement}\n\n'
            except:
                print(f'package {package} could not be loaded!')
        fileopening = fileopening[match.end():]

    string = packagestring
    packagestring = ''

    end = 0
    start = 0
    while regex.search(r'\\(?:newcommand|renewcommand)\{\\[a-zA-Z]+\*?\s*\}\s*(?=\{)', string[end:]):
        match = regex.search(r'\\(?:newcommand|renewcommand)\{\\[a-zA-Z]+\*?\s*\}\s*(?=\{)', string[end:])
        start += match.start()
        end += match.end()
        blockend = match_braces(string[end:])
        packagestring += string[start:end+blockend] + '\n'
        end += blockend +1
        start = end

    end = 0
    start = 0
    while (match := regex.search(r'\\newcounter\{[a-zA-Z]+\s*\}\{[a-zA-Z]+\}(?:\[[a-zA-Z]+\])?', string[end:])):
        start += match.start()
        end += match.end()
        packagestring += string[start:end] + '\n'
        start = end
    end = 0
    start = 0
    while (match := regex.search(r'\\newtheorem\{[a-zA-Z]+\s*\}(?:\[[a-zA-Z]+\])?\{[a-zA-Z]+\}(?:\[[a-zA-Z]+\])?', string[end:])):
        start += match.start()
        end += match.end()
        packagestring += string[start:end] + '\n'
        start = end
    end = 0
    start = 0
    

    stack.packagestring = packagestring

    return stack

################################################### START PARSE
def start_parse(tex_file_name, stack):
    # This starts the parsing process
    # Opens File(s), removes comments and escape sequences, replaces empty lines with \par and replaces all line feeds
    # RETURNS A STRING

    if tex_file_name[-4:] == '.tex' or tex_file_name[-4:] == '.TEX':
        tex_file_name = tex_file_name[0:-4]

    print(f"valepTeX parses file {tex_file_name} ...")

    extension = '.tex'
    if os.path.exists(f'{tex_file_name}.TEX'): extension = '.TEX'

    # Read the LaTeX file
    try:
        with open(f'{tex_file_name}{extension}', 'r', encoding='utf-8') as f:
            tex_file = f.read()
            f.close()
    except: 
        try: 
            with open(f'{tex_file_name}.tex', 'r', encoding='latin-1') as f:
                tex_file = f.read()
                f.close()
            print(f'ATTENTION: file {tex_file_name}.tex opened as latin-1')
            stack.error += f'ATTENTION: file {tex_file_name}.tex opened as latin-1\n'
        except:
            print(f'ERROR: file {tex_file_name}.tex could not be opened as either utf-8 or latin-1')
            stack.error += f'ERROR: file {tex_file_name}.tex could not be opened as either utf-8 or latin-1\n'
            return tex_file_name, stack 
    
    if regex.search(r'\\begin\{document\}[\s\S]*\\end\{document\}', tex_file):
        document_body = regex.search(r'([\s\S]*)\\begin\{document\}([\s\S]*)\\end\{document\}', tex_file)
        tex_file = document_body.group(2)
        if stack.defaultpreamble == False:
            stack = load_preamble_string(stack, document_body.group(1))        

    if stack.preamblemode:
        tex_file = stack.fileopening + tex_file + stack.fileclosing
        stack.preamblemode = False

    tex_file = stack.packagestring + tex_file    
    stack.packagestring = ''
    


    # rename \begin{list}/end{list} to \begin{VALEPlist}/end{VALEPlist} to avoid conflicts with python class list
    tex_file = regex.sub(r'(?<=\\(?:begin|end)\{)list(?=\})', 'List', tex_file)

    ############################################################
    # DEAL WITH ALL KINDS OF VERBATIM COMMANDS AND ENVIRONMENTS
    verbenvs = get_subclasses(VerbatimEnvironment)
    verbenvscontent = dict()
    for name, obj in verbenvs:
        verbenvscontent[name] = []
        i= 0
        # deal with the verbatim environment to keep each verbatim block untouched
        def repl(match):
            nonlocal verbenvscontent, i, name
            verbenvscontent[name].append(match.group(1))
            replacement = f'\\begin{{{name}}}@@@@{i}@@@@\\end{{{name}}}'
            i += 1
            return replacement
        pattern = r'\\begin\{' + name + r'\}([\s\S]*?)\\end\{' + name + r'\}' # also works in f form
        tex_file = regex.sub(pattern, repl, tex_file)
    verbcomms = get_subclasses(VerbatimCommand)
    verbcommscontent = dict()
    for name, obj in verbcomms:
        verbcommscontent[name] = []
        i = 0
        pos = 0
        while regex.search(fr'\\{name}\s*(?={{)',tex_file[pos:]):
            match = regex.search(fr'\\{name}\s*(?={{)',tex_file[pos:])
            start = pos + match.end()
            end = start + match_braces(tex_file[start:])
            verbcommscontent[name].append(tex_file[start:end])
            replacement = f'\\{name}{{@@@@{i}@@@@}}'
            i += 1
            tex_file = tex_file[:start] + replacement + tex_file[end:]
            pos = start + len(replacement)



    ################################################################


    ################################################################
    # REMOVE COMMENTS,  EMPTY LINES \\*[]  macros and $$ $$ and $ $
    # Remove comments
    tex_file = regex.sub(r'(?<!\\)%.*\n','',tex_file)
    # Remove TeX Declarations: 
    tex_file = regex.sub(r'\\def[^a-zA-Z]+.*\n', '\n', tex_file)
    # Insert \par if a line ends with several linefeeds
    tex_file = regex.sub(r'(?<=[^\n])\n\n', r'\\par ', tex_file)
    # Remove remaining empty lines
    tex_file = tex_file.replace('\n', ' ')
    # Replace \\*[length]
    pattern = r'\\\\([\*]?)\[(.*?)\]' 
    def repl_length(match):
        replacement = fr'\VALEPlb{match.group(1)}[{match.group(2)}]'
        return replacement
    tex_file = regex.sub(pattern, repl_length, tex_file)
    tex_file = tex_file.replace(r'\\*',r'\VALEPlb*')
    tex_file = tex_file.replace(r'\\',r'\VALEPlb{}')
    # Remove Dollars: $Formulas$ are not useful in html and xml, therefore 
    # as also recommended by mathjax we use only \(Formulas\) and \[Formulas\] etc.
    def repldoll(match):
        repl = f'\\[{match.group(1)}\\]'
        return repl
    tex_file = regex.sub(r'(?<!\\)\$\$([^\$]+)\$\$',repldoll,tex_file)    
    def repldol(match):
        repl = f'\\({match.group(1)}\\)'
        return repl
    tex_file = regex.sub(r'(?<!\\)\$([^\$]+)\$',repldol,tex_file)



    # Remove escape sequences \{ \} \$ \& \# \% \_
    # some escapted characters and text macros of the form \LETTER{LETTER}
    # do not remove here any text macros of the form \MACRONAME -- this is done in parse_blocks
    escape_sequ = {r'\{': r'\VALEPlbrc{}',
                   r'\}': r'\VALEPrbrc{}',
                   r'\$': r'\textdollar{}',
                   r'\&': r'\VALEPand{}',
                   r'\#': r'\VALEPhash{}',
                   r'\%': r'\VALEPpct{}',
                   r'\,': r'\VALEPThinSpace{}',
                   r'\_': r'\textunderscore{}',
                   r'\~{}': r'\textasciitilde{}',
                   r'---': r'\textemdash{}',
                   r'--': r'\textendash{}',
                   r'"~': '‑', # "~ triggers non-breaking hyphen
                   r"!`": r'¡', r"?`": r'¿',
                   r'``': r'“', r',,': r'„', r"''": r'”',
                   r'"`': '„', '"\'': '“',
                   # r'\t{oo}': '' ???
                   r'\c{c}': 'ç', r'\c{C}': 'Ç', 
                   r'\k{a}': 'ą', r'\k{A}': 'Ą', r'\k{e}': 'ę', r'\k{E}': 'Ę', r'\k{i}': 'į', r'\k{I}': 'Į',  
                   r'\k{u}': 'ų', r'\k{U}': 'Ų', r'\k{o}': 'ǫ', r'\k{O}': 'Ǫ', 
                   r'\b{b}': 'ḇ', r'\b{B}': 'Ḇ', r'\b{d}': 'ḏ', r'\b{D}': 'Ḏ', r'\b{k}': 'ḵ', r'\b{K}': 'Ḵ', 
                   r'\b{l}': 'ḻ', r'\b{L}': 'Ḻ', r'\b{n}': 'ṉ', r'\b{N}': 'Ṉ', r'\b{r}': 'ṟ', r'\b{R}': 'Ṟ', 
                   r'\b{t}': 'ṯ', r'\b{T}': 'Ṯ', r'\b{z}': 'ẕ', r'\b{h}': 'ẖ',  
                   # PUT other symbols here, see Kopka/Daly p. 27 + https://en.wikipedia.org/wiki/List_of_Unicode_characters
                   }
    for key in escape_sequ:
        tex_file = tex_file.replace(key,escape_sequ[key])
    
    # Remove \kernDIMEN \looseness=-x because they otherwise throw text snippets in the main text
    tex_file = regex.sub(r'\\(?:kern|vskip|tabcolsep|topsep|partopsep)\-?\d*\.?\d*(?:em|pt|pc|bp|in|cm|mm|dd|cc|sp)','', tex_file)
    tex_file = regex.sub(r'\\(?:looseness|tolerance|pretolerance|hyphenpenalty|exhyphenpenalty|penalty)\=?\-?\d*', '', tex_file)


    # Remove macros of the form \NONALPHACHAR{CHAR} and their varieties:  
    escape_sequ_re = {r'(?<!\\)~': r'\\VALEPnbsp{}',
                      r'(?<!\\),(?!\s)': r'‚',
                      r'(?<!\\)`': r'‘',
                      r"(?<!\\)'": r'’',
                      # without braces and with optional backslash: 
                      r'\\?"a': r'ä', r'\\?"o': r'ö', r'\\?"u': r'ü',
                      r'\\?"A': r'Ä', r'\\?"O': r'Ö', r'\\?"U': r'Ü', r'\\?"s': r'ß',
                      # without braces
                      r'\\"e': 'ë', r'\\"E': 'Ë', r'\\"i': 'ï', r'\\"I': 'Ï',
                      r"\\'A": 'Á', r"\\'E": 'É',
                      r"\\'a": 'á', r"\\'e": r'é', r"\\'i": 'í', r"\\'o": 'ó', r"\\'u": 'ú',
                      r"\\`a": 'à', r"\\`e": 'è', r"\\`i": 'ì', r"\\`o": 'ò', r"\\`u": 'ù',
                      r"\\\^A": 'Â', r"\\\^E": 'Ê', r"\\\^I": 'Î', r"\\\^O": 'Ô', r"\\\^U": 'Û',
                      r"\\\^a": 'â', r"\\\^e": 'ê', r"\\\^i": 'î', r"\\\^o": 'ô', r"\\\^u": 'û',
                      r'\\\^C': 'Ĉ', r'\\\^c': 'ĉ', r'\\\^g': 'ĝ', r"\\'n": 'ń', r'\\~n': 'ñ', 
                      r'\\\^j': 'ĵ', r"\\~A": 'Ã', r"\\~O": 'Õ', r"\\~a": 'ã', r"\\~o": 'õ',
                      r"\\=A": 'Ā', r"\\=E": 'Ē', r"\\=I": 'Ī', r"\\=O": 'Ō', r"\\=U": 'Ū',
                      r"\\=a": 'ā', r"\\=e": 'ē', r"\\=i": 'ī', r"\\=o": 'ō', r"\\=u": 'ū',
                      r'\\\.c': 'ċ', r'\\\.C': 'Ċ', # etc. 
                      # with braces
                      r'\\"\s*\{a\}': r'ä', r'\\"\s*\{o\}': r'ö', r'\\"\s*\{u\}': r'ü',
                      r'\\"\s*\{A\}': r'Ä', r'\\"\s*\{O\}': r'Ö', r'\\"\s*\{U\}': r'Ü', r'\\"\s*\{s\}': r'ß',
                      r'\\"\s*\{e\}': 'ë', r'\\"\s*\{E\}': 'Ë', r'\\"\s*\{i\}': 'ï', r'\\"\s*\{I\}': 'Ï',
                      r"\\'\s*\{A\}": 'Á', r"\\'\s*\{E\}": 'É',
                      r"\\'\s*\{a\}": 'á', r"\\'\s*\{e\}": r'é', r"\\'\s*\{i\}": 'í', r"\\'\s*\{o\}": 'ó', r"\\'\s*\{u\}": 'ú',
                      r"\\`\s*\{a\}": 'à', r"\\`\s*\{e\}": 'è', r"\\`\s*\{i\}": 'ì', r"\\`\s*\{o\}": 'ò', r"\\`\s*\{u\}": 'ù',
                      r"\\\^\s*\{A\}": 'Â', r"\\\^\s*\{E\}": 'Ê', r"\\\^\s*\{I\}": 'Î', r"\\\^\s*\{O\}": 'Ô', r"\\\^\s*\{U\}": 'Û',
                      r"\\\^\s*\{a\}": 'â', r"\\\^\s*\{e\}": 'ê', r"\\\^\s*\{i\}": 'î', r"\\\^\s*\{o\}": 'ô', r"\\\^\s*\{u\}": 'û',
                      r'\\\^\s*\{C\}': 'Ĉ', r'\\\^\s*\{c\}': 'ĉ', r'\\\^\s*\{g\}': 'ĝ', 
                      r'\\\^\s*\{\\j\}': 'ĵ',
                      r"\\'\s*\{n\}": 'ń', r'\\~\s*\{n\}': 'ñ', 
                      r'\\\^\s*\{j\}': 'ĵ', r"\\~\s*\{A\}": 'Ã', r"\\~\s*\{O\}": 'Õ', r"\\~\s*\{a\}": 'ã', r"\\~\s*\{o\}": 'õ',
                      r"\\=\s*\{A\}": 'Ā', r"\\=\s*\{E\}": 'Ē', r"\\=\s*\{I\}": 'Ī', r"\\=\s*\{O\}": 'Ō', r"\\=\s*\{U\}": 'Ū',
                      r"\\=\s*\{a\}": 'ā', r"\\=\s*\{e\}": 'ē', r"\\=\s*\{i\}": 'ī', r"\\=\s*\{o\}": 'ō', r"\\=\s*\{u\}": 'ū',
                      r'\\\.\s*\{c\}': 'ċ', r'\\\.\s*\{C\}': 'Ċ', 
                      r'\\H\s*\{o\}': 'ő', r'\\H\s*\{O\}': 'Ő', r'\\r\s*\{a\}': 'å', r'\\r\s*\{A\}': 'Å',
                       r'\\r\s*\{\s*\}': '°',
                       r'\\u\s*\{a\}': 'ă', r'\\u\s*\{A\}': 'Ă', 
                       r'\\u\s*\{c\}': 'č', r'\\u\s*\{C\}': 'Č',  # !!! instead of c/C with breve 
                       r'\\u\s*\{d\}': 'd', r'\\u\s*\{D\}': 'Ď',  # !!! instead of d/D with breve
                       r'\\u\s*\{h\}': 'ḫ', r'\\u\s*\{H\}': 'Ḫ',  # !!! is this the correct expression ???
                       r'\\u\s*\{e\}': 'ӗ', r'\\u\s*\{E\}': 'Ӗ',
                       r'\\v\s*\{s\}': 'š', r'\\v\s*\{S\}': 'Š', r'\\v\s*\{c\}': 'č', r'\\v\s*\{C\}': 'Č',
                       r'\\v\s*\{s\}': 'š', r'\\v\s*\{S\}': 'Š', r'\\v\s*\{r\}': 'ř', r'\\v\s*\{R\}': 'Ř',
                      # etc. 
                    }
    for key in escape_sequ_re:
        tex_file = regex.sub(key,escape_sequ_re[key],tex_file)

    




    # recursive call of start_parse for all files being addresses with \input{FILE} (or \include{FILE})
    def repl_input_include(match):
        nonlocal stack
        replacement, stack = start_parse(match.group(1), stack)
        return replacement
    tex_file = regex.sub(r'\\input\{(.*?)\}', repl_input_include, tex_file)

    # restore verbatim blocks:

    ###############################################################################
    # PUT THE VERBATIM STUFF ON PLACE AGAIN:
    for name, obj in verbenvs:
        def repl(match):
            nonlocal verbenvscontent, name
            i = int(match.group(1))
            replacement = f'\\begin{{{name}}}{verbenvscontent[name][i]}\\end{{{name}}}'
            i += 1
            return replacement
        tex_file = regex.sub(fr'\\begin{{{name}}}@@@@(\d+)@@@@\\end{{{name}}}', repl, tex_file)
    for name, obj in verbcomms:
        def repll(match):
            nonlocal verbcommscontent, name
            i = int(match.group(1))
            replacement = f'\\{name}{verbcommscontent[name][i]}'
            return replacement
        tex_file = regex.sub(fr'\\{name}{{@@@@(\d+)@@@@}}', repll, tex_file)
    ###############################################################################

    # add an empty line at the end
    tex_file += '\n\n   '

    return tex_file, stack
# END: START PARSE
####################################################################################


###################################################################################################
# begin: functions that identify groups in brackets, braces, \begin{any}\end{any} and \anybegin \anyend
# seems to be not significantly faster than regex but still does the job in a less esoteric way:
# especially the most general form of group matching as it is done by find_end(string, endstring) seems to be
# not possible to achieve with regular expressions

# identifies the end of a section body (trigger endstring="@") or a body delimited with endstring
# in principle this can replace  match_braces, match_brackets, and match_begin_end but still needs to be tested
# this function really grasps all blocks in curly braces and all environment blocks 
# which is not the case with the other functions
# (Thanks to Werner Damböck for explaining the recursive principle of this function to me.)
def find_end(string, endstring):
    end = 0
    while end < len(string):
        # return the position of endstring once it is matched ...
        if string[end:end + len(endstring)] == endstring: return end + len(endstring)
        # ... or check all blocks and environments:
        elif string[end] == '{':
            length = find_end(string[end + 1:], '}')
            if not length == None: end += length 
            else: 
                print('SYNTAX ERROR: brace block does not end!')
                end += 1
        elif (match := regex.match(r'\\([a-zA-Z]+\*?)\s*\[', string[end:])):
            end += match.end()
            length = find_end(string[end:], ']')
            if not length == None: end += length 
            else: 
                print('SYNTAX ERROR: bracket block does not end!')
                end += 1
        elif (match := regex.match(r'\\begin\{([a-zA-Z]+\*?)\}\s*', string[end:])):
            end += match.end()
            name = f'\\end{{{match.group(1)}}}'
            length = find_end(string[end:], name)
            if not length == None: end += length + len(name)
            else:
                print(f'SYNTAX ERROR: environment {match.group(1)} does not end!')
                end += len(name)
        elif string[end:end+2] == '\\(':
            length = find_end(string[end +2:], '\\)')
            if not length == None: end += length
            else:
                print('SYNTAX ERROR: \\( formula does not end!')
                end += 1
        else:
            end += 1
    return None

########################### the following four functions should be replaced with the 
# more accurate function find_end, but this needs to be done with care
#
# identifies the position of the end of a block in brackets that starts at position 0 of strg
def match_brackets(strg):
    if strg == '':
        return None
    i = 1
    openbr = 0
    closebr = 0
    if not strg[0] == '[':
        return None
    while i < len(strg):
        if strg[i] == '[':
            openbr += 1
        if strg[i] == ']':
            if openbr == closebr:
                return i + 1
            else:
                closebr += 1
        i += 1
    return None

# idenfities the position of the end of a block in braces that starts at position 0 of strg
def match_braces(strg):
    if strg == '':
        return None
    i = 1
    openbr = 0
    closebr = 0
    if not strg[0] == '{':
        return None
    while i < len(strg):
        if strg[i] == '{':
            openbr += 1
        if strg[i] == '}':
            if openbr == closebr:
                return i + 1
            else:
                closebr += 1
        i += 1
    return None

def match_environment(strg, env_name):
    if strg == '':
        return None
    i = 1
    openbr = 0
    closebr = 0
    begin = f'\\begin{{{env_name}}}'
    end = f'\\end{{{env_name}}}'
    while i < len(strg):
        if strg[i:i+len(begin)] == begin:
            openbr += 1
        if strg[i:i+len(end)] == end:
            if openbr == closebr:
                return i
            else:
                closebr += 1
        i += 1
    print(f'HOPPLA: End of env [{env_name}] could not be identified(openbr = {openbr}, closebr = {closebr})')
    return None

def match_begin_end(strg, begin, end):
    if strg == '':
        return None
    i = 1
    openbr = 0
    closebr = 0
    if not strg[0:len(begin)] == begin:
        return None
    while i < len(strg):
        if strg[i:i+len(begin)] == begin:
            openbr += 1
        if strg[i:i+len(end)] == end:
            if openbr == closebr:
                return i + len(end)
            else:
                closebr += 1
        i += 1
    return None
#
# end functions that identify groups
##########################################

# to make the text more readable: replace any sequence of whitespaces with a single space ' '
# in mere strings braces are to be replaced entirely, as soon as \{ and \} is replaced with text macros
def minimize_whitespace(string, stack):
    if not isinstance(string, str): return string
    string = regex.sub(r'\s+', ' ',string)
    string = string.replace('{}','')
    if stack.math == False:
        string = regex.sub(r'[\{\}]{1}','',string)
        string = regex.sub(r'\\\-',r'&#173;',string)
    return string

#####################################################################
# parse blocks:
# the main function of the parser ::: recursive parsing of the entire document :::
# the arguments are: a string and a DocStack object
# returns a doclist object and an updated DocStack object
# runs through the entire string token by token 
# the entire parsing process is done in only one iteration of the string
# the function contains around 32 recursive calls for strings that again form doclist objects:
# these nested doclist objects generally comprise arguments of commands and environments
def parse_blocks(strg, stack):
    if not isinstance(strg, str): return strg
    if not stack: stack = DocStack()
    start = 0
    end = 0
    doc_list = DocNode()
    if strg == '':
        doc_list.elements.append(strg)
        return doc_list, stack
    while len(strg) > end:
        if strg[end] == '\\':
            
            ############################################################################
            # CATCH VERBATIM ENVIRONMENTS AND COMMANDS
            # NOTE that verbatim is the only possibility to carry text content entirely unchanged through valeptex 
            if match_verbatim(strg[end:]):
                name, obj = match_verbatim(strg[end:])
                if issubclass(obj,VerbatimEnvironment):
                    match = regex.match(fr'\\begin{{{name}}}([\s\S]*?)\\end{{{name}}}',strg[end:])
                    if end > start:
                        # strings are added as simple string nodes to the DocNode
                        doc_list.elements.append(minimize_whitespace(strg[start:end], stack))
                    comm = obj(name)
                    arg = Argument()
                    arg.name = 'envcontent'
                    node = DocNode()
                    node.elements.append(match.group(1))
                    arg.content = node
                    comm.reqargs.append(arg)
                    doc_list.elements.append(comm)
                    end += match.end()
                    start = end 
                elif issubclass(obj,VerbatimCommand):
                    match = regex.match(fr'\\{name}', strg[end:])
                    if end > start:
                        # strings are added as simple string nodes to the DocNode
                        doc_list.elements.append(minimize_whitespace(strg[start:end], stack))
                    comm = obj(name)
                    node = DocNode()
                    groupend = match_braces(strg[end + match.end():])
                    verbtext = strg[end + match.end():end + match.end() + groupend].strip('{}')
                    node.elements.append(verbtext)
                    comm.reqargs[0].content = node
                    doc_list.elements.append(comm)
                    end += match.end() + groupend
                    start = end 
            
            #########################################################################
            # COMMANDS WITH ALPHABETIC NAMES
            # identify commands with regular names [a-zA-Z]
            elif regex.match(r'\\@?[a-zA-Z]+\*?\s*', strg[end:]):
                #####################################################################
                # CHECK FOR MERE TEXT BEFORE THE CURRENT LaTeX COMMAND
                # first check if there is already some portion of text to be added to DocNode:
                if end > start:
                    # strings are added as simple string nodes to the DocNode
                    doc_list.elements.append(minimize_whitespace(strg[start:end], stack))
                environment_start = end # store current end value for environment
                command_match = regex.match(r'\\(@?[a-zA-Z]+\*?)\s*', strg[end:])
                command_name = command_match.group(1)
                
                ######################################################################
                # COMMANDS in * form are treatet as python class names ending with _
                # take care to command names with a star at the end:
                if command_name[-1] == '*': command_name_no_star = command_name[:-1] + '_'
                else: command_name_no_star = command_name
                end += command_match.end()
                start = end

                ######################################################################
                # CATCH MERE TEXT MACROS
                # catch all simple text macros of the form \ALPHABETIC that produce mere text 
                # outside math mode:
                if command_name_no_star in stack.text_macros and not stack.math:
                    doc_list.elements.append(stack.text_macros[command_name])
                # and inside math mode:
                elif command_name_no_star in stack.text_macros_math and stack.math:
                    doc_list.elements.append(stack.text_macros_math[command_name])
                        
                # catch all commands that are defined as subclass of Command and do not have any arguments:
                elif command_name_no_star in globals() \
                and issubclass(globals()[command_name_no_star], Command) \
                and globals()[command_name_no_star].args == '' \
                and not issubclass(globals()[command_name_no_star], item) \
                and not (stack.math and issubclass(globals()[command_name_no_star], Math)) :
                    comm = globals()[command_name_no_star](command_name_no_star)
                    doc_list.elements.append(comm)
                # catch all commmands that have no arguments and for which a definition is in the DocStack:
                elif stack.is_defined_textmacro(command_name_no_star) \
                and not (command_name_no_star in globals() and issubclass(globals()[command_name_no_star], Math) and stack.math):
                    doc_list.elements.append(stack.get_definition_textmacro(command_name_no_star))
                # in Formula environments: leave all text macros unchanged if they are declared as Math
                elif command_name_no_star in globals() \
                and issubclass(globals()[command_name_no_star], Command) \
                and globals()[command_name_no_star].args == '' \
                and issubclass(globals()[command_name_no_star], Math) and stack.math:
                    doc_list.elements.append(f'\\{command_name}')

                ######################################################################
                # TREAT ALL OTHER COMMANDS THAT ARE NOT IDENTIFIED AS MERE TEXT MACROS
                # enter this block only if it is an unknown command or a command with arguments or a Pseudoenvironment (\item):    
                else:
                    # Number of required arguments must not be higher than 9
                    no_optargs = 4
                    no_reqargs = 9

                    ###################################################################
                    # IDENTIFY THE NUMBER OF ARGUMENTS FOR KNOWN MACROS
                    # for commands in the namespace the number of required arguments is fixed:
                    if command_name_no_star in globals() and issubclass(globals()[command_name_no_star], Command):
                        xcomm = globals()[command_name_no_star](command_name)
                        no_reqargs = len(xcomm.reqargs) 
                        no_optargs = len(xcomm.optargs)
                        xcomm = None
                    
                    declared_command_name = ''
                    ###################################################################
                    # PREPARE FOR DEALING WITH DECLARATIONS AND ENVIRONMENT CALLS 
                    # before creating the command we need to find out if it is a simple command or a declaration or environment call
                    if command_name in ['newcommand', 'newcommand*', 'renewcommand', 'renewcommand*', 'newenvironment', 'newenvironment*', 'newcounter', 'newtheorem', 'begin']:
                        if regex.match(r'\{[\\]?[a-zA-Z]+\*?\s*\}', strg[end:]):
                            # there can be whitespace in and after the first block
                            declared_command = regex.match(r'\{[\\]?([a-zA-Z]+\*?)\s*\}\s*', strg[end:])
                            declared_command_name = declared_command.group(1)
                            if declared_command_name[-1] == '*': declared_command_name = declared_command_name[:-1] + '_'
                            end += declared_command.end()
                            start = end
                            if declared_command_name  in globals() and issubclass(globals()[declared_command_name], Environment):
                                xcomm = globals()[declared_command_name](declared_command_name)
                                no_reqargs = len(xcomm.reqargs) 
                                no_optargs = len(xcomm.optargs)
                                xcomm = None
                        else:
                            # no correct declaration 
                            declared_command_name = 'FALSENAME'
                            stack.error += f'ERROR in {stack.filename}: incorrect declaration or environment call!\n'
                    # deal with commands that have a length or something as the first required argument 
                    # which is followed by optional arguments:
                    required_length = ''
                    if command_name in ['raisebox']:
                        if (match := regex.match(r'\{(.*?)\}', strg[end:])):
                            required_length = match.group(1)
                            #################################### THIS IS AD HOC, IMPROVE LATER
                            if command_name == 'raisebox':
                                required_length = latex_length_to_css(required_length)
                            end += match.end()
                            start = end

                    ####################################################################
                    # IDENTIFY ALL ARGUMENT BLOCKS IN BRACKETS OR BRACES
                    # search for optional arguments (any number of blocks in brackets)
                    i = 0
                    j = 0
                    bracket_block_content = list()
                    while match_brackets(strg[end:]) and j < no_optargs:
                        bracket_block_length = match_brackets(strg[end:])
                        bracket_block_content.append(strg[end+1:end + bracket_block_length -1])  # string
                        end += bracket_block_length
                        start = end
                        i += 1
                        j += 1
                    # search for required arguments (any number of blocks in braces)
                    # in the case of declared commands it is necessary 
                    # the number of brace blocks is restricted to no_reqargs
                    i = 0
                    j = 0
                    brace_block_content = list()
                    if required_length: brace_block_content.append(required_length)
                    while match_braces(strg[end:]) and j < no_reqargs:
                        brace_block_length = match_braces(strg[end:])
                        brace_block_content.append(strg[end+1:end + brace_block_length -1]) # string
                        end += brace_block_length
                        start = end
                        i += 1
                        j += 1
                    # deal with those cases of commands that have further optional arguments after the required arguments:
                    special_bracket_block_content = ''
                    if command_name in ['newtheorem', 'marginnote'] and match_brackets(strg[end:]):
                        special_bracket_block_length = match_brackets(strg[end:])
                        special_bracket_block_content = strg[end+1:end + special_bracket_block_length -1]  # string
                        end += special_bracket_block_length
                        start = end
                        bracket_block_content.append(special_bracket_block_content)
                    if command_name[-4:] == 'mark' and len(brace_block_content) == 0: brace_block_content.append('') 

                    #################################################################
                    # MERGE THE COMMAND AND THE ARGUMENT BLOCKS
                    # 
                    #################################################################
                    # FIRST IDENTIFY ALL DECLARATIONS
                    # (although only those without arguments are effectively treated further) 
                    if command_name in ['newcommand', 'newcommand*', 'renewcommand', 'renewcommand*']:
                        if declared_command_name and len(brace_block_content) > 0:
                            # create declaration: 
                            new_comm = newcommand(command_name,declared_command_name)
                            new_comm.begdef, stack = parse_blocks(brace_block_content[0], stack) # ignore any further brace block    
                            if len(bracket_block_content) > 0:
                                if bracket_block_content[0].isdigit():
                                    new_comm.narg = int(bracket_block_content[0])
                                    if len(bracket_block_content) > 1:
                                        new_comm.opt, stack = parse_blocks(bracket_block_content[1], stack)
                                else:
                                    stack.error += f'ERROR in {stack.filename}: declaration of command "{declared_command_name}" has incorrect assignment of number of args.\n'
                            # put the declaration on the command stack but not in the document
                            stack.cds.append(new_comm)
                        else: 
                            # declaration is ignored if incorrect
                            stack.error += f'ERROR in {stack.filename}: {command_name} declaration of command "{declared_command_name}" is incomplete.\n'
                    elif command_name in ['newenvironment', 'newenvironment*']:
                        if declared_command_name and len(brace_block_content) > 1:
                            # create declaration: 
                            new_comm = newenvironment(command_name,declared_command_name)
                            new_comm.begdef, stack = parse_blocks(brace_block_content[0], stack) 
                            new_comm.enddef, stack = parse_blocks(brace_block_content[1], stack)  # ignore any further brace block    
                            if len(bracket_block_content) > 0:
                                if bracket_block_content[0].isdigit():
                                    new_comm.narg = int(bracket_block_content[0])
                                    if len(bracket_block_content) > 1:
                                        new_comm.opt, stack = parse_blocks(bracket_block_content[1], stack)
                                else:
                                    stack.error += f'ERROR in {stack.filename}: declaration of environment "{declared_command_name}" has incorrect assignment of number of args.\n'
                            stack.envs.append(new_comm)
                        else: 
                            # declaration is ignored if incorrect
                            stack.error += f'ERROR in {stack.filename}: declaration of environment "{declared_command_name}" is incomplete.\n'
                    elif command_name == 'newcounter':
                        if declared_command_name:
                            new_comm = newcounter(command_name,declared_command_name)
                            if len(bracket_block_content) > 0:
                                stack = new_comm.setincounter(stack, bracket_block_content[0]) # ignore any further blocks
                            if not hasattr(stack, "counters") or stack.counters is None:
                                stack.counters = []
                            if isinstance(stack.counters, list):
                                stack.counters.append(new_comm)
                            else:
                                stack.error += f'ERROR in {stack.filename}: stack.counters is {type(stack.counters).__name__}, expected list.\n'
                        else:
                            stack.error += f'ERROR in {stack.filename}: incomplete newcounter declaration.\n'
                    elif command_name == 'newtheorem':
                        if declared_command_name and len(brace_block_content) > 0:
                            new_comm = newtheorem(command_name,declared_command_name)
                            new_comm.begdef, stack = parse_blocks(brace_block_content[0], stack)
                            if len(bracket_block_content) > 0: new_comm.opt, stack = parse_blocks(bracket_block_content[0], stack)
                            elif special_bracket_block_content: new_comm.secopt, stack = parse_blocks(special_bracket_block_content, stack)
                            stack.theorems.append(new_comm)
                        else: 
                            stack.error += 'ERROR in {stack.filename}: incomplete newtheorem declaration.\n'
                    
                    ###########################################################################
                    ##
                    ## NOW TREAT ALL ENVIRONMENT CALLS
                    ## distinguish several special environments
                    elif command_name == 'begin':
                        if declared_command_name:
                            # identify the content of the environment and add it as the final brace block
                            if match_environment(strg[environment_start:], declared_command_name):
                                env_block_end = match_environment(strg[end:], declared_command_name) 
                                brace_block_content.append(strg[end:end+env_block_end])
                                end = end + env_block_end + len(f'\\end{declared_command_name}') + 2 
                                start = end
                                ################################################################
                                # ALL INSTANCES OF THE CLASS Table = tabular, tabular*, tabbing etc.
                                if declared_command_name in globals() and issubclass(globals()[declared_command_name], Table):
                                    block = DocNode()
                                    if issubclass(globals()[declared_command_name], tabular):
                                        # create environment:
                                        comm = globals()[declared_command_name](declared_command_name)
                                        if issubclass(globals()[declared_command_name], tabular_):
                                            if len(brace_block_content) == 3:
                                                block, stack = parse_blocks(brace_block_content[0], stack)
                                                comm.reqargs[0] = block
                                                decl = brace_block_content[1]
                                                cont = brace_block_content[2]
                                            else:
                                                block, stack = parse_blocks('', stack)
                                                comm.reqargs[0] = block
                                                decl = ''
                                                cont = ''
                                        else:
                                            if len(brace_block_content) == 2:
                                                decl = brace_block_content[0]
                                                cont = brace_block_content[1]
                                            # This second case covers all tabular environments without declarations, e.g., transcols and twocols
                                            elif len(brace_block_content) == 1:
                                                decl = ''
                                                cont = brace_block_content[0]
                                            else: 
                                                decl = ''
                                                cont = ''

                                        # parse the column definitions of tabular
                                        col = 0
                                        
                                        while (colm := regex.match(r'\s*((?:>?\{.*?\})?)\s*((?:\@\{.*?\})*)\s*([\|]*\s*)\s*([lrcp\*]{1})(?:\{(.*?)\})?\s*((?:>?\{.*?\})?)\s*([\|]*\s*)\s*((?:\@\{.*?\})*)\s*',decl)):
                                            cold = Coldef()
                                            comm.coldef.append(cold)
                                            # the standard case: group is identified as l r c or p
                                            if colm.group(4) in ['l', 'r', 'c', 'p']:
                                                # ignore cell width for p cols 
                                                if colm.group(4) == 'p': comm.coldef[col].align = 'l'
                                                else: comm.coldef[col].align = colm.group(4)
                                                # check if there is a border on the left side of the cell:
                                                if colm.group(3): 
                                                    if colm.group(3).strip() == '||': comm.coldef[col].left_style = 'double'
                                                    elif colm.group(3).strip() == '|': comm.coldef[col].left_style = 'solid'
                                                else:  
                                                    if col > 0: 
                                                        if comm.coldef[col-1].right_style == 'double': comm.coldef[col].left_style = 'double'
                                                        elif comm.coldef[col-1].right_style == 'solid': comm.coldef[col].left_style = 'solid'
                                                        elif comm.coldef[col-1].right_style == 'none': comm.coldef[col].left_style = 'none'
                                                    else: 
                                                        comm.coldef[col].left_style = 'none'
                                                # check if there is a border on the right side of the cell: 
                                                if colm.group(7):
                                                    if colm.group(7).strip() == '||': comm.coldef[col].right_style = 'double'
                                                    elif colm.group(7).strip() == '|': comm.coldef[col].right_style = 'solid'
                                                else:
                                                    comm.coldef[col].right_style = 'none'

                                            ################################## other cases are not properly treatet yet:
                                            ############ might be implemented later (or never)
                                            else: 
                                                pass                                            
                                            decl = decl[colm.end():]
                                            col += 1
                                        
                                        # parse the content definition of tabular
                                        # parse a single table row:
                                        def parse_tablerow(rowtext, comm, stack):
                                            rowob = list()
                                            coll = 0
                                            while not find_end(rowtext, '&') == None:
                                                cell = Cell('cell')
                                                # if there is any rowspan cell in a previous row that still has an effect here:
                                                if coll in comm.rowspans:
                                                    if comm.rowspans[coll] < 2: del comm.rowspans[coll]
                                                    else: 
                                                        # decrement the dict value and jump over the column
                                                        comm.rowspans[coll] -= 1
                                                        coll += 1
                                                # multicolumn and multirow cells                                           
                                                if (cellcont := regex.match(r'\s*\\(multicolumn|multirow){1}\{(.*?)\}\{(.*?)\}', rowtext)):
                                                    celltextend = find_end(rowtext[cellcont.end():], '}')

                                                    if celltextend != None: 
                                                        celltext = rowtext[cellcont.end() + 1:cellcont.end() + celltextend]
                                                        celltext.strip('{}')
                                                        rowtext = rowtext[cellcont.end() + celltextend:]
                                                        celltextend = find_end(rowtext, '&')
                                                        if isinstance(celltextend, int): rowtext = rowtext[celltextend:]
                                                        else: 
                                                            print('ATTENTION: something wrong with cellend in multirow or multicolumn cell!')
                                                            rowtext = ''
                                                        if int(cellcont.group(2)) > 0:
                                                            if cellcont.group(1) == 'multicolumn':
                                                                cell.colspan = int(cellcont.group(2))
                                                                block, stack = parse_blocks(celltext, stack)
                                                                arg = Argument()
                                                                arg.content = block
                                                                cell.reqargs.append(arg)
                                                                if (match := regex.match(r'([\|]*)(l|r|c)([\|]*)', cellcont.group(3).strip())):
                                                                    cell.align = match.group(2)
                                                                    if match.group(1) and match.group(1) == '||': cell.left_style = 'double'
                                                                    elif match.group(1) and match.group(1) == '|': cell.left_style = 'solid'
                                                                    else: cell.left_style = 'none'
                                                                    if match.group(3) and match.group(3) == '||': cell.right_style = 'double'
                                                                    elif match.group(3) and match.group(3) == '|': cell.right_style = 'solid'
                                                                    else: cell.left_style = 'none'
                                                                if len(comm.coldef) > coll:
                                                                    cell.top_style = comm.coldef[coll].top_style
                                                                    cell.bottom_style = comm.coldef[coll].bottom_style
                                                                rowob.append(cell)
                                                                coll += int(cellcont.group(2))
                                                            # THIS FIRST NEEDS TO BE TESTED ON A CONCRETE EXAMPLE:
                                                            # rowspan takes a width as a second argument and not an align !!! ....
                                                            else:
                                                                cell.rowspan = int(cellcont.group(2))
                                                                comm.rowspans[coll] = cell.rowspan
                                                                block, stack = parse_blocks(celltext, stack)
                                                                arg = Argument()
                                                                arg.content = block
                                                                cell.reqargs.append(arg)
                                                                if (match := regex.match(r'([\|]*)(l|r|c)([\|]*)', cellcont.group(3).strip())):
                                                                    cell.align = match.group(2)
                                                                    if match.group(1) and match.group(1) == '||': cell.left_style = 'double'
                                                                    elif match.group(1) and match.group(1) == '|': cell.left_style = 'solid'
                                                                    else: cell.left_style = 'none'
                                                                    if match.group(3) and match.group(3) == '||': cell.right_style = 'double'
                                                                    elif match.group(3) and match.group(3) == '|': cell.right_style = 'solid'
                                                                    else: cell.left_style = 'none'
                                                                if len(comm.coldef) > coll:
                                                                    cell.top_style = comm.coldef[coll].top_style
                                                                    cell.bottom_style = comm.coldef[coll].bottom_style
                                                                rowob.append(cell)
                                                                coll += 1
                                                    else: 
                                                        print('ERROR: multirow or multicolumn cell content does not end!')
                                                        stack.error += 'ERROR: multirow or multicolumn cell content does not end!\n'
                                                        celltextend = find_end(rowtext, '&')
                                                        block, stack = parse_blocks('ERROR: multirow or multicolumn cell does not end', stack)
                                                        arg = Argument()
                                                        arg.content = block
                                                        cell.reqargs.append(arg)
                                                        rowob.append(cell)
                                                        coll += 1
                                                        rowtext = rowtext[celltextend:]
                                                # normal cell:
                                                else:
                                                    celltextend = find_end(rowtext, '&') 
                                                    if celltextend != None: 
                                                        celltext = rowtext[:celltextend - 1]
                                                        rowtext = rowtext[celltextend:]
                                                        block, stack = parse_blocks(celltext, stack)
                                                        arg = Argument()
                                                        arg.content = block
                                                        cell.reqargs.append(arg)
                                                        if len(comm.coldef) > coll:
                                                            cell.align = comm.coldef[coll].align
                                                            cell.left_style = comm.coldef[coll].left_style
                                                            cell.right_style = comm.coldef[coll].right_style
                                                            cell.top_style = comm.coldef[coll].top_style
                                                            cell.bottom_style = comm.coldef[coll].bottom_style
                                                        rowob.append(cell)
                                                        coll += 1

                                                    else: 
                                                        print('ATTENTION: something wrong with cellend tabular environment!')
                                                        celltext = ''
                                                        rowtext = ''


                                            return rowob, stack
                                        
                                        margtop = 'none'
                                        # first remove all \par that indicate empty lines which must be ignored: 
                                        cont = cont.replace('\\par','').strip()
                                        if (match := regex.match(r'\s*((?:\\hline|\\cline\{\d*\-\d*\})+)', cont)):
                                            # DEAL WITH HLINES = match.group(1)
                                            # NOT YET able to deal with \cline stuff
                                            cont = cont[match.end():]
                                            if match.group(1) == '\\hline\\hline': margtop = 'double'
                                            elif match.group(1) == '\\hline': margtop = 'solid'

                                        # conditionally add an instance of \\VALEPlb to ensure that the final line is also parsed
                                        pattern = r'\\(hline|VALEPlb|par|cline(\{.*\})?|VALEPlb(\{.*\})?)\s*$'
                                        match = regex.search(pattern, cont)
                                        if not match: cont += '\\VALEPlb'
                                                                                
                                        # attention: currentl<y ignores vertical space specifications in \\
                                        rowend = 0
                                        while (rowend := find_end(cont, '\\VALEPlb')):
                                            rowend -= 8
                                            row = regex.match(r'\\VALEPlb[\*]?(?:\{\})?(?:\[(.*?)\])?\s*((?:\\hline|\\cline\{\d*\-\d*\})*)\s*',cont[rowend:])
                                            rowtext = cont[:rowend]
                                            # group 1 = vertical space, group 2 = hlines
                                            for cold in comm.coldef: cold.top_style = margtop
                                            if row.group(2):
                                                if row.group(2) == '\\hline\\hline': 
                                                    for cold in comm.coldef: cold.bottom_style = 'double'
                                                    margtop = 'double'
                                                elif row.group(2) == '\\hline': 
                                                    for cold in comm.coldef: cold.bottom_style = 'solid'
                                                    margtop = 'solid'
                                            else:
                                                for cold in comm.coldef: cold.bottom_style = 'none'
                                                margtop = 'none'
                                            # ensure that the final cell is also parsed:
                                            rowtext += '&' 
                                            rowob, stack = parse_tablerow(rowtext, comm, stack)
                                            comm.rows.append(rowob)
                                            cont = cont[rowend + row.end():]

                                        doc_list.elements.append(comm)
                                        
                                    elif issubclass(globals()[declared_command_name], tabbing):
                                        comm = tabbing('tabbing')
                                        if len(brace_block_content) > 0:
                                            cont = brace_block_content[-1].strip()
                                        else: 
                                            cont = ''
                                        # ensure that it also reads the final row: 
                                        cont = cont.replace('\\par','').strip()
                                        if not cont[-8:] == '\\VALEPlb': cont += '\\VALEPlb'
                                        # parse the content definition of tabbing
                                        # attention: ignores vertical space specifications in \\
                                        indent = 0   # this is needed for \+ and \- declarations
                                        while (row := regex.match(r'(.*?)(\\VALEPlb\*?(?:\[.*?\])?\s*|\\VALEPlb\{\}\s*|\\kill\s*)(?:\{\})?', cont)):
                                            kill = False
                                            # set the flag 'hidden' to all cells of a row if the row ends with '\kill':
                                            if row.group(2).strip()[-5:] == '\\kill': kill = True
                                            rowtext = row.group(1).strip()
                                            # ensure that it also reads the final cell:
                                            if not rowtext[-2:] in ['\\=', '\\>', '\\<', '\\+', '\\-']: rowtext += '\\>'
                                            rowob = list()
                                            i = 0
                                            # manage \+ indentations:
                                            while i < indent:
                                                cell = Cell('cell')
                                                arg = Argument()
                                                cell.reqargs.append(arg)
                                                rowob.append(cell)
                                                i += 1

                                            # ignores \' and \` refinements and deals with them as if they were normal tabstopps
                                            while regex.match(r'(.*?)\\[\=\>\<\+\-\'\`]{1}',rowtext):
                                                celltext = regex.match(r'(.*?)\\([\=\>\<\+\-\'\`]{1})',rowtext)
                                                if celltext.group(2) == '+': indent += 1
                                                if celltext.group(2) == '-': indent -= 1
                                                if celltext.group(2) == '<' and len(rowob) > 0: rowob.pop()
                                                if celltext.group(2) == "'": pass # nobody really needs this, so do not implement yet
                                                if celltext.group(2) == "`": pass # same here
                                                if celltext.group(2) not in ['+', '-']:    
                                                    block, stack = parse_blocks(celltext.group(1), stack)
                                                    cell = Cell('cell')
                                                    arg = Argument()
                                                    arg.content = block
                                                    cell.reqargs.append(arg)
                                                    if kill: cell.hidden = True    
                                                    rowob.append(cell)
                                                rowtext = rowtext[celltext.end():]

                                            comm.rows.append(rowob)
                                            cont = cont[row.end():]
                                        doc_list.elements.append(comm)
                                
                                #####################################################################
                                # ALL OTHER DEFINED ENVIRONMENTS, i.e. 
                                # environments that are identified as a subclass of Environment:
                                elif declared_command_name in globals() \
                                and issubclass(globals()[declared_command_name], Environment) \
                                and not (issubclass(globals()[declared_command_name], Math) and stack.math):
                                    # create environment:
                                    comm = globals()[declared_command_name](declared_command_name)
                                    # append environment if there are enough required arguments:
                                    if len(comm.reqargs) == len(brace_block_content) - 1:
                                        if issubclass(comm.__class__, Formula): stack.math = True
                                        i = 0
                                        while i < len(comm.optargs):
                                            if len(bracket_block_content) > i: 
                                                block, stack = parse_blocks(bracket_block_content[i], stack)
                                                comm.optargs[i].content = block
                                            i += 1
                                        i = 0
                                        while i < len(brace_block_content) - 1:
                                            block, stack = parse_blocks(brace_block_content[i], stack)
                                            comm.reqargs[i].content = block
                                            i += 1
                                        # add environment content as additional argument:
                                        arg = Argument()
                                        arg.name = 'envcontent'
                                        block, stack = parse_blocks(brace_block_content[i], stack)
                                        arg.content = block
                                        comm.reqargs.append(arg)
                                        doc_list.elements.append(comm)
                                        if len(bracket_block_content) > len(comm.optargs):
                                            stack.error += f'WARNING in {stack.filename}: environment {declared_command_name} contains too many optional arguments. excess arguments are ignored.'
                                            print(f'WARNING: environment {declared_command_name} contains too many optional arguments. excess arguments are ignored.')
                                        stack.math = False
                                    # not enough or too many required arguments:
                                    else:
                                        stack.error += f'ERROR in {stack.filename}: ennvironment {declared_command_name} contains too many or not enough required arguments!'
                                        print(f'ERROR: ennvironment {declared_command_name} contains too many or not enough required arguments!')
                                # ALL Math environments inside of formulas are left unchanged:
                                elif declared_command_name in globals() \
                                    and issubclass(globals()[declared_command_name], Math) and stack.math:
                                    doc_list.elements.append(f'\\begin{{{declared_command_name}}}')
                                    i = 0
                                    while i < len(comm.optargs):
                                        if len(bracket_block_content) > i: 
                                            doc_list.elements.append(f'[{bracket_block_content[i]}]')
                                        i += 1
                                    i = 0
                                    while i < len(brace_block_content) - 1:
                                        doc_list.elements.append(f'{{{brace_block_content[i]}}}')
                                        i += 1
                                    block, stack = parse_blocks(brace_block_content[i], stack)
                                    doc_list.elements.append(block)

                                ######################################################################
                                #
                                # ALL UNKNOWN ENVIRONMENTS: 
                                else:
                                    if stack.math == False: 
                                        stack.unknown[declared_command_name] = 'environment'
                                    comm = UnknownEnvironment(declared_command_name)
                                    i = 0 
                                    while i < len(bracket_block_content):
                                        block, stack = parse_blocks(bracket_block_content[i], stack)
                                        arg = Argument()
                                        arg.content = block
                                        comm.optargs.append(arg)
                                        i += 1
                                    i = 0
                                    while i < len(brace_block_content):
                                        block, stack = parse_blocks(brace_block_content[i], stack)
                                        arg = Argument()
                                        arg.content = block
                                        comm.reqargs.append(arg)
                                        i += 1
                                    doc_list.elements.append(comm)
                            else: 
                                stack.error += f'ERROR in {stack.filename}: environment {declared_command_name} does not end!\n'
                                print(f'ERROR: environment {declared_command_name} does not end!')
                        else:
                            stack.error += 'ERROR in {stack.filename}: incorrect environment definition!'
                            print('ERROR: incorrect environment definition!')
                    
                    #################################################################################
                    ## 
                    ## ALL COMMAND CALLS = commands that either have arguments or are unknown
                    else:
                        ############################################################################
                        # DEFINED COMMANDS 
                        # = command_name refers to a defined subclass of Command: 
                        if command_name_no_star in globals() \
                        and issubclass(globals()[command_name_no_star], Command) \
                        and not (issubclass(globals()[command_name_no_star], Math) and stack.math):
                            
                            ##### first catch all include and input commands:
                            if command_name_no_star == 'include' or command_name_no_star == 'input':
                                file_name = brace_block_content[0]
                                if isinstance (file_name, str):
                                    try:
                                        string, stack = start_parse(file_name, stack)
                                        include_node, stack = parse_blocks(string, stack)
                                        doc_list.elements.append(include_node)
                                    except:
                                        er = f'ERROR in {stack.filename}: inclusion of file {file_name} failed!'
                                        print(er)
                                        stack.error += f'{er}\n'                                
                            ##### second catch all \ensuremath commands:
                            elif issubclass(globals()[command_name_no_star], EnsureMath):
                                if len(brace_block_content) > 0:
                                    # either the content is already part of a formula
                                    if stack.math:
                                        block, stack = parse_blocks(brace_block_content[0], stack)
                                        doc_list.elements.append(block)
                                    # or the content stands in text mode such that a formula must be created:
                                    else:
                                        stack.math = True
                                        block, stack = parse_blocks(brace_block_content[0], stack)
                                        comm = math('math')
                                        arg = Argument()
                                        arg.content = block
                                        comm.reqargs.append(arg)
                                        doc_list.elements.append(comm)
                                        stack.math = False
                                else:
                                    print('ERROR: ensuremath command has no argument!')
                                    error += 'ERROR: ensuremath command has no argument!\n'
                            else:
                                # create command
                                comm = globals()[command_name_no_star](command_name_no_star)
                                # append command if there are enough required arguments:
                                i = 0
                                while i < len(comm.optargs):
                                    if len(bracket_block_content) > i: 
                                        block, stack = parse_blocks(bracket_block_content[i], stack)
                                        comm.optargs[i].content = block
                                    i += 1
                                # throw a warning if there are further bracket blocks and ignore them afterwards
                                if len(bracket_block_content) > len(comm.optargs):
                                    stack.error += f'WARNING in {stack.filename}: command {command_name} contains too many optional arguments. excess arguments are ignored.'
                                    print(f'WARNING: command {command_name} contains too many optional arguments. excess arguments are ignored.')
                                i = 0
                                while i < len(brace_block_content):
                                    block, stack = parse_blocks(brace_block_content[i], stack)
                                    comm.reqargs[i].content = block
                                    i += 1  
                                # deal with normal \item commands who do not deliver their content as an argument
                                if issubclass(comm.__class__, item):
                                    match = regex.search(fr'\\{comm.name}|\\begin{{|\\end{{', strg[end:])
                                    if match: itemend = match.start()
                                    else: itemend = len(strg) - end
                                    itemcontent = strg[end:end + itemend]
                                    block, stack = parse_blocks(itemcontent, stack)
                                    arg = Argument()
                                    arg.content = block
                                    comm.reqargs.append(arg)
                                    end += itemend
                                    start = end

                                if len(comm.reqargs) > 0:
                                    argstring = comm.reqargs[-1].content.render(stack.jinja)

                                #########################
                                # DEAL WITH COUNTERS AND OTHER SPECIAL COMMANDS
                                # Section:
                                if issubclass(comm.__class__, Section):
                                    stack = comm.setcounter(stack)

                                # Note  
                                elif issubclass(comm.__class__, Note):
                                    stack = comm.setcounter(stack)
                                    comm_copy = comm
                                    if command_name_no_star[-4:] == 'text':
                                        stack.markednotes.append(comm_copy)
                                                                
                                # Counter
                                elif issubclass(comm.__class__, setcounter):
                                    if len(brace_block_content) > 1:
                                        value = int(brace_block_content[1])
                                        stack = comm.setcounter(value, stack)
                                    else: 
                                        counter = ''
                                        if len(brace_block_content) > 0: counter = brace_block_content[0]
                                        print(f'ERROR: setcounter {counter} command incomplete!')
                                        stack.error += f'ERROR: setcounter {counter} command incomplete!\n'

                                elif issubclass(comm.__class__, stepcounter):
                                    stack = comm.stepcounter(stack)

                                elif issubclass(comm.__class__, refstepcounter):
                                    stack = comm.stepcounter(stack)

                                elif issubclass(comm.__class__, addtocounter):
                                    if len(brace_block_content) > 1:
                                        value = int(brace_block_content[1])
                                        stack = comm.addtocounter(value, stack)
                                    else: 
                                        counter = ''
                                        if len(brace_block_content) > 0: counter = brace_block_content[0]
                                        print(f'ERROR: setcounter {counter} command incomplete!')
                                        stack.error += f'ERROR: setcounter {counter} command incomplete!\n'

                                elif issubclass(comm.__class__, title):
                                    stack.title = argstring
                                    stack.madetitle = argstring
                                elif issubclass(comm.__class__, author):
                                    stack.author = argstring
                                    stack.madetitle += ". " + stack.author 
                                elif issubclass(comm.__class__, date):
                                    stack.date = argstring
                                    stack.madetitle += ". " + stack.date
                                
                                # ONLY APPEND THE COMMAND IF IT IS NOT LaTeXOnly:
                                if not issubclass(comm.__class__, LaTeXOnly):
                                    doc_list.elements.append(comm)
                        
                        # COMMANDS in Formula environments, which are declared as Math:
                        elif command_name_no_star in globals() \
                        and issubclass(globals()[command_name_no_star], Math) and stack.math:
                            doc_list.elements.append(f'\\{command_name_no_star}')
                            i = 0
                            while i < len(bracket_block_content):
                                doc_list.elements.append(f'[{bracket_block_content[i]}]')
                                i += 1
                            i = 0
                            while i < len(brace_block_content):
                                block, stack = parse_blocks(brace_block_content[i], stack)
                                doc_list.elements.append('{')
                                doc_list.elements.append(block)
                                doc_list.elements.append('}')
                                i += 1  


                        ##############################################################################
                        #
                        # UNKNOWN COMMAND: 
                        # append any blocks of brackets and braces that immediately follow the command
                        else: 
                            # put the command in the dict of unknown commands
                            if stack.math == False:
                                if len(brace_block_content) > 0 or len(bracket_block_content) > 0:
                                    stack.unknown[command_name] = 'command'
                                else:
                                    stack.unknown[command_name] = 'text macro'
                            comm = UnknownCommand(command_name)
                            i = 0
                            while i < len(bracket_block_content):
                                if len(bracket_block_content) > i: 
                                    block, stack = parse_blocks(bracket_block_content[i], stack)
                                    arg = Argument()
                                    arg.content = block
                                    comm.optargs.append(arg)
                                i += 1
                            i = 0
                            while i < len(brace_block_content):
                                block, stack = parse_blocks(brace_block_content[i], stack)
                                arg = Argument()
                                arg.content = block
                                comm.reqargs.append(arg)
                                i += 1
                            doc_list.elements.append(comm)
                                        
            ##########################################################################################
            # 
            # SOME IRREGULAR COMMANDS THAT WERE NOT TREATED EARLIER
            # irregular commands are macros of the form \NONALPHA
            #
            # INLINE FORMULAS \( \):
            elif len(strg) > end + 1 and strg[end+1] == '(':
                if end > start:
                    # strings are added as simple string nodes to the DocNode
                    doc_list.elements.append(minimize_whitespace(strg[start:end], stack))
                length = 2
                stack.math = True
                if match_begin_end(strg[end:],'\\(','\\)'):
                    length = match_begin_end(strg[end:],'\\(','\\)')
                    comm = math('math')
                    content = strg[end+2:end+length-2]
                    block, stack = parse_blocks(content, stack)
                    arg = Argument()
                    arg.content = block
                    comm.reqargs.append(arg)
                    comm.type = 'math'
                    doc_list.elements.append(comm)
                else: 
                    stack.error += f"ERROR in {stack.filename}: formula {strg} \\( does not end.\n" 
                end += length
                start = end
                stack.math = False
            ###########################################################################################
            # DISPLAYED FORMULAS \[ \]
            elif len(strg) > end + 1 and strg[end+1] == '[':
                if end > start:
                    # strings are added as simple string nodes to the DocNode
                    doc_list.elements.append(minimize_whitespace(strg[start:end], stack))
                length = 2
                stack.math = True
                if match_begin_end(strg[end:],'\\[','\\]'):
                    length = match_begin_end(strg[end:],'\\[','\\]')
                    comm = displaymath('displaymath')
                    content = strg[end+2:end+length-2]
                    block, stack = parse_blocks(content, stack)
                    arg = Argument()
                    arg.content = block
                    comm.reqargs.append(arg)
                    comm.type = 'displaymath'
                    doc_list.elements.append(comm)
                else: 
                    stack.error += "ERROR in {stack.filename}: formula \\[ does not end" 
                end += length
                start = end
                stack.math = False
            #########################################################################################
            # 
            # SIMPLY \whitespace which produces whitespace after a command
            elif len(strg) > end + 1 and strg[end+1] == ' ':
                doc_list.elements.append(' ')
                end += 2
                start = end
            ########################################################################################
            # ANYTHING ELSE IS TREATED AS NORMAL TEXT:
            else:
                # is normal text, just increment 'end':
                end += 2
        else:
            # is normal text, just increment 'end':
            end += 1 
        ############################################################################################
        # SPECIFY A COUNTER FOR VERY LARGE DOCUMENTS:
        if len(strg) > 1000_000 and end/10_000 == round(end/10_000): 
            print(f'PARSING: {end} of {len(strg)} chars done ...')
    ################################################################################################
    # APPEND REMAINING PORTIONS OF TEXT TO THE DOCNODE
    if end > start:
        # a string is still on the stack
        doc_list.elements.append(minimize_whitespace(strg[start:end], stack))
    
    return doc_list, stack
# end parse_blocks
#
################################################################################


