# Autogenerated by pnw, better do not edit ...
# Edit pnw.pnw in root directory instead.
# You have been warned ...
# -*- coding: utf-8 -*-
# vim: tw=70 tabstop=4 expandtab shiftwidth=4 softtabstop=4 ft=python
"""
    pnw
    ~~~

    pnw is a text processing tool with a wide range of applications.
    It can be used to compile chunks of text from various sources into
    a new text file. The advantage over cut-and-paste techniques is
    that changes in the source file are automatically followed.
    Secondly, we provide a thin wrapper around the pandoc document
    converter. Therefore, chunks of text can be converted from and to
    a wide range of lightweight markup languages as well as html and
    tex/latex. Although literate programming has become a little out
    of fashion, pnw can be used as a literate programming tool, too. Not
    surprisingly, pnw itself and its documentation is written using
    pnw.  pnw is remotely inspired by noweb and heavily inspired
    by (even borrowing part of the syntax from)
    [antiweb](https://pypi.python.org/pypi/antiweb/0.2.2). An
    important feature of pnw, which is not present in antiweb, is
    the support of namespaces. This allows to create reusable *block
    libraries* which can be imported and addressed in a *dot-notation
    syntax* similar to python modules. 
    
    A pnw file is a text file with tags defining named blocks of
    text. In a way this is similar to an xml file.  Tags may be hidden
    behind comment characters, such that the file remains processable
    by another program. The blocks of text from various pnw files
    can be processed and rendered by another pnw file.
    
    
    ## pnw tags 
    
    A line is recognized as a pnw tag if it is of the form

        <whitespace><one non-word character>@tagname(args,kwargs)

    The indent (length of the whitespace) is significant for some
    tags. The non-word character is completely arbitrary. Typically
    one might use a language specific comment character (`%` for
    latex, `#` for python) here. In the following we either use `#`,
    the python comment character, or omit it altogether and write tags
    in the form `@tagname`.  Here is a quick summary of the available
    pnw tags:

    **#@block(name,args,kwargs)**
     :  Starts block name; one may also write `chunk` instead of
        `block`, they are synonymous. A block ends at
    
        - the start of another block or an `#@end` with a smaller or equal indent,
        - another `#@` tag with a smaller indent,
        - the end of the file.

        Several blocks may have the same name if they have the same
        indent. Then these blocks are combined into a *chunk* of that
        name and the individual subblocks are named name1, name2 etc.

        **Beware**: due to namespace notation, Block names may NOT
        contain dots.

    **#@end**
     :  Explicit end of a block, indent is significant.
    
    **#@import(fname,nspace)**
     :   Imports file fname into nspace. This means that the blocks
         and chunks in fname can be addressed as nspace.name. Within 
         fname blocks/chunks can address each other without nspace prefix. 
         This is analogous to the python import statement.
    
    **#@include(name)**
     :   Includes the block/chunk name instead of this line.  The indent is
         significant: the indent of the |#@include| statement is added when 
         rendering the content of block/chunk name.

    All other lines outside of blocks are ignored. The content of each
    namespace nspace is collected in the block (resp. chunk if several
    files are imported into the same namespacefile fname) nspace.

    For further details, see the documentation.

    :copyright: (c) 2013 by Matthias Lesch ml@matthiaslesch.de
    :license:   Apache license, Version 2.0
"""
version = u'0.0.5'

""" Debugging related things """

def strace():
    pass


""" Imports """

import re
import subprocess
import os.path
import sys
import logging

""" Exceptions and Logging """

class PNWError(Exception):
    def __init__(self,line,msg=""):
        message = "Error in file %s, line %i, \n"%(\
                line.fname,line.lineno)+line.text[0:30]+"...\n"+msg
        Exception.__init__(self, message)

def errmsg(line,msg=""):
    return "Error in file %s, line %i, \n"%(\
            line.fname,line.lineno)+line.text[0:30]+"...\n"+msg
    
logging.basicConfig(filename = u'pnw.log', filemode = u'w', level = logging.DEBUG)
logger = logging.getLogger(u'pnw')

""" Module globals 
We have the following global variables:

**paths**
 :  Search path for files

**imports**
 :  Dictionary mapping imported filenames to their associated namespaces

**chunks**
 :  Dictionary of Chunks and Blocks

**roots**
 :  Set of Root Nodes

**rootchunks**
 :  Set of Root Chunks
"""

paths = ['.','../pgpm']
imports = {}     
chunks = {}
roots = set()
rootchunks = set()


""" The strings in `options`, `keywords`, and `texttypes`
are inserted into the module's namespace as uppercase. I.e.
COLLAPSE = `collapse` etc.
"""

