
import regex
import inspect




'''
CLASS DOCNODE:
The main class is DocNode, which is supposed to comprise of a list of elements of any allowed types
This includes two Categories:
    - Strings
    - Commands and all kinds of subclasses of the Command class

CLASS DOCSTACK:
A list of declarations that must be filled in parallel with DocNode. Declarations are stored in DocStack only

CLASS NEWCOMMAND:
A declaration: newcommand, newenvironment, newtheorem, newcounter

several classes derived from NewCommand

CLASS COMMAND: 
Any command, including environments

several classes derived from Command

'''


def non_empty(string):
    '''returns False if the object is either not a string or empty
    and True if the object is a non empty string'''
    if not isinstance(string, str): return False
    elif regex.search(r'\S+', string): return True
    else: return False


# 
class DocNode():
    '''MAIN CLASS: a list of text-snippets, blocks, and commands
    each LaTeX string/DocNode is parsed into sections of mere text 
    and commands/environments that again 
    can have arguments being treated as LaTeX strings/DocNodes'''
    
    def __init__(self):
        self.elements = list()

    def expand(self):
        '''returns a string that prints the object in a (pseudo) LaTeX notation
        is not up to date '''
        string = ''
        for element in self.elements:
            if isinstance(element, Command):
                if element.name == 'par':
                    string += f'{element.expand()}\n'
                else:
                    string += f'{element.expand()}'
            # an element that is a DocNode is a pseudoenvironment, i.e. {LaTeX in braces}
            elif isinstance(element, DocNode):
                string += f'{element.expand()}'
            elif isinstance(element, str):
                string += str(element)
            else:
                print(f'ERROR: type of {element} could not be identified!\n')
        return string

    def flatten_docnodes(self):
        '''appends each element of a docnode list that is a docnode in itself
        by means of its individual elements and thus flattens the docnode
        does not include tables but only opt and reqargs of Commands'''
        flattened_elements = []
        if self.elements:
            for item in self.elements:
                if isinstance(item, DocNode) and isinstance(item.elements, list):
                    flattened_elements.extend(item.flatten_docnodes())
                elif isinstance(item, str) and non_empty(item):
                    flattened_elements.append(item)
                elif isinstance(item,Command):
                    i = 0
                    while i < len(item.optargs):
                        if isinstance(item.optargs[i].content, DocNode) and isinstance(item.optargs[i].content.elements, list):
                            item.optargs[i].content.elements = item.optargs[i].content.flatten_docnodes()
                        i += 1
                    i = 0
                    while i < len(item.reqargs):
                        if isinstance(item.reqargs[i].content, DocNode) and isinstance(item.reqargs[i].content.elements, list):
                            item.reqargs[i].content.elements = item.reqargs[i].content.flatten_docnodes()
                        i += 1
                    flattened_elements.append(item)
        return flattened_elements

    # BUG: identifies also isolated SystemCommand objects as paragraphs
    def adjust_paragraphs(self):
        '''identifies all paragraphs and marks them with a Par object at 
        the beginning and the end. The Par object uses the parameters parstart
        and parend for this'''
        i = 0
        while i < len(self.elements):
            # paragraphs require the existence of at least one token of str, Format, or math
            # but they can in addition to this also contain other Inline stuff
            if isinstance(self.elements[i], str) or isinstance(self.elements[i], Format) or isinstance(self.elements[i], math):
                par_started = False
                j = 1
                while i - j > 0:
                    test_item = self.elements[i - j]
                    if isinstance(test_item, Par):
                        par_started = True
                        self.elements[i - j].parstart = True
                        j = i
                    elif non_empty(test_item) or isinstance(test_item, Format) or isinstance(test_item, math) or isinstance(test_item, Inline):
                        j += 1
                    else: 
                        j = i
                if par_started == False:
                    comm = par('par')
                    comm.parend = False
                    comm.parstart = True
                    self.elements.insert(i, comm)
                    i += 1
                is_string_or_format = True
                i += 1
                # (1) follow the list until the format/str sequence ends
                # (2) either with a par object or with a division object or because the entire block ends
                # and (3) ensure that at the end of the paragraph is a par object
                while is_string_or_format:
                    if i < len(self.elements) and (non_empty(self.elements[i]) or isinstance(self.elements[i], Format) or isinstance(self.elements[i], math) or isinstance(self.elements[i], Inline)):
                        i += 1
                    else: 
                        is_string_or_format = False
                # if the next object after the paragraph ia a par then identify it as "parend"
                # otherwise insert a par exactly here
                if i < len(self.elements) and isinstance(self.elements[i], Par): self.elements[i].parend = True
                else:
                    comm = par('par')
                    comm.parend = True
                    comm.parstart = False # is changed later, if applicable
                    self.elements.insert(i,comm)

            # the purpose of this is only to catch displaced par objects and remove them
            elif isinstance(self.elements[i], Par):                
                self.elements[i].parend = False
                par_redundant = False
                j = 1
                while i - j > 0:
                    test_item = self.elements[i - j]
                    if non_empty(test_item) or isinstance(test_item, Format) or isinstance(test_item, math):
                        self.elements[i].parend = True
                        j = i
                    elif isinstance(test_item, Inline):
                        j += 1
                    elif isinstance(test_item, Command):
                        j = i
                    elif isinstance(test_item, Par):
                        par_redundant = True
                        j = i
                    else: 
                        j += 1
                        print('ERROR: something wrong in adjust_paragraphs - unidentified type.')
                self.elements[i].parstart = False
                j = 1
                while i + j < len(self.elements):
                    test_item = self.elements[i + j]
                    if non_empty(test_item) or isinstance(test_item, Format) or isinstance(test_item, math):
                        self.elements[i].parstart = True
                        j = len(self.elements)
                    elif isinstance(test_item, Inline):
                        j += 1
                    else:
                        j = len(self.elements)
                if par_redundant or (self.elements[i].parend == False and self.elements[i].parstart == False):
                    del self.elements[i]
                    i -= 1
                
                i += 1
                

            # only divisions may contain paragraphs:
            elif isinstance(self.elements[i], Division) and len(self.elements[i].reqargs) > 0:
                self.elements[i].reqargs[-1].content.adjust_paragraphs()
                i += 1
            else:
                i += 1




    def normalize(self, jinja):
        '''return a string that removes all commands and markup 
        and represents just plain text (is not up to date)'''
        string = ''
        for element in self.elements:
            if isinstance(element, Command): string += f'{element.normalize(jinja)}'
           # an element that is a DocNode is a pseudoenvironment, i.e. {LaTeX in braces}
            elif isinstance(element, DocNode): string += f'{element.normalize(jinja)}'
            elif isinstance(element, str): string += self.normalize_string(element)
        return string
    
    def normalize_string(self, string):
        '''removes all strings from delete and 
        replaces all strings from make_whitespace with a single whitespace'''
        delete = ['&shy;', ]
        make_whitespace = ['&ThinSpace', ]
        for element in delete: string = string.replace(element, '')
        for element in make_whitespace: string = string.replace(element, ' ')
        return string

    def find(self,name):
        '''returns the first object in the docnode with classname "name"
        does not include tables but only Commands 
        and opt and req args of Commands
        returns False if no such node exists'''
        node = False
        for elem in self.elements:
            if isinstance(elem.__class__, DocNode): 
                node = elem.find(name)
                if node: return node
            if issubclass(elem.__class__, Command):
                if elem.name == name: return elem
                for optarg in elem.optargs:
                    node = optarg.content.find(name)
                    if node: return node
                for reqarg in elem.reqargs:
                    node = reqarg.content.find(name)
                    if node: return node
        return node

    def find_object(self,object):
        '''similar to find but also searches Table objects'''
        node = False
        for elem in self.elements:
            if issubclass(elem.__class__, DocNode): 
                node = elem.find_object(object)
                if node: return node
            elif isinstance(elem, Table):
                for row in elem.rows:
                    for cell in row:
                        cell.reqargs[-1].content.find_object(object)
            elif issubclass(elem.__class__, Command):
                if elem.__class__ == object: return elem
                for optarg in elem.optargs:
                    node = optarg.content.find_object(object)
                    if node: return node
                for reqarg in elem.reqargs:
                    node = reqarg.content.find_object(object)
                    if node: return node
        return node

    def supplement_note_marks(self,stack):
        '''adds to each NOTElabel object the content of the 
        corresponding NOTEtext object'''
        for elem in self.elements:
            if isinstance(elem, Note) and elem.notetype == 'label':
                if elem.markednote < len(stack.markednotes):
                    number = elem.markednote
                    if len(stack.markednotes) > number:
                        comm = stack.markednotes[number]
                        if isinstance(comm, Note):
                            elem.reqargs[-1] = comm.reqargs[-1]
                        else:
                            print(f'ERROR: "{comm}" is not a Note object.')
                            stack.error += f'ERROR: "{comm}" is not a Note object.\n'
                    else: 
                        print('ERROR: probably a NOTElabel command without a corresponding NOTEtext command')
                else:
                    print(f'ERROR: marked note {elem.name} no {elem.markednote} is not available in stack.markednotes!')
            elif isinstance(elem, DocNode):
                elem.supplement_note_marks(stack)
            elif isinstance(elem, Table):
                for row in elem.rows:
                    for cell in row:
                        cell.reqargs[-1].content.supplement_note_marks(stack)
            elif isinstance(elem, Command):
                for optarg in elem.optargs:
                    optarg.content.supplement_note_marks(stack)
                for reqarg in elem.reqargs:
                    reqarg.content.supplement_note_marks(stack)
            elif isinstance(elem, str):
                pass
            

    def render(self, jinja):
        '''renders the object on the basis of a jinja dictionary: 
        strings are left unchanged, DocNodes are recursively included, 
        and Commands are rendered by means of 
        the render function of the Command class
        returns the rendered string'''
        string = ''
        for element in self.elements:
            if isinstance(element, Command):
                string += element.render(jinja) 
            elif isinstance(element, DocNode):
                string += element.render(jinja)
            elif isinstance(element, str):
                string += str(element)
            else:
                print(f'RENDERING ERROR: type of {element} could not be identified!\n')
        return string
    
    
    def render_document(self, template, doc_string):
        '''renders the final document on the basis of a string 
        previously rendered with self.render() and a JinjaDocument template
        returns the rendered string'''
        string = ''
        if isinstance(template,JinjaDocument):
            for element in template.doc.elements:
                if isinstance(element, str): string += element
                elif isinstance(element,JinjaExpression): 
                    if element.type == 'obj':
                        string += doc_string
                    elif element.type == 'att':
                        pass # to be implemented later
                    elif element.type == 'filter':
                        pass # to be implemented later
                elif isinstance(element,JinjaStatement):
                    pass # to be implemented later
                else:
                    string += ' [ERROR: incorrect expression in jinja document template] '
                    print(' [ERROR: incorrect expression in jinja document template] ')
            return string
        else:
            return 'ERROR: "template" is not a JinjaDocument object'

    def nonempty(self):
        '''returns True if the docnode contains either at least one nonempty string
        or at least one Command, otherwise it returns False'''
        if len(self.elements) == 0: return False
        for elem in self.elements:
            if str(elem) and not elem == '': return True
            elif not str(elem): return True
        return False

