Remark_To_Markdown.py

Back to Algorithms

Remark/

# -*- coding: utf-8 -*-

# Description: Converts Remark to Markdown and html
# Documentation: algorithms.txt

from __future__ import print_function

import re
import string
import os
import os.path
import datetime
import codecs
import copy
import traceback
import time
import six

from Remark.Version import remarkVersion
from Remark.Macro_Registry import findMacro
from Remark.FileSystem import changeExtension, unixDirectoryName, copyIfNecessary
from Remark.FileSystem import globalOptions, unixRelativePath, writeFile
from Remark.Reporting import Reporter, ScopeGuard
from Remark.DocumentType_Registry import documentType, outputDocumentName
from Remark.DocumentTree import createDocumentTree

emptyList = object()

class Scope(object):
    def __init__(self, parent, name):
        self.parent = parent
        self.name = name
        self.nameSet = dict()

    def name(self):
        return self.name

    def insert(self, name, data):
        #print('Inserted', name, data)
        self.nameSet[name] = data

    def append(self, name, data):
        result = self.search(name)
        if result != None:
            result += data
        else:
            self.insert(name, data)

    def parent(self):
        return self.parent

    def outer(self):
        if self.parent == None:
            return self
        return self.parent

    def shallowSearch(self, name):
        return self.nameSet.get(name)

    def search(self, name):
        #print('Recursive search for', name)
        result = self.shallowSearch(name)        
        if result != None:
            return result
        if self.parent != None:
            return self.parent.search(name)
        return None

    def searchScope(self, name):
        #print('Recursive search for', name)
        result = self.shallowSearch(name)        
        if result != None:
            return self
        if self.parent != None:
            return self.parent.searchScope(name)
        return self

    def get(self, name, defaultValue = emptyList):
        if defaultValue is emptyList: defaultValue = []
        variable = self.search(name)
        if variable == None:
            return defaultValue

        return variable

    def getString(self, name, defaultValue = '', joinString = ''):
        variable = self.get(name)

        if variable == []:
            return defaultValue

        return joinString.join(variable)

    def getInteger(self, name, defaultValue = 0):
        value = None
        text = self.search(name)

        if text != None:
            if len(text) == 1:
                try:
                    value = int(text[0])
                except ValueError:
                    value = None
        else:
            value = defaultValue

        if value == None:
            print('Warning: Could not convert', name, 'to an integer. Using default.')
            value = defaultValue

        return value

class ScopeStack(object):
    def __init__(self):
        self.scopeStack = []

    def open(self, name):
        #print('Scope opened.')
        parent = None
        if len(self.scopeStack) > 0:
            parent = self.top()                
        self.scopeStack.append(Scope(parent, name))

    def close(self):
        #print('Scope closed.')
        self.scopeStack.pop()

    def top(self):
        return self.scopeStack[-1]

    def bottom(self):
        return self.scopeStack[0]

    def printScopes(self):
        print(len(self.scopeStack))
        tabs = 0;
        for scope in self.scopeStack:
            print('\t' * tabs)
            print(scope.name, 'scope:')
            print
            for entry in scope.nameSet.items():
                print(entry[0], ':', entry[1])
            tabs += 1

class MacroInvocation(object):
    def __init__(self, name,
                 parameterSet,
                 outputExpansion,
                 parameterExpansion,
                 beginRow, beginColumn,
                 endRow, endColumn):
        self.name = name
        self.parameterSet = parameterSet
        self.outputExpansion = outputExpansion
        self.parameterExpansion = parameterExpansion
        self.beginRow = beginRow
        self.beginColumn = beginColumn
        self.endRow = endRow
        self.endColumn = endColumn