options = [u'collapse',u'remove']
keywords = [u'chunk',u'block',u'end',u'import',u'include',u'path']
texttypes = [u'text',u'empty']
tags = list(keywords)
tags.extend(texttypes)
reserved_words = list()
reserved_words.extend(keywords)
reserved_words.extend(texttypes)
reserved_words.extend(options)

for item in reserved_words:
    globals()[item.upper()] = item

""" Helper Functions """
""" Pandoc Related """
def dummyconverter(source,to,format):
    "dummyconverter"
    return source

formats = {'md': 'markdown',
           'tex': 'latex'}

def _convert(source,to,format):
    """pandoc converter"""
    format = formats.get(format,format)
    to = formats.get(to,to)
    p = subprocess.Popen(
            ['pandoc','--to='+to,'--from='+format,'--listings'],
            stdin = subprocess.PIPE,
            stdout = subprocess.PIPE)
    return p.communicate(source)[0]
# check for pandoc
try:
    p = subprocess.Popen(
            ['pandoc','-h'],
            stdin = subprocess.PIPE,
            stdout = subprocess.PIPE)
    pandoc = True
except OSError:
    logger.error("pandoc not available")
    pandoc = False

if pandoc:
    convert = _convert
else:    
    convert = dummyconverter
    
def format_nspace(nspace,name):
    """ if name contains a dot it is returned verbatim, otherwise
    returns nspace.name.  """
    if u'.' in name or not nspace: 
        return name
    else:
        return nspace+u'.'+name

def findroot(b):
    """ Traverses the doc tree upwards and returns the root node
    corresponding to `b`."""
    while b.parent_block is not None:
        b = b.parent_block
    return b

def findrootchunk(b):
    """ Traverses the parent chunks upwards and returns the root chunk
    node corresponding to `b`."""
    while b.parent_chunk is not None:
        b = b.parent_chunk
    return b    

def findroots():
    """ Fills the sets `roots` and `rootchunks`."""
    for b in chunks.values():
        roots.add(findroot(b))
        b = findrootchunk(b)
        if isinstance(b,Chunk):
            rootchunks.add(b)

def parseargs(x):
    """ `x` is a string of the form

        arg1,...,argn,kwarg1=value1,...,kwargm=valuem
    
    `(kw)argj` may *not* contain comma or `=`. Positional args are
    collected in list `args`, keyword args are collected in dict
    `kwargs`. Returns `(args,kwargs)`."""
    rawargs=[item.strip() for item in x.split(',')]
    args=list()
    kwargs=dict()
    for item in rawargs:
        if not '=' in item:
            args.append(item)
        else:
            key,value = item.split('=',1)
            key = key.strip()
            value = value.strip()
            kwargs[key] = value
    return (args,kwargs)        

