#@+leo-ver=5-thin
#@+node:ekr.20101110093449.5822: * @file mod_leo2ascd.py
__version__ = ".7" # Set version for the plugin handler.

import leo.core.leoGlobals as g
import leo.core.leoPlugins as leoPlugins
import re
import os
#@+<< define classes >>
#@+node:ekr.20141110071911.17: ** << define classes >>
#@+others
#@+node:ekr.20101110094152.5824: *3* class _AssignUniqueConstantValue
class   _AssignUniqueConstantValue(object):
    """ Provide unique value to be used as a constant """

    #@+others
    #@+node:ekr.20101110094152.5825: *4* __init__
    def __init__(self):
        self.UniqueInternalValue = 0
        self.Assign_at_start()

    #@+node:ekr.20101110094152.5826: *4* class ConstError
    class ConstError(TypeError):
        pass
    #@+node:ekr.20101110094152.5827: *4* __setattr__
    def __setattr__(self,name,value):

        if name in self.__dict__:
            if name != "UniqueInternalValue":
                raise self.ConstError("Can't rebind const(%s)"%name)
        self.__dict__[name]=value
    #@+node:ekr.20101110094152.5828: *4* Assign_at_start
    def Assign_at_start(self):
        self.END_PROGRAM = self.Next()   # signal abort
        self.LINE_WAS_NONE = self.Next() # describe last line printed
        self.LINE_WAS_CODE = self.Next()
        self.LINE_WAS_DOC  = self.Next()
        self.LINE_WAS_HEAD = self.Next()
        self.LINE_PENDING_NONE  = self.Next() # describe next line to be printed
        self.LINE_PENDING_CODE  = self.Next()
        self.LINE_PENDING_DOC   = self.Next()
    #@+node:ekr.20101110094152.5829: *4* Next
    def Next(self):
        self.UniqueInternalValue += 1
        return(self.UniqueInternalValue)
    #@-others
#@+node:ekr.20101110094152.5830: *3* class _ConfigOptions
class _ConfigOptions(object):
    """Hold current configuration options."""
    #@+others
    #@+node:ekr.20101110094152.5831: *4* __init__
    def __init__(self):
        self.current = {}
        self.default = {}
        self.default["maxCodeLineLength"] = '76'
        self.default["delimiterForCodeStart"] = '~-~--- code starts --------'
        self.default["delimiterForCodeEnd"]   = '~-~--- code ends ----------'
        self.default["delimiterForCodeSectionDefinition"] = '*example*'
        self.default["headingUnderlines"] = '=-~^+'
        self.default["asciiDocSectionLevels"] = '5'
        self.default["PrintHeadings"] = "on"

    #@+node:ekr.20101110094152.5832: *4* __GetNodeOptions
    def __GetNodeOptions(self, vnode):
        bodyString = vnode.bodyString()
        lines = bodyString.splitlines()
        for line in lines:
            containsAscConfigDirective = patternAscDirectiveConfig.match(line)
            if containsAscConfigDirective:
                # Leo uses unicode, convert to plain ascii
                name = str(containsAscConfigDirective.group(1))
                value = str(containsAscConfigDirective.group(2))
                if name in self.current:
                    self.current[name] = value
                else:
                    g.es(vnode.headString())
                    g.es("  No such config option: %s" % name)

    #@+node:ekr.20101110094152.5833: *4* GetCurrentOptions
    def GetCurrentOptions(self,c,p):
        self.current.clear()
        self.current = self.default.copy()
        self.__GetNodeOptions(c.rootPosition())
        self.__GetNodeOptions(p)

    #@-others
#@-others
#@-<< define classes >>
#@+<< constants >>
#@+node:ekr.20101110094152.5834: ** << constants >>
CV = _AssignUniqueConstantValue()
CV.NODE_IGNORE = CV.Next()      # demo of adding in code
Conf = _ConfigOptions()

# compile the patterns we'll be searching for frequently
# pylint: disable=anomalous-backslash-in-string
patternSectionName = re.compile("\<\< *(.+?) *\>\>")
patternSectionDefinition = re.compile("(\<\< *)(.+?)( *\>\>)(=)")
patternDirective = re.compile(r"^@")
patternCodeDirective = re.compile(r"^(@c *$)|(@code)")
patternDocDirective = re.compile(r"^(@ |@doc)(.*)")
patternRootDirective = re.compile(r"^@root\s+(.+)")
patternAscDirective = re.compile(r"^@asc")