class Remark(object):
    '''
    Converts Remark to Markdown.
    '''

    def __init__(self, document, documentTree, 
                 inputRootDirectory, outputRootDirectory,
                 reporter = Reporter()):
        self.scopeStack = ScopeStack()
        self.scopeStack.open('global')
        self.document = document
        self.documentTree = documentTree
        self.linkIndex = 0
        self.linkSet = []
        self.usedMacroSet = []
        self.inputRootDirectory = inputRootDirectory
        self.outputRootDirectory = outputRootDirectory
        self.reporter = reporter

        # Here we form regular expressions to identify
        # Remark macro invocations in the text.

        # This matches a macro-identifier. The macro
        # identifier is the string between [[ and ]].
        # It may include characters a to z, A to Z,
        # 0 to 9, the - and the _.
        # Examples: 'set some-variable', 'Gallery'.
        self.macroIdentifier = r'([a-zA-Z_.\- ][a-zA-Z0-9_.\- ]*)'

        # This matches whitespace, which to us means
        # spaces and tabs.
        self.whitespace = r'[ \t]*'

        # This matches an optional inline parameter.
        # Starting from the outside, the whole thing is optional.
        # The first parentheses (?: ) are just for grouping. The
        # inline parameter must start with ':', following by optional
        # whitespace. If something is left, the inline parameter is
        # that what becomes before ]].
        self.optionalInlineParameter = r'(?::' + self.whitespace + r'((?:(?!\]\]).)*))?'

        # The one-line parameter starts with a ':' and continues to
        # to end of the line. The dot . matches anything except \n. 
        # The leading white-space is eaten away.
        self.optionalOneLineParameter = r'(?::' + self.whitespace + r'(.*))?'

        self.optionalOutputExpansion = r'(\+|\-)?'
        self.optionalParameterExpansion = r'(\+|\-)?'

        # Piece together the whole regex for macro-invocation.
        # It is something which starts with [[, ends with ]],
        # has expansion-signs either none, +, -, ++, +-, -+, or --,
        # has a macro identifier, and then an optional inline
        # parameter. Finally, there is an optional one-line paramater
        # after the ]].
        self.macroRegex = re.compile(r'\[\[' + 
                                     self.optionalOutputExpansion +
                                     self.optionalParameterExpansion + 
                                     self.macroIdentifier + 
                                     self.optionalInlineParameter + 
                                     r'\]\]' + 
                                     self.optionalOneLineParameter)

        #macroText = r'((?:(?!]]).)*)'
        #macroRegex = re.compile(r'\[\[' + macroText + r'\]\]' + optionalOneLineParameter)
        self.wholeGroupId = 0
        self.outputExpansionGroupId = 1
        self.parameterExpansionGroupId = 2
        self.identifierGroupId = 3
        self.inlineGroupId = 4
        self.externalGroupId = 5
        self.recursionDepth = 0
        self.used = False

        # Set default variables.
        self.scopeStack.top().insert('indent', ['Verbatim'])
        self.scopeStack.top().insert('remark_version', [remarkVersion()])

    def linkId(self):
        '''
        Generates a unique integer for a new link.

        This integer is used to make the generated Markdown links unique.
        
        returns (integer):
        A unique integer.        
        '''
        result = self.linkIndex
        self.linkIndex += 1
        return result

    def remarkLink(self, description, 
                   fromDocument, toDocument):
        '''
        Generates a Markdown-link from a document to another.

        description (string):
        The description for the link.
        
        fromDocument (Document):
        The document to generate the link from.

        toDocument (Document):
        The document to generate the link to.

        returns (string):
        The generated link in Markdown. As a side-effect 
        the actual link-address is stored for listing the
        link-definition later at the end of the document.
        '''
        fromDirectory = fromDocument.relativeDirectory
        toFile = outputDocumentName(toDocument.relativeName)
        linkTarget = unixRelativePath(fromDirectory, toFile)
        return self.markdownLink(description, linkTarget)

    def markdownLink(self, description, htmlLink):
        '''
        Generates a Markdown-link to the given URL.

        description (string):
        The description for the link.
        
        htmlLink (string):
        The URL of the link.

        returns (string):
        The generated link in Markdown. As a side-effect 
        the actual link-address is stored for listing the
        link-definition later at the end of the document.
        '''
        # The automatically generated Markdown
        # links are named as 'RemarkLink_x' where
        # x is an integer that runs from 0 upwards
        # as new links are retrieved.        
        name = 'RemarkLink_' + str(self.linkId())

        # Form the Markdown link.
        text = '[' + description + '][' + name + ']'

        # To maintain pretty Markdown output, we store the
        # definitions so that we can output them to the
        # end of the document. 
        self.linkSet.append((name, unixDirectoryName(htmlLink)))

        #text = '[' + description + ']<' + unixDirectoryName(htmlLink) + '>'

        return text

    def reportWarning(self, text, type):
        self.reporter.reportWarning(text, type)

    def reportError(self, text, type):
        self.reporter.reportError(text, type)

    def reportDebug(self, text, type):
        self.reporter.reportDebug(text, type)

    def report(self, text, type):
        self.reporter.report(text, type)

    def extractMacro(self, row, match, text):
        '''
        Extracts the information from a macro invocation.
        '''

        # There are four possibilities for the
        # macro invocation:
        #
        # 1) There is no parameter. In this case the
        # entry is of the form '[[Macro]]'.
        #
        # 2) There is an inline parameter. In this case
        # the entry is of the form '[[Macro: parameter here]]'.
        #
        # 3) There is a one-line parameter. In this case
        # the entry is of the form '[[Macro]]: parameter here'.
        #
        # 4) There is a multi-line parameter. In this case
        # the entry is of the form:
        # [[Macro]]:
        #     Parameters
        #     here
        #
        #     More parameters
        #
        # Options 3 and 4 are together called external parameters.

        matchBegin = match.start(self.wholeGroupId)
        matchEnd = match.end(self.wholeGroupId)
        macroName = match.group(self.identifierGroupId) 

        inlineParameter = match.group(self.inlineGroupId)
        onelineParameter = match.group(self.externalGroupId)
        outputExpansion = match.group(self.outputExpansionGroupId)
        if outputExpansion != None:
            if outputExpansion == '+':
                outputExpansion = True
            else:
                outputExpansion = False
        parameterExpansion = match.group(self.parameterExpansionGroupId)
        if parameterExpansion != None:
            if parameterExpansion == '+':
                parameterExpansion = True
            else:
                parameterExpansion = False
        else:
            if outputExpansion != None: 
                parameterExpansion = outputExpansion
            else:
                parameterExpansion = False

        hasExternalParameters = (onelineParameter != None)

        parameterSet = []

        hasInlineParameters = (inlineParameter != None)

        # Extract an inline parameter.
        if hasInlineParameters:
            parameter = inlineParameter.strip()
            parameterSet.append(parameter)

        # Extract a one-line parameter.
        hasOnelineParameter = False
        if hasExternalParameters:
            # If the parameter consists of all
            # whitespace, it is a multi-line parameter
            # so ignore that case here.
            parameter = onelineParameter.strip()
            if parameter != '':
                # One-line parameter
                hasOnelineParameter = True
                parameterSet.append(parameter)

        # A parameter is multi-line if its external but
        # not one-line.
        hasMultilineParameter = (hasExternalParameters and not hasOnelineParameter)

        # If the parameter is not multi-line, we are done.
        if not hasMultilineParameter:
            return MacroInvocation(macroName,
                                   parameterSet,
                                   outputExpansion,
                                   parameterExpansion,                                   
                                   row, matchBegin,
                                   row, matchEnd)

        # The parameter is multi-line. Extract that parameter,
        # and find out its extent.
        parameterSet = self.extractMultilineParameter(text, row + 1)
        nonEmptyLines = len(parameterSet)

        return MacroInvocation(macroName,
                               parameterSet,
                               outputExpansion,
                               parameterExpansion,                                   
                               row, matchBegin,
                               row + nonEmptyLines, len(text[row + nonEmptyLines]))        

    def extractMultilineParameter(self, text, startRow):
        endRow = startRow
        while endRow < len(text):
            # The end of a multi-line parameter
            # is marked by a line which is not all whitespace
            # and has no indentation. 
            if (_leadingTabs(text[endRow], globalOptions().tabSize)[0] == 0 and 
                text[endRow].strip() != ''):
                break
            endRow += 1

        # However, we do not include those whitespace-lines _at the end_
        # that lack indentation. Excluding these lines is important;
        # otherwise a following header-line could be interpreted as
        # a paragraph followed by a separator-line, since there is no
        # separating whitespace. It is also important to preserve those
        # empty-lines which are indented; that whitespace may be significant
        # for a macro.
        while (endRow > startRow and 
            _leadingTabs(text[endRow - 1], globalOptions().tabSize)[0] == 0 and
            text[endRow - 1].strip() == ''):
            endRow -= 1

        # Copy the parameter and remove the indentation from it.
        parameterSet = [_removeLeadingTabs(line, globalOptions().tabSize, 1) 
            for line in text[startRow : endRow]]

        return parameterSet

    def expandBuiltInMacro(self, macroNameSet, parameterSet, scope):
        '''
        Expands a built-in macro.

        macroNameSet (list of strings):
        The macro-name split into whitespace-separated words.

        parameterSet (list of strings):
        The parameter of the macro.

        scope (Scope):
        The current variable-scope.
        '''
        macroName = macroNameSet[0]
        document = self.document

        macroText = ['']    
        macroHandled = False
        getCommand = False

        if not macroHandled and macroName == 'set':
            # Sets a scope variable, e.g.
            # [[set variable]]: some input
            if len(macroNameSet) < 2:
                self.reportWarning('set command is missing the variable name. Ignoring it.',
                                   'invalid-input')
            else:
                variableName = macroNameSet[1]
                if parameterSet != []:                 
                    scope.insert(variableName, parameterSet)
                else:
                    scope.insert(variableName, [''])
            macroHandled = True

        if not macroHandled and macroName == 'set_tag':
            # Sets a document tag, e.g.
            # [[set_tag some-tag]]: some input
            if len(macroNameSet) < 2:
                self.reportWarning('set-tag command is missing the tag-name. Ignoring it.',
                                   'invalid-input')
            else:
                tagName = macroNameSet[1]
                document.setTag(tagName, parameterSet)
            macroHandled = True

        if not macroHandled and macroName == 'tag':
            # Retrieves a tag, e.g.
            # [[tag some-tag]]
            if len(macroNameSet) < 2:
                self.reportWarning('tag command is missing the tag-name. Ignoring it.',
                                   'invalid-input')
            else:
                tagName = macroNameSet[1]
                if tagName in document.tagSet:
                    macroText = document.tag(tagName)
                else:
                    self.reportWarning('Tag ' + tagName + 
                                       ' has not been defined. Ignoring it.',
                                       'undefined-tag')
            macroHandled = True

        if not macroHandled and macroName == 'set_outer':
            # Setting a variable at outer scope, e.g.
            # [[set_outer variable]]: some input
            if len(macroNameSet) < 2:
                self.reportWarning('set_outer command is missing the variable name. Ignoring it.',
                                   'invalid-input')
            else:
                variableName = macroNameSet[1]
                if scope.outer() == scope:
                    self.reportWarning('set_outer: already at global scope.',
                                       'invalid-input')
                outerScope = scope.outer().searchScope(variableName)
                if parameterSet != []:
                    outerScope.insert(variableName, parameterSet)
                else:
                    outerScope.insert(variableName, [''])
            macroHandled = True

        if not macroHandled and macroName == 'set_many':
            # Setting to many scope variables, e.g.
            # [[set_many Gallery]]:
            #       width 250
            #       height 500
            prefix = ''
            if len(macroNameSet) >= 2:
                prefix = macroNameSet[1] + '.'
            for line in parameterSet:
                if line.strip() != '':
                    nameValue = line.split(None, 1)
                    variable = prefix + nameValue[0].strip()
                    if len(nameValue) == 2:
                        scope.insert(variable, [nameValue[1].strip()])
                    else:
                        scope.insert(variable, [''])
            macroHandled = True

        if not macroHandled and macroName == 'add':
            # Appending to a scope variable, e.g.
            # [[add variable]]: some new input
            if len(macroNameSet) < 2:
                self.reportWarning('add command is missing the variable name. Ignoring it.',
                                   'invalid-input')
            else:
                variableName = macroNameSet[1]
                scope.append(variableName, parameterSet)
            macroHandled = True

        if not macroHandled and macroName == 'add_outer':
            # Adding a new line to a variable at outer scope, e.g.
            # [[add_outer variable]]: some new input
            if len(macroNameSet) < 2:
                self.reportWarning('add_outer command is missing the variable name. Ignoring it.',
                                   'invalid-input')
            else:
                variableName = macroNameSet[1]
                outerScope = scope.outer().searchScope(variableName)
                if parameterSet != []:
                    outerScope.append(variableName, parameterSet)
                else:
                    outerScope.append(variableName, [''])
            macroHandled = True

        if not macroHandled and (macroName == 'outer' or macroName == 'get_outer'):
            # Getting a global variable, e.g.
            # [[outer variable]]
            if len(macroNameSet) < 2:
                self.reportWarning(macroName + ' command is missing the variable name. Ignoring it.',
                                   'invalid-input')
            else:
                getCommand = True
                getName = macroNameSet[1]
                getScope = scope.outer().searchScope(macroNameSet[1])
            macroHandled = True

        # This needs to be handled last, so that one can use
        # built-in macros without parameters, such as
        # [[set_many]].
        if not macroHandled and len(macroNameSet) == 1:
            # Getting a scope variable, main form, e.g.
            # [[variable]]
            getName = macroName
            getCommand = True
            getScope = scope
            macroHandled = True

        # This part takes care of actually fetching a variable.
        # It is shared between get (both forms), outer, and get_outer.
        if getCommand:
            # Get the variable.
            result = getScope.search(getName)
            if result != None:
                macroText = result
            else:
                self.reportWarning('get: variable ' + getName + 
                                   ' has not been defined. Ignoring it.', 
                                   'undefined-variable')

        return macroText, macroHandled

    def expandMacro(self, macroInvocation):
        '''
        Expands the given macro invocation.

        macroInvocation (MacroInvocation):
        The information about the macro invocation.

        returns (list of strings, set of document-objects):
        The text the macro expands to.
        '''
        # This is where we will gather the expanded
        # contents of the macro.
        macroText = ['']
        macroHandled = False

        self.recursionDepth += 1

        # maxRecursionDepth = 100
        # if self.recursionDepth > maxRecursionDepth:
        #     self.reportDebug(
        #         'Macro expansion recursion exceeded ' + 
        #         str(maxRecursionDepth) + 
        #         ' levels.', 
        #         'debug-recursion')
        #     sys.exit(0)

        # This function expands the given macro in
        # the current position.
        scope = self.scopeStack.top()

        # By default, the output will be expanded. 
        # If a proper macro is invoked, then its
        # decision overrides this default.
        expandOutput = True

        # Retrieve the macro names and parameters.
        macroNameSet = macroInvocation.name.split()
        macroName = macroNameSet[0]
        parameterSet = macroInvocation.parameterSet

        # Handle external macros.
        if len(macroNameSet) == 1:
            # Search for the macro.
            macro = findMacro(macroName)

            # Get the macro suppress list.
            suppressList = scope.search('suppress_calls_to')
            if suppressList == None:
                suppressList = []

            if macro != None:
                # The macro is not run if it is
                # in suppress list.
                if not macroName in suppressList:
                    # Run the actual macro.
                    with ScopeGuard(self.reporter, macro.name()):
                        macroText = macro.expand(parameterSet, self)

                    if macroText == []:
                        macroText = ['']

                    # Mark the macro as used.
                    self.usedMacroSet.append(macro)

                    # The output of the macro is either
                    # recursively expanded or not.
                    # The macro suggests a default for this
                    # behavior.
                    expandOutput = macro.expandOutput()

                macroHandled = True

        # Handle built-in macros.
        # Note that this has to be done after the external
        # macros, since otherwise the variable retrieval
        # [[variable]] would match those macros.
        if not macroHandled:
            macroText, macroHandled = self.expandBuiltInMacro(
                macroNameSet, parameterSet, scope)

        # If no macro was recognized, report a warning and continue.
        if not macroHandled:
            self.reportWarning('Don\'t know how to handle macro ' + 
                               macroInvocation.name + '. Ignoring it.',
                               'unknown-macro')

        # The invocation can override the decision 
        # whether to expand the output.
        if macroInvocation.outputExpansion != None:
            expandOutput = macroInvocation.outputExpansion

        if macroHandled and expandOutput:
            # Expand recursively.
            self.scopeStack.open(macroInvocation.name)
            self.scopeStack.top().insert('parameter', macroInvocation.parameterSet)
            macroText = self.convert(macroText)
            self.scopeStack.close()

        self.recursionDepth -= 1

        return macroText

    def postConversion(self):
        '''
        Runs through the post-conversions of used macros and
        returns a text containing all link-definitions in
        Markdown syntax.

        returns (list of strings):
        The link-definitions in Markdown syntax.
        '''

        # Run through the post-conversions of all used macros.    
        for macro in self.usedMacroSet:
            macro.postConversion(self)

        # Generate the link definitions.
        text = []
        for link in self.linkSet:
            text.append('[' + link[0] + ']: ' + link[1])

        return text

    def convert(self, text):
        '''
        Converts Remark text to Markdown text.
        
        text (list of strings):
        The Remark text to convert.
        
        returns (list of strings):
        The converted Markdown text.
        '''

        # The strategy in this function is to trace the 'text' 
        # line by line while expanding the macros to 'newText'.

        row = 0
        column = 0
        newText = ['']
        while row < len(text):
            # Replace the first characters with spaces
            # so that the previous macros won't interfere
            # with the rest of the processing.
            line = ' ' * column + text[row][column :]

            # The indentation macro is invoked if and only if
            # 1) a non-empty line starts with a tab, and
            tabbedNonEmpty = (
                line.strip() != '' and 
                line[0] == '\t')

            # 2) the line in 1 is preceded by a row of whitespace, and
            precededByWhitespace = (
                row == 0 or
                text[row - 1].strip() == '')

            # 3) the first non-empty line preceding line in 1 does
            # not start with a tab.
            indentationMacro = False
            if tabbedNonEmpty and precededByWhitespace:
                # The first two conditions are satisfied.
                # This line possibly starts an indentation macro.

                # Check the third condition.
                indentationMacro = True
                for i in range(row - 2, -1, -1):
                    if text[i].strip() != '':
                        # The line is non-empty.
                        if text[i][0] == '\t':
                            # The first non-empty line starts
                            # with a tab. Therefore this line
                            # does not start an indentation macro.
                            indentationMacro = False
                        break

            if indentationMacro:
                # There is an indentation-macro invocation here.

                # Add an empty line.
                newText.append('')

                # Gather the multiline parameter.
                parameterSet = self.extractMultilineParameter(text, row)

                # Get the name of the indentation macro.
                macroName= self.scopeStack.top().getString('indent').strip()

                switches = 0;

                outputExpansion = None
                parameterExpansion = None
                if len(macroName) >= 1:
                   if macroName[0] == '+':
                       outputExpansion = True
                       parameterExpansion = True
                       switches += 1
                   elif macroName[0] == '-':
                       outputExpansion = False
                       parameterExpansion = False
                       switches += 1

                if len(macroName) >= 2 and switches == 1:
                    if macroName[1] == '+':
                        parameterExpansion = True
                        switches += 1
                    elif macroName[1] == '-':
                        parameterExpansion = False
                        switches += 1

                macroName = macroName[switches : ]

                macroInvocation = MacroInvocation(
                     macroName,
                     parameterSet,
                     outputExpansion,
                     parameterExpansion,
                     row, 0,
                     row + len(parameterSet), 0)
            else:
                # See if there is a macro somewhere on the line.
                match = re.search(self.macroRegex, line)
                if match == None:
                    # There is no macro on the line: 
                    # copy the line verbatim.

                    if column == 0 and line.strip() == '':
                        # The line is all whitespace. This
                        # signifies a new-line.
                        if newText[-1].strip() != '':
                            # A new-line is to be started
                            # only if there is already content
                            # on the latest-line.
                            newText.append('')
                    else:
                        # Concatenate the rest of the line to
                        # the latest line.
                        newText[-1] += line[column :]

                    # In any case, start a new line.
                    newText.append('')
                    row += 1
                    column = 0
                    continue

                #print('I read:')
                #print(match.group(0))

                # Yes, there is a macro on the line.
                # First copy the possible verbatim content.
                matchBegin = match.start(0)
                newText[-1] += line[column : matchBegin]
                column = matchBegin

                # Find out the whole macro invocation.
                macroInvocation = self.extractMacro(row, match, text)

            # Debug-report the macro-invocation.
            underlining = '-' * len(macroInvocation.name)

            invocationText = []
            invocationText.append(
                macroInvocation.name + ' ' +
                '(' + 
                    str(macroInvocation.beginRow + 1) + 
                    ', ' +
                    str(macroInvocation.beginColumn + 1) + 
                ')' +
                ' -> ' +
                '(' + 
                    str(macroInvocation.endRow + 1) + 
                    ', ' +
                    str(macroInvocation.endColumn + 1) + 
                ')')
            if len(macroInvocation.parameterSet) > 0:
                invocationText.append(underlining)
                invocationText += macroInvocation.parameterSet
                invocationText.append(underlining)
            self.reportDebug(invocationText, 'debug-macro-invocation')

            # See if the user requests the macro parameter to be 
            # expanded before the macro.
            if macroInvocation.parameterExpansion:
                # The parameter should be expanded before the macro.
                self.scopeStack.open(macroInvocation.name)
                macroInvocation.parameterSet = self.convert(macroInvocation.parameterSet)
                self.scopeStack.close()

            # Recursively expand the macro.
            macroText = self.expandMacro(macroInvocation)

            # Debug-report the result of the macro-expansion.
            if len(macroText) > 0:
                self.reportDebug([underlining] + macroText + [underlining], 'debug-macro-expansion')
            else:
                self.reportDebug('', 'debug-macro-expansion')

            # Append the first line of the macro expansion to 
            # the end of the latest line.
            newText[-1] += macroText[0]
            # Append the other lines of the macro expansion to
            # the following lines.
            newText += macroText[1 :]

            # Move on.
            row = macroInvocation.endRow
            column = macroInvocation.endColumn

        # The last '' is extraneous.
        if newText[-1] == '':
            newText[-1 :] = []

        return newText

    def macro(self, macroName, macroParameter = ''):
        '''
        Expands a macro with the given parameter.

        macroName (string):
        The name of the macro.

        macroParameter (list of strings):
        The parameter of the macro.

        returns (list of strings):
        The output of the macro.
        '''
        text = ['[[' + macroName + ']]']
        if isinstance(macroParameter, six.string_types):
            if macroParameter.strip() != '':
                text[0] += ': ' + macroParameter
        elif len(macroParameter) > 0:
            text[0] += ':'
            for line in macroParameter:
                text.append('\t' + line)
        return self.convert(text)

    def htmlHeader(self):
        '''
        Returns the join of all htmlHead()'s of used macros.

        returns (list of strings):
        The join of all htmlHead()'s of used macros.
        '''
        htmlText = []
        for macro in self.usedMacroSet:
            htmlText += macro.htmlHead(self)
        return htmlText                                