class DocStack():
    '''stack/variable with all declarations 
    this object carries several things through the entire parsing process: 
       - All kinds of declarations of commands, environments, counters, and theorems
       - Information about loaded packages and documentclasses
       - Information about default file openings and fileclosings
       - A list of unknown commands
       - An error/log stream
       - variables such as "math" that facilitate the parsing process
    '''
    # treat separately: \textgreater \textless \VALEPThinSpace \VALEPnbsp \VALEPlb \par
    # also: \/  \-   
    text_macros_general = {# standard characters:
        'textemdash': '—', 'textendash': '–', 'textcent': '¢', 'textdegree': '°',
        'textordmasculine': '°', 'textsection': '§', 'textsterling': '£', 
        'textasciitilde': '~', 'textasciicircum': '^','textunderscore': '_', 'textvisiblespace': '␣', 
        'textasteriskcentered': '⚹', 'textbraceleft*': '{', 'textbraceright*': '}',
        'textbullet': '•', 'textperiodcentered': '·', 'textellipsis*': '…', 'ldots': '…', 'dots': '…',
        # quotation marks: ###################################################################################
        'glqq': '„', 'grqq': '“', 'glq': '‚', 'grq': '‘', 
        'flqq': '«', 'guillemotleft': '«', 'guillemotright': '»', 'frqq': '»', 'flq': '‹', 'frq': '›', 
        'textquotedblleft': '“', 'textquotedblright': '”', 'textquoteleft': '‘', 'textquoteright': '’',
        # other special characters:
        'o': 'ø', 'O': 'Ø', 'l': 'ł', 'L': 'Ł', 'i': 'ı', 'j': 'ȷ', 'P': '¶', 'textparagraph*': '¶', 'ddag': '‡',
        'oe': 'œ', 'OE': 'Œ', 'ae': 'æ', 'AE': 'Æ', 'aa': 'å', 'AA': 'Å', 'ss': 'ß', 'SS': 'ẞ',
        'dh*': 'ð', 'DH*': 'Đ', 'DJ*': 'Đ', 'dj*': 'đ', #'NG*': '', 'ng*': '', 'TH*': '', 'th*': '',
        'S': '§', 'dag': '†', 'textbar': '|', 'texttrademark': '™', 'textexclamdown': '¡', 
        'textregistered': '®', 'cirlcedR': '®', 'textquestiondown': '¿', 'copyright': '©', 'textcopyright*': '©', 'textcopyright': '©',
        'euro': '€', 'EUR': '€', 'texteuro': '€', 'EURofc': '€', 'pounds': '£', 
        'checkmark': '✓', 'maltese': '✠', 'textdagger': '†', 'textonehalf': '½', 'textonequarter': '¼', 
        'textthreequarters': '¾', 'langle': '⟨', 'rangle': '⟩', 'lfloor': '⌊', 'rfloor': '⌋', 'lceil': '⌈',
        'rceil': '⌉', 'dq': '″', 'zoll': '″', # inch, second etc. U+2033
        # IPA:
        'esh': '​ʃ', 'schwa': 'ə',
        # PUT HERE FURTHER SYMBOLS, e.g. from the ca. 23000 symbols in 'The comprehensive LaTeX symbol list' 
        # https://tug.ctan.org/info/symbols/comprehensive/symbols-a4.pdf, pp. 8-18
        }
    
    # text macros to be ignored in math mode
    text_macros_special = {
        'VALEPlbrc': '{', 'VALEPrbrc': '}', 'VALEPand': '&amp;', 'VALEPhash': '#', 'VALEPpct': '%',
        'textdollar*': '$', 'textdollar': '$', 'textbackslash': '\\',
        }

    # text macros to be included only in math mode
    text_macros_special_math = {
        'VALEPlbrc': '\\{', 'VALEPrbrc': '\\}', 'VALEPand': '\\&', 'VALEPhash': '\\#', 'VALEPpct': '\\%',
        'VALEPnbsp': '~', 'VALEPThinSpace': '\\,',  'VALEPlb': '\\\\', 'emph': '\\mathit' # the emph replacement is more a test
        }

    text_macros = {**text_macros_special, **text_macros_general}

    text_macros_math = {**text_macros_special_math, **text_macros_general}

    def __init__(self):
        '''Declares all variables of the DocStack object:
            - cds = list of all command declarations
            - envs = list of all environment declarations
            - counters = dict of all counters (which will be immediately filled 
                with standard counters)
            - theorems = list of all theorems
            - packages = set of all packages that were already included
            - packagestring = string of all declarations from packages
            - fileopening = default fileopening
            - fileclosing = defult fileclosing
            - unknown = dict of all unknown macros

            - and a bunch of further important global variables
        '''
        self.cds = list()
        self.envs = list()
        self.counters = dict()
        # create and append counters:
        # sections
        i = -10
        while i < 21:
            new_count = newcounter('newcounter',f'section@{i}')
            self.counters[f'section@{i}'] = new_count
            if i > -10: self.counters[f'section@{i-1}'].incounters.append(f'section@{i}')
            i += 1
        ## get all subclasses of a class "classname"
        def get_sub_classes(cls):
            subclasses = []
            for subclass in cls.__subclasses__():
                subclasses.append(subclass)
                subclasses.extend(get_sub_classes(subclass))
            return subclasses
        # notes: each instance of Note
        classes = get_sub_classes(Note)
        for cls in classes:
            new_count = newcounter('newcounter', cls.__name__)
            self.counters[cls.__name__] = new_count
        # and a special counter for notes of the form \NOTEmark and \NOTEtext
        new_count = newcounter('newcounter', 'MarkedNotes')
        self.counters['MarkedNotes'] = new_count
        # enums, equations, figures, tables
        new_count = newcounter('newcounter', 'enumi')
        self.counters['enumi'] = new_count
        new_count = newcounter('newcounter', 'enumii')
        self.counters['enumii'] = new_count
        new_count = newcounter('newcounter', 'enumiii')
        self.counters['enumiii'] = new_count
        new_count = newcounter('newcounter', 'enumiv')
        self.counters['enumiv'] = new_count
        new_count = newcounter('newcounter', 'equation')
        self.counters['equation'] = new_count
        new_count = newcounter('newcounter', 'figure')
        self.counters['figure'] = new_count
        new_count = newcounter('newcounter', 'table')
        self.counters['table'] = new_count

        # global variables for theorems, packages, and declarations from packages
        self.theorems = list()
        self.packages = set()
        self.packagestring = ''
        # global variable for the file opening and closing
        self.fileopening = '\n\n\\begin{document}\n\n'
        self.fileclosing = '\n\n\\end{document}\n\n%% END DOCUMENT valeptex\n'
        # dict containing all unknown macros
        self.unknown = dict()

        #### SOME FURTHER GLOBAL VARIABLES:
        # author date and title of a document
        self.title = r"valep\(\mathsf{\TeX}\): conversion from \(\mathsf{\LaTeX}\) to HTML"
        self.author = ""
        self.date = ""
        self.madetitle = ""
        # specifiy the depth of the table of contents (= \tocdepth)
        self.tocdepth = 4
        # error stream:
        self.error = ''
        # A list that contains the content of all notes of the form \NOTEmark and \NOTEtext
        self.markednotes = list()
        self.markednotes.append('')  # the first meaningful entry is going to have index '1'
        # set to 'True' while parsing math content:
        self.math = False 
        # jinja specification and template string
        self.jinja = JinjaSpec('', self)
        self.template_string = ''
        # name of the file just processed:
        self.filename = ''
        # flags set in comparsion with arguments:
        self.normalize = False
        self.writelog = True
        self.writeunknown = False
        self.defaultpreamble = True
        self.preamblemode = True
        self.firstdoc = True
        self.carnapedition = False
        # output file format: default is .html
        self.output_format = 'html'
        
    def reset_counters(self):
        '''resets all counters of the counter dict'''
        for counter in self.counters: self.counters[counter].reset()
    
    def reset_transient_elements(self):
        '''resets all those transient elements that become refilled 
        for each document in batch mode'''
        self.title = r"valep\(\mathsf{\TeX}\): conversion from \(\mathsf{\LaTeX}\) to HTML"
        self.author = ""
        self.date = ""
        self.madetitle = ""
        # A list that contains the content of all notes of the form \NOTEmark and \NOTEtext
        self.markednotes = list()
        self.markednotes.append('')  # the first meaningful entry is going to have index '1'

    def is_defined_textmacro(self, command_name):
        '''checks if a command of class command_name is a defined text macro
        excludes all text macros being mentioned in the list exceptions
        '''
        exceptions = ['lt', 'gt']
        for comm in self.cds:
            if comm.name == command_name and comm.narg == 0 and not command_name in exceptions: return True
        return False

    def get_definition_textmacro(self, command_name):
        '''returns the definition as a DocNode object if it is 
        a defined texmacro (i.e. macro without arguments)
        returns False otherwise
        '''        
        for comm in self.cds:
            if comm.name == command_name: return comm.begdef
        return False



    def expand(self):
        '''expands the most important variables and returns them as a string'''
        string = ''
        if self.cds: string = '\nLIST OF DECLARATIONS:\n(1) Commands:\n'
        for element in self.cds:
            if isinstance(element, NewCommand): string += f'{element.expand()}\n'
            else: print(f'ERROR: {element} does not have the type NewCommand!')
        if self.envs: string += '\n(2) Environments:\n'
        for element in self.envs:
            if isinstance(element, NewCommand): string += f'{element.expand()}\n'
            else: print(f'ERROR: {element} does not have the type NewCommand!')
        if self.counters: string += '\n(3) Counters:\n'
        for element in self.counters:
            if isinstance(self.counters[element], NewCommand): string += f'{self.counters[element].expand()}\n'
            else: print(f'ERROR: {element} does not have the type NewCommand!')
        if self.theorems: string += '\n(4) Theorems:\n'
        for element in self.theorems:
            if isinstance(element, NewCommand): string += f'{element.expand()}\n'
            else: print(f'ERROR: {element} does not have the type NewCommand!')
        if self.packages: string += '\n(5) Packages:\n'
        for element in self.packages:
            if isinstance(element, Command): string += f'{element.expand()}\n'
        if self.unknown: string += '\nUNKNOWN COMMANDS AND ENVIRONMENTS\n\n'
        keys = list(self.unknown.keys())
        keys.sort()
        for k in keys: string += f'{k} ({self.unknown[k]})\n'
        return string

    def expand_unknown(self):
        '''expands the dict of all unknown macros and returns it as a string'''
        string = '\nUNKNOWN COMMANDS AND ENVIRONMENTS\n\n'
        keys = list(self.unknown.keys())
        keys.sort()
        for k in keys: string += f'{k} ({self.unknown[k]})\n'
        return string