# New Leo2AsciiDoc directives
patternAscDirectiveConfig = re.compile(r'^@ascconfig\W+(\w+)\s+(\S+)')
patternAscDirectiveFile = re.compile(r'^@ascfile *"*([\w\\/\.]*)"*')
patternAscDirectiveExit = re.compile(r"^@ascexit")
patternAscDirectiveIgnore = re.compile(r"^@ascignore")
patternAscDirectiveSkip = re.compile(r"^@ascskip")
patternAscDirectiveSkipToggle = re.compile(r"^@ascskip\s*(\w+)+.*")
#@-<< constants >>
#@+others
#@+node:ekr.20140920145803.17999: ** init
def init():
    '''Return True if the plugin has loaded successfully.'''
    leoPlugins.registerHandler(('new','menu2'), CreateAscMenu)
    g.plugin_signon(__name__)
    return True
#@+node:ekr.20140920145803.18000: ** Functions
#@+node:ekr.20101110094152.5837: *3* CodeChunk
def CodeChunk(text, width=72):
    """Split a line of text into a list of chunks not longer
    than width."""
    chunkList = []
    chunkStart = 0
    chunkEnd = 0
    lastSpacePosition = 0
    shortWidth = width - 4
    prefix = ''
    # suffix = ' \\'
    textLen = len(text)
    if width > textLen:
        chunkList.append(text)
    else:
        while chunkEnd < textLen:
            if len(chunkList) > 0:
                prefix = '  '
            chunkEnd = chunkStart + shortWidth
            if chunkEnd > textLen:
                chunkList.append(prefix + text[chunkStart:])
                chunkEnd = textLen          # get out of jail
            else:
                lastSpacePosition = text.rfind(' ',chunkStart, chunkEnd +1)
                if lastSpacePosition != -1:  # success
                    chunkList.append(prefix + text[chunkStart:lastSpacePosition] + ' \\')
                    chunkStart = lastSpacePosition + 1
                else:
                    chunkEnd = chunkStart + shortWidth
                    chunkList.append(prefix + text[chunkStart:chunkEnd] + ' \\')
                    chunkStart = chunkEnd
    return chunkList
#@+node:ekr.20101110094152.5847: *3* CreateAscMenu
def CreateAscMenu(tag,keywords):
    """Create the Outline to AsciiDoc menu item in the Export menu."""
    c = keywords.get('c')
    if not c: return
    exportMenu = c.frame.menu.getMenu('export')
    newEntries = (
        ("-", None, None),
        ("Export all to &AsciiDoc","Alt+Shift+A",WriteAll),
        ("Export current tree to AsciiDoc","Alt+Shift+T",WriteTreeOfCurrentNode),
        ("Log all root and ascfile to log pane","Alt+Shift+L",WriteAllRoots),
    )
    c.frame.menu.createMenuEntries(exportMenu,newEntries,dynamicMenu=True)
#@+node:ekr.20101110094152.5836: *3* GetAscFilename
def GetAscFilename(c,p):
    'Checks a node for a filename directive.'
    # f is the Leo outline
    ascFileName = None
    lines = p.b.splitlines()
    for line in lines:
        containsAscFileDirective = patternAscDirectiveFile.match(line)
        if containsAscFileDirective:
            ascFileName = containsAscFileDirective.group(1)
            if (ascFileName is not None):
                base = os.path.split(c.mFileName)[0]  # linux or windows
                if (((base[0]=="/") and (ascFileName[0] != "/")) or
                   ((base[1]==":") and (ascFileName[1] != ":"))):
                    # no full pathname specified
                    ascFileName = os.path.join(base, ascFileName)
                Conf.GetCurrentOptions(c,p)
    return ascFileName
#@+node:ekr.20101110094152.5835: *3* SectionUnderline
def SectionUnderline(h,level,v):
    'Return a section underline string.'
    asciiDocSectionLevels = int(Conf.current["asciiDocSectionLevels"])
    if level < 0:
        g.es("Section level is less than 1:\n  %s" % v.headString())
        level = 1
    elif level > asciiDocSectionLevels - 1:
        g.es("Section level is more than maximum Section Levels: %d\n  %s" % (
            asciiDocSectionLevels, v.headString()))
        level = asciiDocSectionLevels - 1
    str = Conf.current["headingUnderlines"][level]  #'
    return str*max(len(h),1)