class Block():
    """
    This is the base class for nodes of the document tree. For each
    line of a pnw document an instance of `Block` is created. A
    `Block` needs the following default information:

    **text**
     :  The (line of) text
    **fname**
     :  The name of the file
    **lineno**
     :  Line number of the text line

    **Important attributes:**

    **parent_ block**
     :  the parent block in the document tree

    **parent_chunk**
     :  if several blocks have the same name they are combined into a
        `Chunk`. This attribute points to the parent `Chunk`

    **children**
     :  list of child nodes

    **args,kwards**
     :  list of args, resp. dictionary of keyword args found in ()
        after the tag

    **tag**
     :  name of the tag

    **indent**
     :  indentation (int) of the block/chunk

    **Public methods:**
        
        def expand(self,indent=0,depth=0,toformat=None):
        def render(self,indent=0,depth=0,toformat=None):

    `expand` returns a list of lines, render just joins them into
    a string. `indent` and `toformat` are hopefully self-explanatory.
    `depth` is the recursion level up to which `includes` are
    expanded. Beyond that level `include(foo)` is rendered as
    `<<foo>>` with the correct indentation.
    """
    def __init__(self,text,fname,lineno,parent_block = None, parent_chunk = None):
        self.text = text
        self.name = u''
        self.fname = fname
        self.lineno = lineno
        self.parent_block = parent_block
        self.parent_chunk = parent_chunk
        self.children = []
        self.parse()

    @property
    def nspace(self):
        return imports[self.fname]

    @property
    def format(self):
        return self.kwargs.get('format')

    def append(self,block):
        block.parent_block = self
        self.children.append(block)

    def expand(self,indent=0,depth=0,toformat=None):
        # this is really ugly, find better solution that
        # fiddling with negative indents
        if self.indent < 0:
            selfind = 0
        else:
            selfind = self.indent
        res = []
        if self.children and depth > 0:
            for child in self.children:
                res.extend(child.expand(indent+child.indent-selfind,depth-1,toformat=self.format))
        else:        
            if self.tag == TEXT: # return verbatim
                res.append(u' ' * indent + self.text[selfind:])
            elif self.tag == EMPTY:
                res.append(self.text)
            elif self.tag == INCLUDE: 
                # ch gets expanded *instead* of self, thus depth is same
                ch = chunks[self.args[0]]
                res.extend(ch.expand(indent,self.depth or\
                    depth,toformat=toformat))
            if self.tag in [BLOCK,CHUNK]:
                res.append(u' ' * (indent) +'<<'+self.name+'>>\n') 
        tmp = u'\n'.join(res)
        if self.tag in [BLOCK,CHUNK] and toformat and self.format:
            tmp = convert(tmp,toformat,self.format)
            res = tmp.split('\n')
        return res 

    def render(self,indent=0,depth=0,toformat=None):
        return u'\n'.join(self.expand(indent,depth,toformat))

    def parse(self):
        #TODO arbitrary non-word char instead of only % #
        m=re.match("(?P<indent>\s*)(%|#)@(?P<tag>\w*)(\((?P<args>.*)\)){0,1}",self.text)
        if m:
            d = m.groupdict()
            tag = d['tag'] #m.group(2)
            if tag not in tags:
                logger.error(errmsg(self,"Unknown Tag %s"%tag))
            self.tag = tag
            if d['args']:
                args,kwargs = parseargs(d['args'])
            else: 
                args = [u'',u'']
                kwargs = dict()
            self.indent = len(d['indent'])
            self.args = args
            self.kwargs = kwargs
            while len(self.args) < 2:
                self.args.append(u'')
            if self.tag in [BLOCK,CHUNK]:
                self.name = args[0]
                self.tag = BLOCK
            # tags we process just here on the fly
            if self.tag == PATH:
                paths.insert(0,args[0])
            if self.tag == INCLUDE:
                if self.args[1]:
                    try:
                        self.depth = int(self.args[1])
                    except:
                        raise PNWError(self,"optional argument of\
                        include tag must be a nonnegative integer") 
                else:
                    self.depth = None
            return (tag,args,self.indent)
        elif m is None:
            if self.text.strip() == u'':
                self.tag = EMPTY
                self.indent = 256
            else:    
                self.tag = TEXT
                self.indent = len(self.text) - len(self.text.lstrip())
            return None

    def _repr(self,depth=256,indent = 0):
        res = []
        if self.tag not in [TEXT,EMPTY]:
            try:
               res.append(u' '*indent+u'--- <BLOCK %s:%s:%i:%s: %s '%\
                       (self.name,self.fname,self.lineno,self.tag,\
                       self.text.rstrip("\n").strip())+u'>')
            except:
                #strace()
                pass
        if depth > 0:
            for child in self.children:
                res.extend(child._repr(depth-1,indent+4))
        return res

    def __repr__(self):
        if self.tag in keywords:
            return u'\n'.join(self._repr())
        else:
            return u'--- <TEXT %s:%i:%s: %s'%(self.fname,self.lineno,\
                    self.tag,self.text.rstrip("\n").strip())+u'>'

class Chunk(Block):
    """
    A Chunk is a node which is outside the normal document tree. A
    ``Chunk`` ``cname`` is created when a second ``Block`` ``cname``
    is found. All ``n`` Blocks of name ``cname`` become children of
    the newly created ``Chunk``.  The Blocks are renamed ``cnamei``
    where ``i`` runs from ``1`` to ``n``.
    """
    def __init__(self,name,block):
        Block.__init__(self,block.text,block.fname,block.lineno)
        self.__id__ = CHUNK
        self.indent = block.indent # cannot be parsed from block.text, 
                                   # since we play around with indent -1
        self.name = name
        self.tag  = CHUNK
        self.append(block)

    def append(self,block):
        if block.indent != self.indent:
            raise PNWError(block,"All blocks in a chunk must have the same\
                            indentation level")
        block.parent_chunk = self
        self.children.append(block)

    def _repr(self,depth=256):
        res = []
        res.append(u'<Chunk %s:%s:%i:%s: %s'%(self.name,self.fname,\
                self.lineno,self.tag,self.text.rstrip('\n'))+u'>')
        for child in self.children:
            res.extend(child._repr(depth = depth))
        return res    

    def __repr__(self):
        return u'\n'.join(self._repr())