class NewCommand():
    '''comprises all macros of the types 
    newcommand, newcommand*, newcounter, newinvironment, newenvironment*, newtheorem
    this is clumsy but it works. It covers all declarations
    By now onyl text-macros (i.e. LaTeX macros without arguments) 
    are resolved during the parsing process
    '''
    def __init__(self, type, name):
        # any of: newcommand, newcommand*, newcounter, newinvironment, newenvironment*, newtheorem
        self.type = str(type)   
        # is required in any case. All names are written without heading backslash
        self.name = str(name)   
        # number of arguments, default None
        self.narg = 0
        # optional argument, default None
        self.opt = DocNode()
        # second optional argument, only in newtheorem with sectioning counter
        self.secopt = DocNode()
        # first definition, required in commands and environments, default None (= empty)
        self.begdef = DocNode()
        # second definition, required in environments, default None (= empty)
        self.enddef = DocNode()
    def expand(self):
        if self.type == 'newcommand' or self.type == 'newcommand*':
            command = f'\\{self.name}'
        else: 
            command = self.name
        if self.narg: narg = f'[{self.narg}]' 
        else: narg = ''
        if self.opt and self.opt.expand(): 
            opt = f'[{self.opt.expand()}]'
        else: opt = ''
        if self.secopt and self.secopt.expand(): 
            secopt = f'[{self.secopt.expand()}]'
        else: secopt = ''
        if self.begdef and self.begdef.expand(): 
            begdef = f'{{{self.begdef.expand()}}}'
        else: begdef = ''
        if self.enddef and self.enddef.expand(): 
            enddef = f'{{{self.enddef.expand()}}}'
        else: enddef = ''
        return f'\\{self.type}{{{command}}}{narg}{opt}{begdef}{enddef}{secopt}'