#@+node:ekr.20101110094152.5843: *3* WriteAll
def WriteAll(c):

    p = c.rootPosition()
    while p:
        ascFileN = GetAscFilename(c,p)
        if ascFileN:
            WriteTreeAsAsc(p,ascFileN)
            p.moveToNodeAfterTree()
        else:
            p.moveToThreadNext()
#@+node:ekr.20101110094152.5845: *3* WriteAllRoots
def WriteAllRoots(c):
    "Writes @root directive and/or @ascfile directive to log pane."
    patternAscDirectiveFile = re.compile(r'^@ascfile')
    patternRoot = re.compile(r'^@root')
    g.es('Looking for @root or @ascfile.')
    for p in c.all_positions():
        printedHeading = False
        for line in p.b.splitlines():
            printLine = False
            containsAscFileDirective = patternAscDirectiveFile.match(line)
            if containsAscFileDirective:
                printLine = True
            containsRootDirective = patternRoot.match(line)
            if containsRootDirective:
                printLine = True
            if printLine:
                if not printedHeading:
                    g.es(p.h)
                    printedHeading = True
                g.es('  ' + line)
#@+node:ekr.20101110094152.5839: *3* WriteNode
def WriteNode(v,startinglevel, ascFile):
    'Writes the contents of the node v to the ascFile.'
    containsAscIignore = None
    skippingDocLines = False
    startingCodeExtract = False
    inCodeExtract = False
    statusOfWriteOutputLine = None

    def WriteOutputLine(lineString):
        'Writes a line of text to the output file.'
        try:
            ascFile.write("%s\n" % lineString)
        except IOError:
            g.es("Could not write to output file: %s" % ascFile.name)
            # statusOfWriteOutputLine = CV.END_PROGRAM

    # Get the headline text.
    h = v.headString()
    markedupAsSection = patternSectionName.match(h)
    if markedupAsSection:
        h = markedupAsSection.group(1) # dump the angle brackets

    # Put the body text into a list of lines.
    bodyString = v.bodyString()
    lines = bodyString.splitlines()
    lastLinePrintedType = CV.LINE_WAS_NONE
    # By default, nodes start with a code section.
    pendinglineType = CV.LINE_PENDING_CODE
    for line in lines:
        containsRootDirective = None
        containsSectionDefinition = patternSectionDefinition.match(line)
        if containsSectionDefinition:
            # dump the angle brackets, etc.
            #       line = containsSectionDefinition.group(2)  + '\n' + \
            #       (SectionUnderline(containsSectionDefinition.group(2),2,v))
            line = '.' + containsSectionDefinition.group(2)
            pendinglineType = CV.LINE_PENDING_CODE
            startingCodeExtract = True

        containsCodeDirective = patternCodeDirective.match(line)
        if containsCodeDirective:
            pendinglineType = CV.LINE_PENDING_CODE
            skippingDocLines = False
            continue                    # don't print this line

        containsDocDirective = patternDocDirective.match(line)
        if containsDocDirective:
            pendinglineType = CV.LINE_PENDING_DOC
            if containsDocDirective.group(2):
                # it is legal to have text on the same line
                # as a doc directive.
                line = containsDocDirective.group(2)
            else:
                continue

        containsAscDirective = patternAscDirective.match(line)
        if containsAscDirective:
            containsAscIignore = patternAscDirectiveIgnore.match(line)
            if containsAscIignore:
                break
            containsAscExit = patternAscDirectiveExit.match(line)
            if containsAscExit:
                break
            containsAscSkip = patternAscDirectiveSkip.match(line)
            if containsAscSkip:
                containsAscSkipDirectiveToggle = patternAscDirectiveSkipToggle.match(line)
                if containsAscSkipDirectiveToggle:
                    if containsAscSkipDirectiveToggle.group(1).lower() == "on":
                        skippingDocLines = True
                    elif containsAscSkipDirectiveToggle.group(1).lower() == "off":
                        skippingDocLines = False
                continue
        containsOtherDirective = patternDirective.match(line)
        if containsOtherDirective:
            containsRootDirective = patternRootDirective.match(line)
            if containsRootDirective:
                line = "*note*\nThe code sections that follow, when extracted from a " + \
                       "Leo outline, will be located in: %s\n*note*" % \
                       containsRootDirective.group(1)
            else:
                continue
        # We have something to print, so print heading.
        if lastLinePrintedType == CV.LINE_WAS_NONE:
            if (len(h) > 0) and (Conf.current["PrintHeadings"] == "on"):
                WriteOutputLine("\n\n%s" % h)
                WriteOutputLine(SectionUnderline(h,v.level()-startinglevel,v))
                lastLinePrintedType = CV.LINE_WAS_HEAD
        if pendinglineType == CV.LINE_PENDING_DOC:
            if lastLinePrintedType != CV.LINE_WAS_DOC and \
               lastLinePrintedType != CV.LINE_WAS_HEAD:
                WriteOutputLine("%s" % Conf.current["delimiterForCodeEnd"])
                if inCodeExtract:
                    WriteOutputLine("\n%s" % Conf.current["delimiterForCodeSectionDefinition"])
                    inCodeExtract = False
                lastLinePrintedType = CV.LINE_WAS_DOC
            if skippingDocLines:
                if not containsRootDirective: # always document a root directive
                    continue
        if pendinglineType == CV.LINE_PENDING_CODE:
            if lastLinePrintedType != CV.LINE_WAS_CODE:
                if startingCodeExtract:
                    WriteOutputLine("\n%s" % line)
                    WriteOutputLine("%s" % Conf.current["delimiterForCodeSectionDefinition"])
                    inCodeExtract = True
                    line = ''
                WriteOutputLine("%s" % Conf.current["delimiterForCodeStart"])
                lastLinePrintedType = CV.LINE_WAS_CODE
                if startingCodeExtract:
                    startingCodeExtract = False
                    continue
            maxCodeLineLength = int(Conf.current["maxCodeLineLength"])
            if len(line) <= maxCodeLineLength:
                WriteOutputLine("%s" % line)
            elif len(line.rstrip()) <= maxCodeLineLength:
                WriteOutputLine("%s" % line.rstrip())
            else:
                lineList = CodeChunk(line, maxCodeLineLength)
                for ln in lineList:
                    WriteOutputLine("%s" % ln)
            lastLinePrintedType = CV.LINE_WAS_CODE
        else:
            WriteOutputLine("%s" % line)
        if statusOfWriteOutputLine is not None:
            return statusOfWriteOutputLine

    if lastLinePrintedType == CV.LINE_WAS_CODE:
        WriteOutputLine("%s" % Conf.current["delimiterForCodeEnd"])
        if inCodeExtract:
            WriteOutputLine("\n%s" % Conf.current["delimiterForCodeSectionDefinition"])
            inCodeExtract = False
    if containsAscIignore is not None:
        return CV.NODE_IGNORE # flag ignore tree to caller