def _leadingTabs(text, tabSize, tabsAtMost = -1):
    '''
    Returns the number of leading tabs.
    If there are 'tabSize' number of consecutive spaces, 
    then this will interpreted as a single tab.

    text (string):
    The text from which to count the leading tabs from.

    tabSize (integer):
    The number of spaces in a tab.

    tabsAtMost (integer):
    The number of leading tabs to count at most.
    If this is negative, then the number of tabs
    to count is not limited.

    returns (integer, integer):
    The first number of is the number of leading tabs,
    as defined above. The second number is the number
    of leading characters taking part to this count.
    '''
    tabs = 0
    consecutiveSpaces = 0
    characters = 0;
    for c in text:
        if c == '\t':
            tabs += 1
            characters += consecutiveSpaces + 1
            consecutiveSpaces = 0
        elif c == ' ':
            consecutiveSpaces += 1
            if consecutiveSpaces == tabSize:
                # Interpret the spaces as a single tab.
                tabs += 1
                characters += consecutiveSpaces
                consecutiveSpaces = 0
        else:
            break

        if tabsAtMost >= 0 and tabs == tabsAtMost:
            break

    return tabs, characters

def _removeLeadingTabs(text, tabSize, tabsAtMost = -1):
    '''
    Removes at most a given number of leading tabs from the text.

    If there are less leading tabs than the given number, then all 
    the leading tabs are removed.

    text (string):
    The text from which to remove the leading tabs from.

    tabSize (integer):
    The number of spaces in a tab.

    tabsAtMost (integer):
    The number of leading tabs to remove at most. If this is negative,
    then all leading tabs will be removed.

    returns (string):
    The text with leading tabs removed.
    '''

    tabs, characters = _leadingTabs(text, tabSize, tabsAtMost)
    return text[characters :]