class newcommand(NewCommand):
    '''main subclass of NewCommand, 
    which represents the LaTeX macro newcommand'''
    pass

class newenvironment(NewCommand):
    '''any newenvironment declaration'''
    pass

def int_to_Roman(num):
    '''converts an int into a string representing the corresponding
    roman number (works for all ints > 0 but only roman numerals 
    up to 100_000 are included), uses uppercase letters'''
    num = int(num)
    if isinstance(num, int): 
        val = [100_000, 90_000, 50_000, 40_000, 10_000, 9000, 5000, 4000, 1000, 
               900, 500, 400, 100, 90, 50, 40, 10, 9,  5, 4, 1]
        syb = ['ↈ', 'ↇↈ', 'ↇ', 'ↂↇ', 'ↂ', 'ↁↂ', 'ↁ', 'Mↁ', 'M', 'CM', 'D', 
               'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I' ]
        roman_num = ''
        i = 0 
        while num > 0:
            for _ in range(num // val[i]):
                roman_num += syb[i]
                num -= val[i]
            i += 1
        return roman_num
    else: 
        return None
    
def int_to_roman(num):
    '''just like int_to_Roman but uses lowercase letters'''
    num = int(num)
    if isinstance(num, int):
        val = [100_000, 90_000, 50_000, 40_000, 10_000, 9000, 5000, 4000, 1000, 
               900, 500, 400, 100, 90, 50, 40, 10, 9,  5, 4, 1]
        syb = ['ↈ', 'ↇↈ', 'ↇ', 'ↂↇ', 'ↂ', 'ↁↂ', 'ↁ', 'Mↁ', 'm', 'cm', 'd', 
               'cd', 'c', 'xc', 'l', 'xl', 'x', 'ix', 'v', 'iv', 'i' ]
        roman_num = ''
        i = 0 
        while num > 0:
            for _ in range(num // val[i]):
                roman_num += syb[i]
                num -= val[i]
            i += 1
        return roman_num    
    else:
        return None

def int_to_alph(num):
    ''' converts each integer into a lowercase letter 
    0 == z, 27 == a '''
    num = int(num)
    if isinstance(num, int):
        alph = ['z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 
                'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y']
        return alph[num % 26]
    else: 
        return None

def int_to_Alph(num):
    '''just like int_to_alph but uses uppercase letters'''
    num = int(num)
    if isinstance(num, int):
        alph = ['Z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
                'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y']
        return alph[num % 26]
    else: 
        return None

def int_to_fnsymbol(num):
    '''converts each int into one element of the list fnsymbols
    0 == ‡‡, 10 == * ''' 
    num = int(num)
    if isinstance(num, int):
        fnsymbol = ['‡‡', '*', '†', '‡', '§', '¶', '‖', '**' , '††']
        return fnsymbol[num % 9]
    else: 
        return None

class newcounter(NewCommand):
    ''' all counters are handled with the newcounter class
    In addition to NewCommand there are three variables:
        - counterstyle (Arabic, Roman etc. 
            currently hardly used since the counterstyle is mostly determined
            at jinja level)
        - value = integer, as in LaTeX the initial value is 0
        - incounters = list of all counters that must be reset when the 
            counter is incremented
    '''
    def __init__(self, type, name):
        super().__init__(type, name)
        self.counterstyle = ''
        self.value = 0
        self.incounters = []
    
    def setincounter(self, stack, string):
        '''specifies an element of the list incounters'''
        if isinstance(string, str) and string in stack.counters: 
            if not self.name in stack.counters[string].incounters:
                stack.counters[string].incounters.append(self.name)
        return stack


    def getvalue(self):
        '''returns the countervalue'''
        return self.value
        
    def setvalue(self,num):
        '''sets the countervalue to num'''
        if isinstance(num, int): self.value = num

    def increment(self):
        '''increments the countervalue'''
        self.value += 1

    def decrement(self):
        '''decrements the countervalue'''
        self.value -= 1

    def addtocounter(self,num):
        '''adds num to countervalue'''
        if isinstance(num, int): self.value += num

    def reset(self):
        '''resets the countervalue to 0'''
        self.value = 0


class newtheorem(NewCommand):
    '''specifies a new theorem (is currently not implemented)'''
    pass


class JinjaSpec():
    '''class for jinja2h specifications
    parses jinja specifications that have the form 
    \n\nname: LIST OF COMMANDNAMES\nSPECIFICATION

    the main variables are: 
    - stack = a Docstack object that is transfered to the object as an argument
    - elements = dict that contains all specifications 
        in the form command: specification
    '''
    def __init__(self, string, stack):
        '''fills the stack and all specifciations as contained in string
        in the object'''
        self.stack = stack
        self.elements = dict()
        string = regex.sub (r'\{#[^#].*#\}','',string) # remove comments
        specs = string.split('\n\nname:')
        for specit in specs:
            if specit:
                comms = specit.splitlines()[0]
                spec = specit[len(comms)+1:]
                comms = comms.strip().split()
                speclist = JinjaCommSpec(spec, self.stack)  # ADD strip() here again                    
                for comm in comms:
                    self.elements[comm] = speclist

    def expand(self):
        '''simply prints all elements'''
        for element in self.elements:
            print(element)


class JinjaDocument():
    '''class for jinja2h document templates, 
        e.g. html header that places the rest of the document with an {{ obj }} expression
    '''
    def __init__(self, string, template_string, stack):
        self.stack = stack
        self.string = string
        template_string = regex.sub (r'\{#[^#].*#\}','',template_string) # remove comments
        self.doc = JinjaCommSpec(template_string, stack)
    
class JinjaCommSpec():
    '''class for a single jinja command specification 
    parses it into a list of text-elements, jinja statements, and jinja expressions
    The variables are: 
    - stack = DocStack object
    - elements = list of all elements of the specification
    - normaltext = string, normaltext representation
    '''
    def __init__(self, spec, stack):
        '''stores the stack object and parses the spec string'''
        self.stack = stack
        self.elements = list()
        self.normaltext = ''
        while regex.search(r'\{(\%|\{|\!)(.*?)(\%|\}|\!)\}',spec):
            specstart = regex.search(r'\{(\%|\{|\!)(.*?)(\%|\}|\!)\}',spec)
            specend = specstart
            # put text at the beginning of spec on the list
            if specstart.start() > 0: self.elements.append(spec[0:specstart.start()])   # string
            if specstart.group(1) == "%" and specstart.group(3) == "%":                 # statement
                statement = specstart.group(2).strip()
                if statement[0:2] == 'if':                                              # if statement
                    if regex.search(r'\{\%\s*endif\s*\%\}',spec):
                        specend = regex.search(r'\{\%\s*endif\s*\%\}',spec)
                        statement = JinjaStatement(spec[specstart.start():specend.end()], stack)
                        self.elements.append(statement)
                    else:
                        self.error += f'jinja ERROR in {stack.filename}: incomplete if statement.\n'
                        print(f'jinja ERROR: incomplete if statement.')
                else:                                                                   # no other statement type is know
                    stack.error += f'jinja ERROR in {stack.filename}: statement type not covered.\n'
                    print(f'jinja ERROR: statement type not covered.')
            elif specstart.group(1) == "{" and specstart.group(3) == "}":               # expression
                expression = JinjaExpression(specstart.group(2).strip())
                self.elements.append(expression)
            elif specstart.group(1) == "!" and specstart.group(3) == "!":              # {! normal text !}
                self.normaltext = specstart.group(2).strip()
            else:
                stack.error += f'jinja ERROR in {stack.filename}: incorrect syntax.\n'
                print(f'jinja ERROR: incorrect syntax.')
            spec =  spec[specend.end():]
        if spec: self.elements.append(spec)
    

class JinjaIfClause():
    '''if clause in jinja statements of the form {% if QUESTION %} ANSWER ...'''
    def __init__(self, question, answer, stack):
        self.stack = stack
        self.question = JinjaExpression(question)
        self.answer = JinjaCommSpec(answer, stack)


class JinjaStatement():
    '''jinja statement
    currently the only covered variety is: 
    {% if EXP %} {% elif EXP %} {% else %} {% endif %} 
    for statements might be included later but are currently not needed
    Variables: 
    - stack = DocStack object
    - elifs = list of if/elif clauses
    - else = JinjaCommSpec for else clause
    - fortarget = JinjaExpression for for clause (not implemented)
    - forobject = JinjaExpression for for object (not implemented)
    - forblock = JinjaCommspec for main block of for statement (not implemented)
    '''
    def __init__(self, spec,stack):
        '''stores stack and parses spec into statement elements'''
        self.stack = stack
        self.elifs = list()
        self.fortarget = JinjaExpression('')
        self.forobject = JinjaExpression('')
        self.forblock = JinjaCommSpec('', stack)
        self.elseclause = JinjaCommSpec('', stack)
        if spec[0:2] == '{%' and spec[-2:] == '%}':
            # first case: if statement
            if regex.match(r'\{\%\s*if', spec):
                # first find if statement:
                statement = regex.match(r'\{\%\s*if(.*?)\%\}(.*?)(?=\{\%)',spec)
                question = statement.group(1).strip()
                answer = statement.group(2)
                clause = JinjaIfClause(question, answer, stack)
                self.elifs.append(clause)
                spec = spec[statement.end():]
                # second find any elif statements
                while regex.match(r'\{\%\s*elif(.*?)\%\}(.*?)(?=\{\%)',spec):
                    statement = regex.match(r'\{\%\s*elif(.*?)\%\}(.*?)(?=\{\%)',spec)
                    question = statement.group(1).strip()
                    answer = statement.group(2)
                    clause = JinjaIfClause(question, answer, stack)
                    self.elifs.append(clause)
                    spec = spec[statement.end():]
                # third find any else statement
                if regex.match(r'\{\%\s*else\s*\%\}(.*?)(?=\{\%)',spec):
                    statement = regex.match(r'\{\%\s*else\s*\%\}(.*?)(?=\{\%)',spec)
                    answer = statement.group(1)
                    self.elseclause = JinjaCommSpec(answer, stack)
                    spec = spec[statement.end():]
                if not regex.match(r'\{\%\s*endif\s*\%\}',spec): print(f'jinja ERROR: if statement ends with {spec}')
            else:
                print(f'Something wrong with jinja statement {spec}')
        else: 
            print(f'There is something wrong with jinja statement {spec}')



class JinjaExpression():
    '''Jinja Expression, varieties: obj, obj.attributes, obj.members, obj.counter
    additional functions would be helpful e.g. obj.attributes.XXX.YYY here: .YYY'''
    def __init__(self, expr):
        # einfachster Fall, letztes reqarg wird als Funktionswert übergeben:
        expr = expr.strip()
        if expr == 'obj': 
            self.type = 'obj'
            self.attribute = ''
        elif expr[0:14] == 'obj.attributes':
            self.type = 'att'
            self.attribute = expr[15:]
        elif expr[0:11] == 'obj.members':
            self.type = 'memb'
            self.member = expr[12:]
        elif expr[0:11] == 'obj.counter':
            self.type = 'counter'
            if regex.match(r'\((\-?\d+)\)',expr[11:]):
                self.level = int(regex.match(r'\((-?\d+)\)',expr[11:]).group(1))
                self.style = 'arabic'
            elif regex.match(r'(\(\'[a-zA-Z]+\'\))',expr[11:]):
                self.level = 0
                self.style = regex.match(r'\(\'([a-zA-Z]+)\'\)',expr[11:]).group(1)
            elif regex.match(r'\(\-?\d+\,\'[a-zA-Z]+\'\)',expr[11:]):
                match = regex.match(r'\((\-?\d+)\,\'([a-zA-Z]+)\'\)',expr[11:])
                self.level = int(match.group(1))
                self.style = match.group(2)
            else:
                self.level = 0
                self.style = 'arabic'
        else:
            self.type = 'filter'
            self.attribute = expr
        

class Argument():
    '''Argument of a Command. Variables:
    - name = string
    - datatype = string
    - content = DocNode()
    '''
    def __init__(self):
        self.name = str()
        self.datatype = str()
        self.content = DocNode()
    



class Command():
    '''Any LaTeX command or environment must be specified as a subclass of Command.
    Parameter: 
    Args = string that represents all arguments of the command

    Variables: 
    - name = string (in most cases redundant, i.e. identical with the class name)
    - optargs = list of all optional arguments
    - reqargs = list of all required arguments
    - argdict = dictionary of all argument names '''
    args = ''
    def __init__(self, name):
        # any string is allowed 
        self.name = str(name)
        self.optargs = list()
        self.reqargs = list()
        self.argdict = dict()
        def repl(match):
            replacement = f'[{match.group(1).strip()}]'
            return replacement
        args = regex.sub(r'\[([^\]]*)\]', repl, self.__class__.args)
        args = args.split()
        optargnr = 0
        reqargnr = 0
        for arg in args:
            if arg[0] == '[' and arg[-1] == ']':
                argcontent = arg[1:-1].split(':')
                if len(argcontent) > 1: datatype = argcontent[1]
                else: datatype = ''
                argument = Argument()
                argument.name = argcontent[0]
                argument.datatype = datatype
                self.optargs.append(argument)
                self.argdict[argcontent[0]] = ['opt',optargnr]
                optargnr += 1
            else:
                argcontent = arg.split(':')
                if len(argcontent) > 1: datatype = argcontent[1]
                else: datatype = ''
                argument = Argument()
                argument.name = argcontent[0]
                argument.datatype = datatype
                self.reqargs.append(argument)
                self.argdict[argcontent[0]] = ['req',reqargnr]
                reqargnr += 1

    def render_jinja_spec(self, spec, jinja):
        comm = ''
        for element in spec.elements:
            if isinstance(element,str): comm += element
            elif isinstance(element,JinjaExpression):
                if element.type == 'obj':
                    if self.reqargs:
                        if issubclass(self.__class__, includegraphics):
                            text = self.reqargs[-1].content.render(jinja)
                            if text[-4:] == ".eps": text = text[:-4] + ".svg"
                            comm += text
                        elif issubclass(self.__class__, VerbatimCommand):
                            verbtext = self.reqargs[-1].content.render(jinja).strip('{}')
                            comm += verbtext
                        else:
                            comm += self.reqargs[-1].content.render(jinja)
                    else: 
                        # probably not necessary to throw an exception here ...
                        comm += ''
                elif element.type == 'att':
                    if element.attribute in self.argdict:
                        if self.argdict[element.attribute][0] == 'opt':
                            # optional attribute
                            comm += self.optargs[self.argdict[element.attribute][1]].content.render(jinja)
                        elif self.argdict[element.attribute][0] == 'req':
                            # required attribute
                            comm += self.reqargs[self.argdict[element.attribute][1]].content.render(jinja)
                        else:
                            comm += '[UNKNOWN ARGUMENT]'
                    else:
                        # special attribute
                        if regex.search(r'\[(.*)\]',element.attribute):
                            index = regex.search(r'\[(.*)\]',element.attribute)
                            argpr = index.group(1)
                            arg = element.attribute[0:index.start()]
                            if argpr in self.argdict:
                                if self.argdict[argpr][0] == 'opt' and self.optargs[self.argdict[argpr][1]][arg]:
                                    comm += self.optargs[self.argdict[argpr][1]][arg]
                                elif self.argdict[argpr][0] == 'req' and self.reqargs[self.argdict[argpr][1]][arg]:
                                    comm += self.reqargs[self.argdict[argpr][1]][arg]
                                else:
                                    comm += '[something wrong with special attribute]'
                elif element.type == 'counter':
                    if issubclass(self.__class__, Section):
                        counterlevel = self.level + element.level
                        counterstyle = element.style
                        if counterlevel in self.countervalue:
                            value = self.countervalue[counterlevel]
                            if isinstance(value, int):
                                if counterstyle == 'arabic':
                                    comm += str(value)
                                elif counterstyle == 'Roman':
                                    comm += int_to_Roman(value)
                                elif counterstyle == 'roman':
                                    comm += int_to_roman(value)
                                elif counterstyle == 'alph':
                                    comm += int_to_alph(value)
                                elif counterstyle == 'Alph':
                                    comm += int_to_Alph(value)
                                elif counterstyle == 'fnsymbol':
                                    comm += int_to_fnsymbol(value)     
                            else: 
                                comm += f'Jinja Error: "{value}" is not an int'                           
                    elif issubclass(self.__class__, Note):
                        value = self.countervalue
                        counterstyle = element.style
                        if isinstance(value, int):
                            if counterstyle == 'arabic':
                                comm += str(value)
                            elif counterstyle == 'Roman':
                                comm += int_to_Roman(value)
                            elif counterstyle == 'roman':
                                comm += int_to_roman(value)
                            elif counterstyle == 'alph':
                                comm += int_to_alph(value)
                            elif counterstyle == 'Alph':
                                comm += int_to_Alph(value)
                            elif counterstyle == 'fnsymbol':
                                comm += int_to_fnsymbol(value)                                 
                        else: 
                            comm += f'Jinja Error: "{value}" is not an int' 
                    else:
                        ###################### OTHER COUNTER VARIETIES ------------------->
                        pass

                elif element.type == 'filter':
                    comm += ' [FILTER (not yet implemented) ]'
                else:
                    comm += ' [RENDERING ERROR IN JINJA FILE] '

            elif isinstance(element,JinjaStatement):
                for quanda in element.elifs:
                    if quanda.question and quanda.answer:
                        qu = False
                        if quanda.question.type == 'obj':
                            if self.reqargs: 
                                content = self.reqargs[-1].content.expand()
                                if content: qu = True
                            
                        elif quanda.question.type == 'att':
                            attname = quanda.question.attribute.split('[')
                            if len(attname) > 1:
                                pass
                                # SOMETHING SEEMS TO BE WRONG HERE !!!!!!!!!!!!!
                                # attribute has an additional argument
                                #comm += '[jinja complex argument]'
                            else: 
                                # simple attribute
                                if attname[0] in self.argdict:
                                    if self.argdict[attname[0]][0] == 'opt' and self.optargs[self.argdict[attname[0]][1]]:
                                        content = self.optargs[self.argdict[attname[0]][1]].content.expand()
                                        if content: qu = True
                                    elif self.argdict[attname[0]][0] == 'req' and self.reqargs[self.argdict[attname[0]][1]]:
                                        content = self.reqargs[self.argdict[attname[0]][1]]
                                        if content: qu = True
                        else:
                            comm += '[FILTER (not yet implemented)]'
                        if qu:
                            # rendere das komplette Argument
                            comm += self.render_jinja_spec(quanda.answer, jinja)
                    else:
                        comm += '[jinja ERROR: if clause is incomplete.]'
                if element.elseclause:
                    comm += self.render_jinja_spec(element.elseclause, jinja)
            else:
                comm += ' [RENDERING ERROR IN JINJA FILE] '
        return comm



    def render(self, jinja):
        comm = ''
        # catch all formulas and pass them as LaTeX objects
        if issubclass(self.__class__, Formula):
            if isinstance(self,math): comm += f'\\({self.expand_formula()}\\)'
            elif isinstance(self,displaymath): comm += f'\\[{self.expand_formula()}\\]'
            elif isinstance(self, equation): comm += f'\\begin{{equation}}{self.expand_formula()}\\end{{equation}}'
            elif isinstance(self, equation_): comm += f'\\begin{{equation*}}{self.expand_formula()}\\end{{equation*}}'
            elif isinstance(self, eqnarray): comm += f'\\begin{{eqnarray}}{self.expand_formula()}\\end{{eqnarray}}'
            elif isinstance(self, eqnarray_): comm += f'\\begin{{eqnarray*}}{self.expand_formula()}\\end{{eqnarray*}}'
            else:
                comm = ' [ERROR: formula type unknown] '
            return comm
        # tabular an tabbing environments
        elif issubclass(self.__class__, Table):
            # tabular, tabular*, and tabbing are treated as tabular objects here
            # later refinements are needed to take care to various specialities of these things
            # ATTENTION HTML ONLY: this first implementation renders each tabular or tabbing as an html table
            if self.name in jinja.elements:
                tablespec = jinja.elements[self.name].elements[0].split('@')
                tabledict = dict()
                for spec in tablespec: 
                    if len(spec) > 1: tabledict[spec[0:2]] = spec[2:]
                if not 'tb' in tabledict: tabledict['tb'] = ''
                if not 'rb' in tabledict: tabledict['rb'] = ''
                if not 'cb' in tabledict: tabledict['cb'] = ''
                if not 'cc' in tabledict: tabledict['cc'] = ''
                if not 'ce' in tabledict: tabledict['ce'] = ''
                if not 're' in tabledict: tabledict['re'] = ''
                if not 'te' in tabledict: tabledict['te'] = ''
                comm += f'\n{tabledict['tb']}'
                for row in self.rows:
                    comm += f'\n{tabledict['rb']}'
                    for cell in row:
                        ########################
                        # ATTENTION: this is rather an ad hoc solution 
                        # elements of the style declaration can already be given in 'cb'
                        cellstyle = tabledict['cb']
                        if (match := regex.match(r'((?:.*?)style="(?:.*?));?">', cellstyle)): cellstyle = match.group(1) + ';'
                        elif cellstyle[-1] == '>': cellstyle = cellstyle[:-1] + ' style="'
                        if cell.hidden: cellstyle += 'visibility: hidden; height: 0; line-height: 0;'
                        if cell.left_style == cell.right_style == cell.top_style == cell.bottom_style: 
                            cellstyle += f'border-style:{cell.left_style};'
                        else:
                            cellstyle += f'border-left-style:{cell.left_style};border-right-style:{cell.right_style};border-top-style:{cell.top_style};border-bottom-style:{cell.bottom_style};'
                        # ATTENTION: this needs to be done differently: 
                        cellstyle += 'border-width:1px;padding:5px;'
                        if cell.align == 'l': cellstyle += 'text-align:left;"'
                        if cell.align == 'r': cellstyle += 'text-align:right;"'
                        if cell.align == 'c': cellstyle += 'text-align:center;"'
                        if cell.colspan > 1: cellstyle += f' colspan="{str(cell.colspan)}"'
                        if cell.rowspan > 1: cellstyle += f' colspan="{str(cell.rowspan)}"'
                        comm += f'{cellstyle}>{tabledict['cc']}{cell.reqargs[-1].content.render(jinja)}{tabledict['ce']}'
                    comm += f'{tabledict['re']}'
                comm += f'\n{tabledict['te']}\n'
            else:
                comm += '\n<table>\n'
                for row in self.rows:
                    comm += '\n<tr>'
                    for cell in row:
                        comm += f'<td>{cell.reqargs[-1].content.render(jinja)}</td>'
                    comm += '</tr>'
                comm += '\n</table>\n'
            return comm
        elif issubclass(self.__class__, Par):
            start = self.name + '@start'
            end = self.name + '@end'
            if start in jinja.elements and end in jinja.elements:
                if self.parend: 
                    comm += self.render_jinja_spec(jinja.elements[end], jinja) 
                if self.parstart: 
                    comm += '\n' + self.render_jinja_spec(jinja.elements[start], jinja)
                return comm
            else: 
                print('ERROR: no start and and definition for Par object in jinja specification')
                if self.name in jinja.elements:
                    comm = self.render_jinja_spec(jinja.elements[self.name], jinja)
                    return comm
        
        else:
            # catch all other commands that have a jinja specification 
            if self.name in jinja.elements:
                comm = self.render_jinja_spec(jinja.elements[self.name], jinja)
                return comm
            # or use default rendering
            # NEEDS TO BE IMPLEMENTED on the basis of jinja spec for @default
            # THIS IS JUST A PRELIMINARY VERSION :::::
            # 
            else:            
            #    comm += f'\n<latex-cnd data-name="{self.name}">'    
            #    i = 0
            #    for optarg in self.optargs: 
            #        i += 1
            #        comm += f'<latex-optarg data-nr="{i}">{optarg.content.render(jinja)}</latex-optarg>'
            #    i = 0
            #    for reqarg in self.reqargs: 
            #        i += 1
            #        comm += f'<latex-reqarg data-nr="{i}">{reqarg.content.render(jinja)}</latex-reqarg>'
            #    comm += '</latex-cnd>\n'
                return comm


    def expand(self):
        if issubclass(self.__class__, Table): 
            return self.expand_table()
        optargs = ''
        for arg in self.optargs: optargs += f'[{arg.content.expand()}]'
        reqargs = ''
        for arg in self.reqargs: reqargs += f'{{{arg.content.expand()}}}'
        if optargs or reqargs:
            if self.name == 'math':             # AD HOC FIX FOR WEIRD BEHAVIOR IN FORMULAS
                return f'{optargs}{reqargs}'
            else:    
                return f'\\{self.name}{optargs}{reqargs}'
        else:
            return f'\\{self.name} '

    def normalize(self, jinja):
        if issubclass(self.__class__, Lineend): return '\n'
        elif issubclass(self.__class__, NoLineend): return ' '
        elif issubclass(self.__class__, TextMacro) and jinja.elements[self.name].normaltext: 
            return jinja.elements[self.name].normaltext
        elif issubclass(self.__class__, Table): return '[TABLE]'
        elif issubclass(self.__class__, Formula): return '[FORMULA]'
        elif not issubclass(self.__class__, Ignore):
            if self.reqargs:
                arg = self.reqargs[-1]
                return arg.content.normalize(jinja)
        return ''

    def expand_table(self):
        str = ''
        for row in self.rows:
            str += '    ROW: '
            for cell in row:
                expand = cell.expand()
                str += f'CELL: {expand}\n'
        return str

    def expand_formula(self):
        optargs = ''
        for arg in self.optargs: optargs += f'[{arg.content.expand()}]'
        reqargs = ''
        if len(self.reqargs) > 1:
            for arg in self.reqargs[0:-1]: 
                reqargs += f'{{{arg.content.expand()}}}'
        if len(self.reqargs) > 0: reqargs += self.reqargs[-1].content.expand()
        return f'{optargs}{reqargs}'

# this is a marker for all commands and environments that create divisions
# where in the self.reqarg[-1].content paragraphs might be contained
# IS NEEDED IN DocNode.adjust_paragraphs()
class Division():
    pass

# this class is intended as a marker for all objects that do not interrupt the flow of a paragraph
class Inline():
    pass

# is not exactly inline but can be taken as if it where: 
class SystemCommand(Command, Inline):
    pass

class UnknownCommand(Command, Inline):
    pass

# any command that specifies format for the text in the argument such as:
# italics, underlined, deletion, insertion, editor addition, etc. etc.
# WARNING: Format objects may not contain several paragraphs!! 
class Format(Command):
    args = 'self'

class usepackage(SystemCommand):
    args = '[ opt ] self'

class input(SystemCommand):
    args = 'self'

class include(SystemCommand):
    args = 'self'




# indicates for a subclass not to be included in the normalize process of a document:
class Ignore():
    pass

class Environment(Command):
    pass

class FormatEnv(Environment):
    args = ''

class UnknownEnvironment(Environment, Inline):
    pass

class document(Environment, Division):
    args = ''

class Formula(Environment):
    pass

class Math(Command):
    pass

# This is important for dynamically switching from and to math mode: 
class EnsureMath(Format):
    args = 'self'

class ensuremath(EnsureMath):
    pass

class math(Formula):
    args = ''
    
class displaymath(Formula):
    args = ''

class equation(Formula):
    args = ''

class equation_(Formula):
    args = ''

class eqnarray(Formula):
    args = ''

class eqnarray_(Formula):
    args = ''

# each column of a table is characterized by a Coldef
# here top_style and bottom_style may change from row to row
class Coldef():
    def __init__(self):
        # style-options: none, solid, double
        self.align = 'l'
        self.style = ''
        self.left_style = 'solid'
        self.right_style = 'solid'
        self.top_style = 'solid'
        self.bottom_style = 'solid'

class Cell(Command):
    args = 'content'
    def __init__(self, name):
        super().__init__(name)
        self.align = 'l'
        self.left_style = 'none'
        self.right_style = 'none'
        self.top_style = 'none'
        self.bottom_style = 'none'
        self.colspan = 1
        self.rowspan = 1
        self.hidden = False

class Table(Environment):
    args = '[ opt ] def'
    def __init__(self, name):
        super().__init__(name)
        self.coldef = list()
        self.rows = list()
        self.rowspans = dict()

class tabular(Table):
    pass

class tabular_(tabular):
    args = 'width [opt] def'

class longtable(tabular):
    args = 'width [opt] def'

class twocols(tabular):
    args = ''
    def __init__(self, name):
        super().__init__(name)
        n = 2
        for i in range(n):
            coll = Coldef()
            coll.left_style = 'none'
            coll.right_style = 'none'
            coll.top_style = 'none'
            coll.bottom_style = 'none'
            self.coldef.append(coll)

class transcols(tabular):
    args = ''
    def __init__(self, name):
        super().__init__(name)
        n = 2
        for i in range(n):
            coll = Coldef()
            coll.left_style = 'none'
            coll.right_style = 'none'
            coll.top_style = 'none'
            coll.bottom_style = 'none'
            self.coldef.append(coll)

class mytabular(tabular):
    pass

class tabbing(Table):
    args = ''



class Section(Command):
    args = '[ opt ] self'
    level = 1               # options: int, range: 
                            #                       -10
                            #    
                            #   part,               -1
                            #   chapter,            0
                            #   section,            1
                            #   subsection,         2
                            #   subsubsection,      3
                            #   subsubsection,      4
                            #   subsubsubsection,   5
                            #
                            #   paragraph,          10
                            #   subparagraph        11
                            #
                            #                       16   16-20: no resetting of counters
                            #
                            #                       20
    def __init__(self,name):
        super().__init__(name)
        self.countervalue = {}
    
    def setcounter(self,stack):
        level = self.level
        i = 0
        # store current counter and reset values of all subcounters
        if f'section@{level}' in stack.counters: 
            stack.counters[f'section@{level}'].increment()
            self.countervalue
            self.countervalue[level] = stack.counters[f'section@{level}'].getvalue()
            n = level + 1
            while n < 10:
                if f'section@{n}' in stack.counters: 
                    stack.counters[f'section@{n}'].reset()
                n += 1
        # store values for all parent counters
        j = -10
        while j < level:
            if f'section@{j}' in stack.counters: 
                self.countervalue[j] = stack.counters[f'section@{j}'].getvalue()
            j += 1
        # reset Note counters and other counters, if applicable:
        def get_sub_classes(cls):
            subclasses = []
            for subclass in cls.__subclasses__():
                subclasses.append(subclass)
                subclasses.extend(get_sub_classes(subclass))
            return subclasses
        if level == 0 or level == 16:
            # notes: each instance of Note
            classes = get_sub_classes(Note)
            for cls in classes:
                if cls.__name__ in stack.counters: stack.counters[cls.__name__].reset()

        return stack

# any footnote, marginnote, endnote etc.
class Note(Format):
    args = 'self'
    def __init__(self,name):
        super().__init__(name)
        self.countervalue = 0
        self.notetype = 'solo'
        self.markednote = 0
        if name[-4:] == 'mark':
            self.notetype = 'label'
            #self.name = name[:-4]
        elif name[-4:] == 'text':
            self.notetype = 'text'
            #self.name = name[:-4]

    def setcounter(self,stack):
        #name = self.name + 'mark'
        if self.notetype == 'solo': 
            stack.counters[self.name].increment()
            self.countervalue = stack.counters[self.name].getvalue()
        elif self.notetype == 'label': 
            name = self.name[:-4]      
            stack.counters[self.name].increment()
            stack.counters[name].increment()
            if stack.firstdoc: 
                stack.counters['MarkedNotes'].decrement()
                stack.firstdoc = False
            stack.counters['MarkedNotes'].increment()
            self.markednote = stack.counters['MarkedNotes'].getvalue()
            self.countervalue = stack.counters[name].getvalue()
        elif self.notetype == 'text': 
            name = self.name[:-4]
            if stack.counters[name + 'mark'].getvalue() > 0:
                stack.counters[name].addtocounter(- stack.counters[name + 'mark'].getvalue())
                stack.counters[name].increment()
                stack.counters[name + 'mark'].reset()
                self.countervalue = stack.counters[name].getvalue()
            else:
                stack.counters[name].increment()
                self.countervalue = stack.counters[name].getvalue()

        return stack


class footnote(Note):
    args = '[ number ] text'

class marginpar(Note):
    args = '[ lefttext ] righttext'

class marginnote(Note):
    args = 'self [ length ]'

# counters must be created with newcounter, this is only for refering to counter values:
class Counter(SystemCommand):
    args = 'self'
    def __init__(self,name):
        super().__init__(name)
        self.countervalue = 0

    def stepcounter(self,stack):
        if self.reqargs: name = self.reqargs[0]
        else: name = ''
        if name in stack.counters: 
            stack.counters[name].increment()
            self.countervalue = stack.counters[name].getvalue()
        
        return stack

    def setcounter(self,value,stack):
        if self.reqargs: name = self.reqargs[0]
        else: name = ''
        i = 0
        if not isinstance(value, int): value = 0
        if name in stack.counters: 
            stack.counters[name].setvalue(value)
            self.countervalue = stack.counters[name].getvalue()
        
        return stack

    def addtocounter(self,value,stack):
        if self.reqargs: name = self.reqargs[0]
        else: name = ''
        i = 0
        if not isinstance(value, int): value = 0
        if name in stack.counters: 
            stack.counters[name].addtocounter(value)
            self.countervalue = stack.counters[name].getvalue()
        
        return stack


class setcounter(Counter):
    args = 'name value'

class addtocounter(Counter):
    args = 'name value'

class stepcounter(Counter):
    args = 'name'

class refstepcounter(Counter):
    args = 'name'

class LaTeXList(Environment):
    args = ''

class List(LaTeXList):
    args = 'label parameter'

# BETTER Enumerate and enumerate only as a subclass !!!!
class enumerate(LaTeXList):
    args = ''

class itemize(LaTeXList):
    args = ''

class description(LaTeXList):
    args = ''

class Item(Command):
    args = '[ label ] content'

# normal item commands that do not deliver the content as a reqarg
class ItemWithoutContent(Item):
    args = '[ label ]'

class item(ItemWithoutContent):
    args = '[ description ]'

class iitem(item):
    pass

# nonstandard item commands that deliver the content as a reqarg
class ItemWithContent(Item):
    pass

# Bibliographies 
class Bibliography(LaTeXList):
    pass

# some Commands for internal use only:

class Whitespace(Format):
    args = ''

# any whitespace that does not include a line shift:
class NoLineend(Whitespace):
    args = ''

class Lineend(Whitespace):
    pass


class VALEPThinSpace(NoLineend):
    args = '' 
    
class VALEPnbsp(NoLineend):
    args = '' 

# ANY TEXTMACRO that produces just (formatted or unformatted) text
class TextMacro(Format):
    args = ''

# this class is essential for identifying paragraphs in the document:
class Par(Command):
    args = ''
    def __init__(self,name):
        super().__init__(name)
        self.parend = True
        self.parstart = True

class par(Par):
    pass

class newline(Lineend):
    args = '[ opt ]'

class VALEPlb(Lineend):
    args = '[ space ]'

class VALEPlb_(Lineend):
    args = '[ space ]'


# further important standard LaTeX macros:

# character > (treat with care in xml and html)
class gt(TextMacro):
    args = ''

# character < (treat with care in xml and html)
class lt(TextMacro):
    args = ''

# soft hyphen: replaces \-
class VALEPshy(Format):
    args = ''

class Verbatim(Command):
    pass

class VerbatimEnvironment(Environment,Verbatim):
    pass

class VerbatimCommand(Verbatim, Inline):
    args = 'self'

class verbatim(VerbatimEnvironment):
    args = ''

class htmlcode(VerbatimCommand):
    args = 'self'

class rawhtml(VerbatimEnvironment):
    args = ''

class htmlonly(Command, Inline):
    args = 'self'

class LaTeXOnly(VerbatimCommand):
    args = 'self'

class latexonly(LaTeXOnly):
    args = 'self'




# any reference 
# varieties include: Index, Label, Ref, Link
class Reference(Command, Ignore, Inline):
    args = 'self'

# any index entry
class Index(Reference):
    pass

class Label(Reference):
    pass

class Ref(Reference):
    pass

class Link(Reference):
    args = 'link text'

class Graphic(Reference):
    args = 'src'

class includegraphics(Graphic):
    args = '[ argll ] [ argur ]  self' 