#@+node:ekr.20101110094152.5838: *3* WriteTreeAsAsc
def WriteTreeAsAsc(p,fn):
    'Writes the tree under p to the file ascFile'
    try:
        ascFile = open(fn,'w')
    except IOError:
        g.es("Could not open output file: %s" % fn)
        return
    stopHere = p.nodeAfterTree()
    startinglevel = p.level()
    while p and p != stopHere:
        val = WriteNode(p,startinglevel,ascFile)
        if  val == CV.END_PROGRAM:
            ascFile.close()
            return
        elif val == CV.NODE_IGNORE:
            p.moveToNodeAfterTree() # ran into an @ascignore
        else:
            p.moveToThreadNext()
    ascFile.close()
    g.es('wrote: %s' % fn)
#@+node:ekr.20101110094152.5841: *3* WriteTreeOfCurrentNode (not used)
def WriteTreeOfCurrentNode(c):

    p = c.p
    while p:
        ascFileN = GetAscFilename(c,p)
        if ascFileN:
            break
        else:
            p.moveToParent()
    if ascFileN is None:
        g.es("Sorry, there was no @ascfile directive in this outline tree.")
    else:
        WriteTreeAsAsc(p,ascFileN)
#@-others
#@@language python
#@@tabwidth -4
#@-leo