def registerblock(name,block):
    """
    Fills the dictionary `chunks` and takes care of the tree
    structure. If the name does not yet exist, the block is added to
    the dictionary `chunks` with key `name`. Otherwise, if necessary,
    a new chunk `name` is created and the block is appended to its
    list of children.
    """
    nspace = block.nspace
    name = format_nspace(nspace,name)
    if name.startswith('API'):
        #strace()
        pass
    if name in chunks:
        ch = chunks[name]
        if isinstance(ch,Chunk):
            block.name += str(len(ch.children)+1)
        elif isinstance(ch,Block):
            ch.name += '1'
            chunks[ch.name] = ch
            ch = Chunk(name,ch)
            chunks[ch.name] = ch
            block.name += '2'
        ch.append(block)
        chunks[block.name] = block
        block.parent_chunk = ch
    else:
        chunks[name] = block
        block.name = name
        ch = block
    return ch

def pnwimport(fname,nspace,line=None):
    """
    This is the main function for using pnw as a python module.  The
    file `fname` is parsed and its blocks/chunks are read into the
    namespace `nspace`. The argument `line` is used internally for
    conveying error information and it can be ignored by external
    user.
    
    Indent is taken care of in chunk defs, not in imports, so the
    indentation of import statement is irrelevant.
    """
    if line is None:
        line = Block(u'Command Line','CLI',0)
    imports[fname] = nspace
    f = None
    for path in paths:
        tmpname = os.path.join(path,fname)
        try:
             f = open(tmpname,"r")
        except IOError:
            continue
        break
    if f is None:
        raise PNWError(line,"Could not open file %s"%fname)
    else:
        with f:
            linegen=enumerate(f.read().split('\n'),1)
            rootblock = Block(u'#@chunk(%s)'%nspace,fname,lineno = 0)
            rootblock.indent = -1
            registerblock(nspace,rootblock)
            parse_block(linegen,rootblock)

def parse_block(linegen,node): 
    lineno,text = linegen.next()
    line = Block(text,node.fname,lineno,node)
    while True:
        if line.tag == IMPORT:
            imp_fname,imp_nspace = line.args[0:2]
            if imp_fname in imports:
                raise PNWError(line, "pnw does dot support multiple \
                        imports of the same file, file already imported")
            pnwimport(imp_fname,imp_nspace,line)
        elif (line.tag in keywords and line.indent < node.indent) or\
                (line.tag in [END,BLOCK,CHUNK] and line.indent <= node.indent): 
            # our block ends here
            return (line,lineno)
        elif line.tag in [BLOCK,CHUNK]:
            registerblock(line.args[0],line)
            if REMOVE not in line.args:
               node.append(line) # we need to append this node
            line,lineno=parse_block(linegen,line)
            continue
        elif (line.tag in [EMPTY,TEXT]) and line.indent < node.indent:
                raise PNWError(line,u'indentation error')
        else:
            node.append(line)
        if line.tag in [INCLUDE]: 
            # is here, because node must get appended, list here those directives which need nspace adjustment
            line.args[0] = format_nspace(line.nspace,line.args[0])
        # if we are here, we need to get a new line or close up
        try:
            lineno,text = linegen.next()
            line = Block(text,node.fname,lineno,node)
        except StopIteration:
            # end of file reached, add a #@end with correct indentaton
            lineno+=1
            line = Block(u'#@end',node.fname,lineno,node)
            line.indent = -1
            return (line,lineno)

def showchunks(depth=sys.maxint):
    for c in chunks.values():
        print u'\n'.join(c._repr(depth))

cli_description = \
"""
This is pnw version %s 2013 Matthias Lesch (ml@matthiaslesch.de).
Parses a pnw file with pnw directives into an element tree. Renders
to standard output. Logfile can be found under pnw.log.

License: Apache Version 2.0
"""%version

def CLI():
    import argparse
    parser = argparse.ArgumentParser(description =cli_description,
    usage="pnw options [Filename]")
    parser.add_argument("nw",help="The pnw file")
    parser.add_argument("-R","--chunk",help="Extract and render chunk",type = str, default = '')
    parser.add_argument("-F","--file", help="Render whole file",action = 'store_true')
    parser.add_argument("-t",'--tree', help='Show Doc Tree',action = 'store_true')
    parser.add_argument('-f','--format', help = u'Output format',
            type = str, default = None)
    parser.add_argument("-d", help='depth of tree',type = int, default = 256)
    parser.add_argument('-a','--debug', help = 'show arguments and do\
    nothing', action='store_true')
    args=parser.parse_args()
    pnwimport(args.nw,u'')
    if args.debug:
        print args
        raise SystemExit('Exit')
    elif args.tree:
        print showchunks(depth = args.d)
    else:
        print chunks[args.chunk].render(depth = args.d, toformat = args.format)

if __name__ == "__main__":
    CLI()



