add muos to index
[pipapo-website/.git] / menugen.py
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3 # -*- python -*-
4 ##
5 ## menugen.py  -  generate naviagtion menu for the Lumiera.org website
6 ##
7
8 #  Copyright (C)         Lumiera.org
9 #    2011,               Hermann Vosseler <Ichthyostega@web.de>
10 #
11 #  This program is free software; you can redistribute it and/or
12 #  modify it under the terms of the GNU General Public License as
13 #  published by the Free Software Foundation; either version 2 of
14 #  the License, or (at your option) any later version.
15 #####################################################################
16
17
18 import os
19 import re
20 import sys
21 import types
22 import string
23 from os import path
24 from optparse import OptionParser
25 from itertools import ifilter,chain
26
27 import menuformat   # defines specific HTML to generate
28
29
30 #------------CONFIGURATION-----------------------------
31 PROGVER      = 0.1
32 PROGNAME     = 'menugen'
33 TREE_ROOT    = 'root'
34 INDEX_NAME   = 'index'
35 SRC_FILE_EXT = '.txt'
36 WEBPAGE_EXT  = '.html'
37 menuSpec_RE  = re.compile(r'^//\s*MENU\s*:\s*', re.IGNORECASE)
38 #------------CONFIGURATION-----------------------------
39
40
41
42
43 def addPredefined():
44     ''' populate the menu with a set of basic nodes createing a backbone,
45         which can then be extended by values extracted from individual pages.
46     '''
47     root = Node(TREE_ROOT, label='pipapo.org')
48     #proj = root.linkChild('project')
49     
50     root.enabled(False)   # suppress adding any further children
51     
52     # explicitly recurse into the following subdirectories
53     #root.discover(includes='project documentation download contribute search devs-vault'.split())
54     
55     # proj.linkChild ('screenshots')
56     #proj.linkChild ('faq')
57     
58     # ordering of news entries
59     Node('news').sortChildren(reverse=True) \
60                 .putChildLast('old_news')
61
62
63
64
65 # -----------parse-cmdline----------------------------
66 def parseAndDo():
67     
68     usage = "usage: %prog [options] [directory]"
69     
70     parser = OptionParser(usage=usage, version="%s %1.2f" % (PROGNAME,PROGVER)) 
71     
72     parser.add_option("-p", "--predefined",    action="store_true"
73                      ,help="populate the menu with a built-in backbone structure")
74     parser.add_option("-s", "--scan",          action="store_true"
75                      ,help="scan recursively to discover pages. Starts in current directory, unless another starting point is explicitly stated")
76     parser.add_option("-d", "--debug",         action="store_true"
77                      ,help="diagnostic dump of internal datastructures")
78     parser.add_option("-t", "--text",          action="store_true"
79                      ,help="output textual representation of the menu")
80     parser.add_option("-w", "--webpage",       action="store_true"
81                      ,help="generate active menu webpage (HTML)")
82     
83     (options, args) = parser.parse_args()
84     
85     global startdir
86     if len(args) >= 1:
87         startdir = args[0]
88     else:
89         startdir = '.'
90     startdir = path.abspath(startdir)
91     if len(args) > 1:
92         __warn('additional arguments "%s" ignored.' % args[1:])
93     if not (options.debug or options.text or options.webpage):
94         __warn('no output generation option specified.')
95     
96     
97     #------dispatch-action--------------
98     if options.predefined:
99         addPredefined()
100     if options.scan:
101         discoverPages(startdir)
102     if options.debug:
103         dumpTables()
104     if options.webpage:
105         generateHtmlMenu()
106     elif options.text:
107         generateTextMenu()
108     
109     sys.exit(0)
110
111
112
113 ##################### Parsing and Discovery ######################
114
115 def discoverPages (startdir):
116     if not isDir(startdir):
117         __err('unable to scan/discover contents: '+
118               '"%s" is not an accessable directory' % startdir)
119     
120     discoverLocation (TREE_ROOT)
121
122
123 def discoverLocation(loc, parent=None):
124     file = findSource (loc)
125     node = scanSource (loc, file, parent)
126     for child in discoverChildren(node, loc):
127         discoverLocation (child, parent=node)
128
129
130
131 isDir  = lambda p: p and path.isdir(p)  and os.access(p,os.R_OK)
132 isFile = lambda p: p and path.isfile(p) and os.access(p,os.R_OK)
133
134 nameID = lambda p: p and path.splitext(path.basename(p))[0]
135
136
137 ########################
138 ### Discovery strategies
139
140 def expandRoot (loc):
141     ''' expand a relative path
142         starting with the TREE_ROOT token.
143         @return: absolute path based on the global startdir
144     '''
145     global startdir
146     if (loc and loc.startswith(TREE_ROOT)):
147         return path.join(startdir, stripPrefix(loc,TREE_ROOT))
148     else:
149         return loc
150
151
152 def relativeRoot (loc):
153     global startdir
154     if not loc: return TREE_ROOT
155     if loc.startswith(TREE_ROOT): return loc
156     if not path.isabs(loc):
157         loc = path.join(startdir, loc)
158     
159     loc = path.normpath(loc)
160     if not loc.startswith(startdir):
161         __err('unable to establish page URL: '
162              +'Location %s is outside webroot %s' %(loc, startdir))
163     loc = stripPrefix(loc, startdir)
164     return path.join (TREE_ROOT, loc)
165
166
167 def stripPrefix (loc,prefix):
168     if loc and loc.startswith(prefix):
169         loc = loc[len(prefix):]
170         if loc.startswith('/'):
171             loc = loc[1:]
172     return loc
173
174
175 def navigateRelative(location, subPath):
176     if path.isabs(subPath) or subPath.startswith(TREE_ROOT):
177         return expandRoot(subPath)    # use subPath as absolute path
178     else:
179         fullpath = expandRoot(location)
180         if not isDir(fullpath):
181             location = path.dirname(location)
182         return path.join(location, subPath)
183
184
185 def findSource (loc):
186     ''' strategy to find a relevant source file
187         corresponding to the given path location
188     '''
189     return buildFilename (loc, SRC_FILE_EXT)
190
191
192 def buildFilename (loc, FILE_EXT):
193     ''' strategy to build acceptable file names,
194         especially handling directory index files
195         @param loc: filename or location to search 
196         @param FILE_EXT: extension to require on files 
197         @return: an existing file or index file
198     '''
199     loc = expandRoot(loc)
200     if isDir(loc):
201         IndexFile = INDEX_NAME+FILE_EXT
202         filePath = path.join(loc,IndexFile)  # try name/index.EXT
203         if isFile(filePath):
204             return filePath
205         IndexFile = nameID(loc)+FILE_EXT
206         filePath = path.join(loc,IndexFile)  # try name/name.EXT
207         if isFile(filePath):
208             return filePath
209     (base,ext) = path.splitext(loc)
210     filePath = base+FILE_EXT                 # try name.EXT
211     if isFile(filePath):
212         return filePath
213     else:
214         return None
215
216     
217 def discoverChildren(node, currentLocation):
218     ''' get possible child locations to scan.
219         Decision where to search is delegated to the Node
220     '''
221     if not node: return []
222     
223     candidates = node.childDiscovery(currentLocation)
224     uniqueChildren = set (candidates)
225     return uniqueChildren;
226
227
228 def discoverChildrenRecursively(location):
229     ''' strategy how to proceed from a given location
230         to find possible child menu entries. Should
231         return None to stop recursive descent '''
232     target = expandRoot(location)
233     if isDir(target):
234         currentBase = nameID(location)
235         for entry in os.listdir(target):
236             eID = nameID(entry)
237             if not eID          : continue      # skip hidden files
238             if INDEX_NAME  ==eID: continue      # skip name/index.txt
239             if currentBase ==eID: continue      # skip name/name.txt
240             yield path.join(location,eID)
241     elif isFile(target):
242         yield path.join(path.dirname(target), nameID(target))
243
244
245 class DiscoveryRedirect:
246     ''' functor object to be used for discovering children.
247         like the simple #discoverChildrenRecursively strategy,
248         it it will be attached to a node to be used when discovering
249         possible sub-menu entries. But here this discovery can be
250         parametised with relative paths to include and names to filter
251     '''
252     def __init__(self, includes=[], excludes=[], srcdirs=[]):
253         filter = lambda loc: not nameID(loc) in excludes
254         self.excludesFilter = filter
255         self.includes = includes
256         self.srcdirs = srcdirs
257     
258     def __call__(self, location):
259         ''' when invoked to discover a start location,
260             apply our includes and excludes relative to that
261             location and yield an iterator for possible children
262             - each of our srcdirs is appended in turn to the
263               given location to form a new place to explore
264             - the includes are immediately treated as chilrden
265             - the bare name of all results found there
266               are checked against our excludes
267         '''
268         if not (self.srcdirs or self.includes):
269             self.srcdirs = ['.']
270         prependLocation = lambda relPath: navigateRelative(location, relPath)
271         buildSubIter = lambda loc: ifilter(self.excludesFilter, [loc])
272         buildDirIter = lambda loc: ifilter(self.excludesFilter,
273                                            discoverChildrenRecursively(loc))
274         toRetrieve = map(prependLocation, self.srcdirs )
275         toInclude = map(prependLocation, self.includes )
276         sources = map(buildDirIter, toRetrieve) + map(buildSubIter, toInclude)
277         return chain(*sources)
278     
279     
280     def chain(self, includes=[], excludes=[], srcdirs=[]):
281         prevFilter = self.excludesFilter
282         newfilter = lambda loc: not nameID(loc) in excludes and prevFilter(loc)
283         self.excludesFilter = newfilter
284         self.includes += includes
285         self.srcdirs  += srcdirs
286
287
288
289
290
291
292
293 def scanSource (location, srcFile, parentNode):
294     ''' scan the Asciidoc source text
295         for Menu specs embedded in comments.
296         Translate these into Node invocations
297         @note various Placement subclasses perform
298               the actual parsing and manipulations. 
299     '''
300     if not (srcFile and location): return None
301     if not parentNode:
302         nodeID = TREE_ROOT     # first Node encountered is /root by definition
303     else:
304         nodeID = nameID(location)
305         nodeID = path.join(parentNode.menuPath(), nodeID)
306     node = Node(nodeID)
307     
308     assert isFile(srcFile)
309     srcTxt = file(srcFile)
310     title = findTitle(srcTxt)
311     if title and node.hasNoLabel():
312         node.label = title
313     for spec in extractMenuSpecs(srcTxt):
314         ok = Placement.maybeParse (node, spec)
315         if not ok:
316             __warn('Source "%s":\t ignoring MENU: %s' % (nodeID, spec))
317     
318     # establish webpage path
319     webpage = buildFilename(location, WEBPAGE_EXT)
320     if not webpage:
321         __warn('unable to resolve(%s): %s' % (WEBPAGE_EXT,location))
322         webpage = srcFile
323     
324     # attach to menu if enabled
325     node.establishPosition (parentNode, webpage)
326     node.preprocess()
327     return node
328
329
330 def findTitle(srcFile):
331     assert not srcFile.closed
332     srcFile.seek(0)
333     title = None
334     for line in srcFile:
335         if not title:
336             title = line.strip()      # search first nonempty line
337         elif line.startswith('=' * (len(title)-1)):
338             return title              # immediately followed by '====....'
339         else:
340             return None               # everything else is invalid
341
342
343 def extractMenuSpecs(srcFile):
344     assert not srcFile.closed
345     srcFile.seek(0)
346     for line in srcFile:
347         match = menuSpec_RE.match (line)
348         if (match):
349             yield line[match.end():].strip()
350
351
352
353
354
355
356 ##################### Datastructures #############################
357
358 class NodeIndex:
359     def __init__(self):
360         self.cache = {}
361         self.all = []
362     
363     def find(self, key):
364         node = self.cache.get(key)
365         if not node:
366             for entry in self.all:
367                 if entry.matches(key):
368                     node = entry
369                     self.cache[key] = node
370         return node # may be None
371     
372     def add(self, key, node):
373         self.cache[key] = node
374         self.all.append(node)
375
376
377
378 class Node(object):
379     ''' Menu building block: An entry within the menu tree or DAG
380         Operations provided for building the structure and for adding atributes.
381         The final menu generation step traverses the node structure and accesses
382         the properties to generate the desired output
383     '''
384     index = NodeIndex()
385     
386     def __new__(type, id, **args):
387         ''' Factory function for nodes: retrieve existing or create new '''
388         if not id:
389             return None # neutral
390         if isinstance(id, Node):
391             return id   # idempotent
392         
393         assert isinstance(id, basestring)
394         node = Node.index.find(id)
395         if node == None:
396             node = object.__new__(type)
397             Node.index.add(id,node)
398         return node
399     
400     
401     def __init__(self, id, **args):
402         if not self._isInit():
403             self.id = normaliseComponentId(id)
404             self.url = None
405             self.label = titleFormatted(self.id)
406             self.parents = []
407             self.children = []
408             self.placements = []
409             self._active = True
410             self._relativePath = None
411             self._childDiscoveryStrategy = discoverChildrenRecursively
412         self.__dict__.update(args)
413     
414     def _isInit(self):
415         return 'id' in self.__dict__
416     
417     def __str__(self):
418         return 'Node(%s)' % self.id
419     
420     def enabled(self, yes=True):
421         self._active = yes
422     
423     def hasNoLabel(self):
424         return self.label == titleFormatted(self.id)
425     
426     
427     def __iter__(self):
428         ''' this is the main access interface:
429             after the menu tree has been populated,
430             the output generation will walk the tree
431             by visiting each node and recursing down.
432             @note invoking this method for the first time
433                   will execute any stored placement and
434                   processing constraints and specifications,
435                   thus bringing the menu into final shape
436         '''
437         self._applyPlacementSpecs()
438         return self.children.__iter__()
439     
440     def hasChildren(self): 
441         self._applyPlacementSpecs()
442         return 0 < len(self.children)
443     
444     def preprocess(self):
445         ''' used by directory discovery / file parsing '''
446         for placementSpec in self.placements:
447             placementSpec.preprocess (self)
448     
449     def _applyPlacementSpecs(self):
450         for placementSpec in self.placements:
451             placementSpec.execute (self)
452         self.placements = []
453     
454     
455     def __getattr__(self, methodID):
456         ''' enable DSL-style use of Node instances.
457             When invoking an unknown method, we'll try
458             all currently registered Placement spec handlers.
459             The first one able to handle that method will create
460             a Placement/Postprocessing entry, which will be stored
461             to be applied later, before fetching the children.
462         '''
463         return Placement.maybeInvoke (self, methodID)
464     
465     
466     
467     def linkChild (self, childId):
468         if not self._active: return None
469         child = Node(childId)
470         if child and not child in self.children:
471             self.children.append(child)
472             child.linkParent(self)
473         return child
474     
475     def linkParent (self, parentId):
476         if not self._active: return None
477         parent = Node(parentId)
478         if parent and not parent in self.parents:
479             self.parents.append(parent)
480             parent.linkChild(self)
481         return parent
482     
483     def detach(self):
484         ''' detach node from menu tree '''
485         for parent in self.parents:
486             parent.children.remove(self)
487         self.parents = []
488         self.children = []
489         self._active = False
490     
491     
492     def establishPosition (self,parent,webpage):
493         ''' define the primary coordinates of this menu entry.
494             @param parent: primary parent, defining the menuPath 
495             @param webpage: used to form the in-tree URL
496         '''
497         parent = self.linkParent(parent)
498         if parent:
499             self.parents.remove (parent)
500             self.parents.insert(0, parent)
501         if webpage:
502             self._relativePath = relativeRoot(webpage)
503     
504     
505     def matches (self, nodeKey):
506         ''' decide if this node is equivalent to the given nodeKey.
507             That is, either the key is our own ID, or it matches some
508             postfix part of our location in the menu and ends with our ID.
509             Typically this is used to retrieve an existing node by symbolic ID,
510             using the Node('id') constructor notation
511         '''
512         if not nodeKey: return False
513         if self.id == nodeKey: return True
514         mPath = self.menuPath()
515         return ('/' in nodeKey
516                and (   mPath.endswith(nodeKey))
517                     or nodeKey.endswith(mPath))
518     
519     
520     def childDiscovery(self, location):
521         ''' @param location: current starting point to find children 
522             @return iterator yielding possible child nodes to discover,
523         '''
524         return self._childDiscoveryStrategy (location)
525     
526     
527     def menuPath(self):
528         ''' constructs the path within the menu, starting with this node as leaf '''
529         if self.parents:
530             return self.parents[0].menuPath() + '/' + self.id
531         else:
532             return self.id
533     
534     
535     def getUrl(self):
536         ''' generate an URL suitable for accessing this entry.
537             Pages in-tree are given as site-absolute URL, while
538             external URLs are passed literally
539         '''
540         return self.url or normaliseLocalURL(self._relativePath or self.menuPath())
541     
542     
543     def getParentUrl(self):
544         if self.parents:
545             return self.parents[0].getUrl()
546         else:
547             return ''
548
549
550
551
552 ### Helpers for ID / URL handling
553
554 def normaliseComponentId(id):
555     return nameID(id)
556         
557
558 def normaliseLocalURL(url):
559     if url.startswith(TREE_ROOT):
560         url = url[4:]
561     if not url.startswith('/'):
562         url = '/'+url
563     return url
564
565
566 def titleFormatted(nameID):
567     nameID = nameID.strip()
568     if nameID.islower():
569         nameID = nameID.capitalize()
570     else:
571         # break 'CamelCase' words apart
572         nameID = camelCase_RE.sub(r'\1 \2', nameID)
573     return nameID
574
575 camelCase_RE = re.compile(r'([a-z])([A-Z])')
576
577
578
579
580 ##################### Attachment Control #########################
581
582 class Placement(object):
583     ''' baseclass for specifications
584         to control if and how some entries are
585         to be included into the generated menu tree.
586         Concrete Placement subclasses are (post)processing Instructions
587         They are either picked up by parsing a textual spec in Asciidoc page,
588         or by invoking a suitable method on an existing Node instance, e.g.
589         in the #addPredefined() function (internal DSL style).
590         Each Node instance may collect a list of individual Placement specs.
591         Typical examples being to sort the children, place an entry at a
592         specific point, or disable recursion or menu generation alltogether.
593     '''
594     handlers = [] # List of all usable kinds of Placement specs (Subclasses)
595     
596     def preprocess(self,node): pass
597     def execute(self, node):                   __err("abstract") # make this placement spec effective on the given node 
598     def acceptVerb(self, methodID, *arg,**kw): __err("abstract") # try to accept a method invocation to yield a placement
599     def acceptDSL(self, specificationTextLine):__err("abstract") # try to accept a textual spec from a file to be parsed
600     
601     @staticmethod
602     def maybeParse (targetNode, specification):
603         ''' try to find a suitable Placement subclass (handler),
604             which is able to accept the given DSL text line
605         '''
606         for handler in Placement.handlers:
607             try:
608                 placement = apply(handler).acceptDSL(specification)
609                 if placement:
610                     targetNode.placements.append(placement)
611                     return targetNode
612             except:
613                 print_warning("»%s« (%s)" % (sys.exc_type,sys.exc_value))
614         return None
615     
616     @staticmethod
617     def maybeInvoke (targetNode, methodID):
618         ''' @return functor to build the first suitable Placement subclass (handler),
619             which is able to process the given method call with the concrete arguments
620         '''
621         def tryVerb (*arg,**kw):
622             for handler in Placement.handlers:
623                 try:
624                     placement = apply(handler).acceptVerb(methodID, *arg,**kw)
625                     if placement:
626                         targetNode.placements.append(placement)
627                         return targetNode
628                 except: pass
629             print_warning('DSL-method "%s" not applicable for %s' % (methodID,targetNode))
630             return None
631         
632         return tryVerb
633
634
635
636
637 class PlaceChildAfter(Placement):
638     ''' concrete child placement specification,
639         denoting that a given child has to be placed at a
640         specific point in the list of the child nodes (sub menu entries)
641         of the menu entry currently in question. The position is given
642         by mentioning another child, after which to place the entry.
643         Alternatively, this placement spec may also be used to put
644         a child node at the start of the list
645     '''
646     
647     def __init__(self):
648         self.refPoint = None
649         self.childToPlace = None
650     
651     def __repr__(self):
652         return '|child %s after %s|' % (self.childToPlace,self.refPoint)
653     
654     def execute(self, node):
655         ''' This placement expresses an child ordering constraint
656             Do what needs to be done to the children of the given node,
657             in order to fulfill this constraint.
658             @note no ref point -> prepend child
659             @note ref point not found -> append child
660         '''
661         assert node
662         assert self.childToPlace
663         node.linkChild(self.childToPlace)
664         node.children.remove (self.childToPlace)
665         if not self.refPoint:
666             insertPoint = 0
667         elif self.refPoint and self.refPoint in node.children:
668             insertPoint = 1 + node.children.index (self.refPoint)
669         else:
670             insertPoint = len(node.children)
671         node.children.insert (insertPoint, self.childToPlace)
672     
673     
674     def acceptVerb(self, methodID, child, refPoint=None):
675         ''' when invoked as DSL method with the given parameters,
676             try to configure this child placement constraint such
677             as to reflect the given placement wish
678             @return this constraint or None, if the DSL method name
679                     or the concrete parameters are not suitable
680         '''
681         if 'putChildAfter' == methodID and child:
682             self.childToPlace = Node(child)
683             self.refPoint = Node(refPoint)
684             return self
685         if ('putChildFirst' == methodID or
686             'prependChild' == methodID ) and child:
687             self.childToPlace = Node(child)
688             self.refPoint = None
689             return self
690         if ('putChildLast' == methodID or
691             'appendChild' == methodID ) and child:
692             self.childToPlace = Node(child)
693             self.refPoint = 'atEnd'
694             return self
695         else:
696             return None
697     
698     
699     def acceptDSL(self, specificationTextLine):
700         ''' try to parse the spec into a child ordering constraint.
701             @return this constraint, suitably configured, or None
702         '''
703         match = childAfter_RE.search (specificationTextLine)
704         if (match):
705             self.childToPlace = Node (match.group(3))
706             self.refPoint     = Node (match.group(4))
707             assert self.childToPlace
708             return self
709         match = childPrepend_RE.search (specificationTextLine)
710         if (match):
711             self.childToPlace = Node (match.group(1))
712             self.refPoint     = None
713             assert self.childToPlace
714             return self
715         match = childAppend_RE.search (specificationTextLine)
716         if (match):
717             self.childToPlace = Node (match.group(3))
718             self.refPoint     = 'atEnd'
719             assert self.childToPlace
720             return self
721         else:
722             return None
723
724
725 # DSL Parsing...
726 quote_ = r'[\'\"]?'
727 s__    = r'\s*' 
728 nodeID_= s__+quote_ + r'(\w[\w\/\-\.]*)' + quote_+s__
729
730 attach_child_after_ = r'((attach|put)\s+)?child'+nodeID_+r'after'+nodeID_
731 prepend_child_      = r'prepend(?:\s+child)?'+nodeID_
732 append_child_       = r'((append|attach)\s+)?child\s+'+nodeID_
733
734 childAfter_RE   = re.compile (attach_child_after_, re.IGNORECASE)
735 childAppend_RE  = re.compile (append_child_,       re.IGNORECASE)
736 childPrepend_RE = re.compile (prepend_child_,      re.IGNORECASE)
737
738
739
740
741
742
743 class AttachExternalLink(Placement):
744     
745     def __init__(self):
746         self.subID = None
747         self.label = None
748         self.url = None
749     
750     def __repr__(self):
751         return '|attach "%s" -->%s|' % (self.subID,self.url)
752     
753     def execute(self, node):
754         assert node
755         assert self.url
756         if not self.subID:
757             self.subID = nameID(self.url)
758         if not self.label:
759             self.label = titleFormatted(self.subID)
760         
761         nodeID = path.join(node.id, self.subID)
762         newNode = Node(nodeID, label=self.label, url=self.url)
763         node.linkChild(newNode)
764     
765     
766     def acceptVerb(self, methodID, url, id=None, label=None):
767         if methodID in ['link', 'attachLink']:
768             self.url = url
769             self.subID = id
770             self.label = label
771             return self
772         else:
773             return None
774     
775     
776     def acceptDSL(self, specificationTextLine):
777         match = externalLink_RE.search (specificationTextLine)
778         if (match):
779             self.subID = match.group(1)
780             self.url   = match.group(2)
781             self.label = match.group(3)
782             return self
783         else:
784             return None
785
786
787
788 nodeSpec_       = r'(?:(?:child|node)'+nodeID_+')?'               # optional nodeID in group(1)
789 urlSpec_        = r'([^\s\[]+)'                                   # mandatory url/path in group(2)
790 labelSpec_      = r'\[([^\]]*)\]'                                 # label in [] as group(3) 
791 externalLink_   = nodeSpec_+'link:'+urlSpec_+labelSpec_+r'\s*$'   # id-url-label + only whitespace to line end
792
793 externalLink_RE = re.compile (externalLink_, re.IGNORECASE)
794
795
796
797
798
799
800 class DefineLabel(Placement):
801     
802     def __init__(self):
803         self.label = None
804         
805     def __repr__(self):
806         return '|pageLabel "%s"|' % self.label
807     
808     def preprocess(self,node):
809         assert node
810         if self.label: node.label = self.label
811         
812     def execute(self, node): pass
813     def acceptVerb(self, _): return None
814     
815     def acceptDSL(self, specificationTextLine):
816         match = labelSpec_RE.search (specificationTextLine)
817         if (match):
818             self.label = match.group(1).strip()
819             return self
820         else:
821             return None
822
823
824 labelSpec_   = r'(?:label|title)\s+(.+)'
825 labelSpec_RE = re.compile (labelSpec_, re.IGNORECASE)
826
827
828
829
830
831
832 class SortChildren(Placement):
833     
834     def __init__(self):
835         self.ascending = True
836     
837     def __repr__(self):
838         return '|sort children|'
839     
840     def execute(self, node):
841         ''' when applied, sort the child nodes alphabetically
842         '''
843         assert node
844         node.children.sort(key = lambda child: child.label.lower(), reverse = not self.ascending)
845     
846     
847     def acceptVerb(self, methodID, reverse=False):
848         if 'sortChildren' == methodID:
849             self.ascending = not reverse
850             return self
851         else:
852             return None
853     
854     
855     def acceptDSL(self, specificationTextLine):
856         match = sortChildren_RE.search (specificationTextLine)
857         if (match):
858             direction = match.group(1) or ''
859             direction = direction.lower()
860             if direction.startswith('desc') or direction.startswith('reverse'):
861                 self.ascending = False
862             return self
863         else:
864             return None
865
866
867 sortChildren_   = r'sort(?:\s+children)?(?:\s+(ascending|asc|descending|desc|reverse|reversed))?'
868 sortChildren_RE = re.compile (sortChildren_, re.IGNORECASE)
869
870
871
872
873
874 class EnableEntry(Placement):
875     
876     def __init__(self):
877         self.on = None
878         self.detach = False
879     
880     def __repr__(self):
881         return '|enable=%s detach=%s|' % (self.on,self.detach)
882     
883     def preprocess(self, node):
884         assert node
885         node.enabled(self.on)
886         if self.detach: node.detach()
887     
888     def execute(self, node):
889         assert node
890         assert None != self.on
891         if self.detach: node.detach()
892     
893     
894     def acceptVerb(self, methodID):
895         if 'detach' == methodID:
896             self.on = False
897             self.detach = True
898             return self
899         # note: class Node has an 'enabled' method, which can be invoked directly.
900         #       But this method only toggles activity, but doesn't detach
901         else:
902             return None
903     
904     
905     def acceptDSL(self, specificationTextLine):
906         match = activateNode_RE.search (specificationTextLine)
907         if (match):
908             if match.group(1):
909                 self.on = True
910                 self.detach = False
911             else:
912                 self.on = False
913             return self
914         match = detachNode_RE.search (specificationTextLine)
915         if (match):
916             self.on = False
917             self.detach = True
918         else:
919             return None
920
921
922 activateNode_   = r'(on|active|activate)|(off|disable|deactivate)'
923 detachNode_     = r'detach'
924
925 activateNode_RE = re.compile (activateNode_, re.IGNORECASE)
926 detachNode_RE   = re.compile (detachNode_,   re.IGNORECASE)
927
928
929
930
931
932 class RedirectDiscovery(Placement):
933     ''' this placement spec instructs the node
934         to serach for children at specific places ('includes')
935         or to exclude some of the discovered children
936     '''
937     
938     def __init__(self):
939         self.includes = []
940         self.excludes = []
941         self.srcdirs  = []
942     
943     def __repr__(self):
944         return '|discover %s (+%s) without %s|' % (self.includes,self.srcdirs,self.excludes)
945     
946     def preprocess(self, node):
947         strategy = node._childDiscoveryStrategy
948         if isinstance(strategy, DiscoveryRedirect):
949             strategy.chain(self.includes, self.excludes, self.srcdirs)
950         else:
951             strategy = DiscoveryRedirect(self.includes, self.excludes, self.srcdirs)
952         node._childDiscoveryStrategy = strategy 
953     
954     def execute(self, node): pass
955     
956     
957     def acceptVerb(self, methodID, includes=[], excludes=[], srcdirs=[]):
958         if 'discover' == methodID:
959             self.includes=includes
960             self.excludes=excludes
961             self.srcdirs =srcdirs
962             return self
963         else:
964             return None
965     
966     
967     def acceptDSL(self, specificationTextLine):
968         match = pullSrcDirSpec_RE.search (specificationTextLine)
969         if (match):
970             tokens = match.group(1)
971             tokens = listSplitter_RE.split(tokens)
972             self.srcdirs += tokens
973         else:
974             for match in discoverySpec_RE.finditer (specificationTextLine):
975                 tokens = match.group(2)
976                 tokens = listSplitter_RE.split(tokens)
977                 if 'include' == match.group(1):
978                     self.includes += tokens
979                 else:
980                     self.excludes += tokens
981         
982         if self.includes or self.excludes or self.srcdirs:
983             return self  # successfully parsed
984         else:
985             return None  # fail, maybe try other Placement spec
986
987
988 pathToken_       = r'[\w\./]+'
989 listDelim_       = r'\s*,\s*'
990 tokenList_       = '('+pathToken_+'(?:'+listDelim_+pathToken_+')*)'
991 discoverySpec_   = r'(include|exclude)\s+'+tokenList_
992 pullSrcDirSpec_  = r'include\s*dirs?\s+'+tokenList_
993
994 discoverySpec_RE = re.compile (discoverySpec_, re.IGNORECASE)
995 pullSrcDirSpec_RE= re.compile (pullSrcDirSpec_, re.IGNORECASE)
996 listSplitter_RE  = re.compile (listDelim_)
997
998
999
1000
1001
1002 ### Define all usable Placement kinds:
1003 Placement.handlers += [AttachExternalLink
1004                       ,RedirectDiscovery
1005                       ,PlaceChildAfter
1006                       ,SortChildren
1007                       ,DefineLabel
1008                       ,EnableEntry
1009                       ]
1010
1011
1012
1013 ##################### Output Generation ##########################
1014
1015 def dumpTables():
1016     print '\nMenu Tree:\n'
1017     print walkMenuTree (Dumper())
1018     print '(end)Menu Tree\n\n'
1019
1020
1021 def generateTextMenu():
1022     print walkMenuTree (TextFormatter())
1023
1024
1025 def generateHtmlMenu():
1026     from menuformat import HtmlGenerator, ScriptGenerator, generateHTML
1027     
1028     buildingBlocks = {'menuBody'  : walkMenuTree(HtmlGenerator())
1029                      ,'menuScript': walkMenuTree(ScriptGenerator())
1030                      }
1031     print generateHTML(buildingBlocks)
1032
1033
1034 def walkMenuTree (tool, subTree = Node(TREE_ROOT)):
1035     ''' Tree visitation
1036     '''
1037     if not subTree.hasChildren():
1038         tool.treatLeaf (subTree)
1039     else:
1040         tool.treatPrefix (subTree)
1041         for child in subTree:
1042             walkMenuTree (tool, child)
1043         tool.treatPostfix (subTree)
1044     
1045     return tool
1046
1047
1048
1049 class Formatter:
1050     
1051     def __init__(self):
1052         self.level = 0
1053         self.output = []
1054         self.formatters = {}
1055     
1056     def __str__(self):
1057         return '\n'.join (self.output)
1058     
1059     # Subclasse have to define:
1060     # INDENT, LEAF, PRE_SUB, POST_SUB and the showNode() method
1061     
1062     def format(self, template, **vars):
1063         engine = self.formatters.get(template)
1064         if not engine:
1065             engine = string.Template(template)
1066             self.formatters[template] = engine
1067         renderedText = engine.substitute(vars)
1068         return self.level * self.INDENT + renderedText
1069     
1070     def show(self, formattedData):
1071         self.output.append(formattedData)
1072     
1073     def treatLeaf(self, node):
1074         self.showNode(self.LEAF, node)
1075     
1076     def treatPrefix(self, node):
1077         self.showNode(self.PRE_SUB, node)
1078         self.level += 1
1079     
1080     def treatPostfix(self, node):
1081         self.level -=1
1082         self.show (self.format(self.POST_SUB, ID=node.id))
1083
1084
1085
1086 class TextFormatter(Formatter):
1087     
1088     INDENT   ='    |'
1089     LEAF     =' +-'
1090     PRE_SUB  =' +- |'
1091     POST_SUB ='    |____________'
1092     
1093     def showNode(self, template, node):
1094         self.show (self.format (template+' '+node.label))
1095
1096
1097
1098 class Dumper(Formatter):
1099     
1100     INDENT   ='\t'
1101     LEAF     ='Leaf'
1102     PRE_SUB  ='Sub_'
1103     POST_SUB ='--(end)$ID----------\n'
1104     
1105     
1106     def showNode(self, kind, node):
1107         self.show (self.format ('%s: "%s"' % (kind, node.id)))
1108         self.show (self.format ('....: url='+node.getUrl()))
1109         if (node.label != node.id):
1110             self.show (self.format ('....: label='+node.label))
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122 #
1123 # --Messages-and-errors-------------------------------------
1124 #
1125 def __err(text):
1126     print "--ERROR-------------------------"
1127     print >> sys.stderr, text
1128     sys.exit(255)
1129     
1130 def __exerr(text):
1131     __err(text + ": »%s« (%s)" % (sys.exc_type,sys.exc_value))
1132
1133 def __warn(text):
1134     print >> sys.stderr, "--WARNING--   " + str(text)
1135
1136 print_warning = __warn
1137
1138
1139
1140 if __name__ == "__main__":
1141     parseAndDo()