"Fossies" - the Fresh Open Source Software Archive

Member "zim-0.71.1/zim/export/template.py" (14 Mar 2019, 15715 Bytes) of package /linux/privat/zim-0.71.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "template.py" see the Fossies "Dox" file reference documentation and the last Fossies "Diffs" side-by-side code changes report: 0.69.1_vs_0.70.

    1 
    2 # Copyright 2008-2014 Jaap Karssenberg <jaap.karssenberg@gmail.com>
    3 
    4 
    5 '''This module defines the ExportTemplateContext, which is a dictionary
    6 used to set the template parameters when exporting.
    7 
    8 Export template parameters supported::
    9 
   10   generator
   11     .name   -- "Zim x.xx"
   12     .user
   13 
   14   title
   15 
   16   navigation    - links to other export pages (if not included here)
   17     home
   18     up
   19     prev            -- prev export file or None
   20     next            -- next export file or None
   21 
   22   links         -- links to other export pages (index & plugins / ...) - sorted dict to have Index, Home first followed by plugins
   23 
   24     link
   25         .name
   26         .basename
   27 
   28   pages         -- iter over special + content
   29     .special    -- iter special pages to be included (index / plugins / ...) - support get() as well here
   30     .content    -- iter pages being exported
   31 
   32         page
   33             .title      -- heading or basename
   34             .name / .section / .basename
   35             .heading
   36             .body       -- full body minus first heading
   37             .content    -- heading + body
   38             .headings(max_level)    -- iter over headings
   39 
   40                 headingsection
   41                     .level
   42                     .heading
   43                     .body
   44                     .content
   45 
   46             .links
   47             .backlinks
   48             .attachments
   49 
   50                 file
   51                     .basename
   52                     .mtime
   53                     .size
   54 
   55   options       -- dict with template options (for format)
   56 
   57   toc([page])           -- iter of headings in this page or all of pages
   58   index([section])  -- index of full export job, not just in this page
   59   uri(link|file)
   60   resource(file)
   61   anchor(page|section)
   62 
   63 From template base::
   64 
   65   range() / len() / sorted() / reversed()
   66   strftime()
   67   strfcal()
   68 
   69 Test in a template for single page export use: "IF loop.first and loop.last"
   70 '''
   71 
   72 import os
   73 
   74 from functools import partial
   75 
   76 import logging
   77 
   78 logger = logging.getLogger('zim.export')
   79 
   80 
   81 from zim import __version__ as ZIM_VERSION
   82 
   83 import zim.datetimetz as datetime
   84 
   85 from zim.utils import OrderedDict
   86 from zim.fs import format_file_size
   87 
   88 from zim.notebook import Path, LINK_DIR_BACKWARD, LINK_DIR_FORWARD
   89 
   90 from zim.formats import ParseTree, ParseTreeBuilder, Visitor, \
   91     FORMATTEDTEXT, BULLETLIST, LISTITEM, STRONG, LINK, HEADING
   92 
   93 from zim.templates import TemplateContextDict
   94 from zim.templates.functions import ExpressionFunction
   95 
   96 from zim.newfs import FileNotFoundError
   97 from zim.notebook.index import IndexNotFoundError
   98 from zim.notebook import Path
   99 
  100 
  101 class ExportTemplateContext(dict):
  102     # No need to inherit from TemplateContextDict here, the template
  103     # will do a copy first anyway to protect changing content in this
  104     # object. This means functions and proxies can assume this dict is
  105     # save, and only "options" is un-save input.
  106     #
  107     # This object is not intended for re-use -- just instantiate a
  108     # new one for each export page
  109 
  110     def __init__(self, notebook, linker_factory, dumper_factory,
  111         title, content, special=None,
  112         home=None, up=None, prevpage=None, nextpage=None,
  113         links=None,
  114         index_generator=None, index_page=None,
  115     ):
  116         '''Constructor
  117 
  118         When exporting one notebook page per export page ("multi file"),
  119         'C{content}' is a list of one page everytime. Even for exporting
  120         special pages, they go into 'C{content}' one at a time.
  121         The special pages are linked in 'C{links}' so the template can
  122         refer to them.
  123 
  124         When exporting multiple notebook pages to a single export page
  125         ("single file"), 'C{content}' is a list of all notebook pages a
  126         nd 'C{special}' a list.
  127 
  128         @param notebook: L{Notebook} object
  129         @param linker_factory: function producing L{ExportLinker} objects
  130         @param dumper_factory: function producing L{DumperClass} objects
  131         @param title: the export page title
  132         @param content: list of notebook pages to be exported
  133         @param special: list of special notebook pages to be exported if any
  134         @param home: link to home page if any
  135         @param up: link to parent export page if any
  136         @param prevpage: link to previous export page if any
  137         @param nextpage: link to next export page if any
  138         @param links: list of links to special pages if any, links are
  139         given as a 2-tuple of a key and a target (either a L{Path} or
  140         a L{NotebookPathProxy})
  141         @param index_generator: a generator function or that
  142         provides L{Path} or L{Page} objects to be used for the
  143         the C{index()} function. This method should take a single
  144         argument for the root namespace to show.
  145         See the definition of L{Index.walk()} or L{PageSelection.index()}.
  146         @param index_page: the current page to show in the index if any
  147         '''
  148         # TODO get rid of need of notebook here!
  149         template_options = TemplateContextDict({}) # can be modified by template
  150         self._content = content
  151         self._linker_factory = linker_factory
  152         self._dumper_factory = partial(dumper_factory, template_options=template_options)
  153         self._index_generator = index_generator or content
  154         self._index_page = index_page
  155 
  156         self.linker = linker_factory()
  157 
  158         def _link(l):
  159             if isinstance(l, str):
  160                 return UriProxy(l)
  161             elif isinstance(l, Path):
  162                 return NotebookPathProxy(l)
  163             else:
  164                 assert l is None or isinstance(l, (NotebookPathProxy, FileProxy))
  165                 return l
  166 
  167         if special:
  168             pages = ExportTemplatePageIter(
  169                 special=PageListProxy(notebook, special, self._dumper_factory, self._linker_factory),
  170                 content=PageListProxy(notebook, content, self._dumper_factory, self._linker_factory)
  171             )
  172         else:
  173             pages = ExportTemplatePageIter(
  174                 content=PageListProxy(notebook, content, self._dumper_factory, self._linker_factory)
  175             )
  176 
  177         self.update({
  178             # Parameters
  179             'generator': {
  180                     'name': 'Zim %s' % ZIM_VERSION,
  181                     'user': os.environ['USER'], # TODO allow user name in prefs ?
  182             },
  183             'title': title,
  184             'navigation': {
  185                 'home': _link(home),
  186                 'up': _link(up),
  187                 'prev': _link(prevpage),
  188                 'next': _link(nextpage),
  189             },
  190             'links': OrderedDict(), # keep order of links for iteration
  191             'pages': pages,
  192 
  193             # Template settings
  194             'options': template_options, # can be modified by template
  195 
  196             # Functions
  197             #~ 'toc': self.toc_function,
  198             'index': self.index_function,
  199             'pageindex': self.index_function, # backward compatibility
  200             'uri': self.uri_function,
  201             'anchor': self.anchor_function,
  202             'resource': self.resource_function,
  203         })
  204 
  205         if links:
  206             for k, l in list(links.items()):
  207                 l = _link(l)
  208                 self['links'][k] = l
  209 
  210 
  211     def get_dumper(self, page):
  212         '''Returns a L{DumperClass} instance for source page C{page}
  213 
  214         Only template options defined before this method is called are
  215         included, so only construct the "dumper" when you are about to
  216         use it
  217         '''
  218         linker = self._linker_factory(source=page)
  219         return self._dumper_factory(linker)
  220 
  221     #~ @ExpressionFunction
  222     #~ def toc_function(self):
  223         #~ # TODO
  224         #~ #       needs way to link heading achors in exported code (html)
  225         #~ #       pass these anchors through the parse tree
  226         #~
  227         #~ builder = ParseTreeBuilder()
  228         #~ builder.start(FORMATTEDTEXT)
  229         #~ builder.start(BULLETLIST)
  230 
  231         #~ for page in self._content:
  232             #~ current = 1
  233             #~ for level, heading in ...:
  234                 #~ if level > current:
  235                     #~ for range(current, level):
  236                         #~ builder.start(BULLETLIST)
  237                     #~ current = level
  238                 #~ elif level < current:
  239                     #~ for range(level, current):
  240                         #~ builder.end(BULLETLIST)
  241                     #~ current = level
  242 
  243                 #~ builder.start(LISTITEM)
  244                 #~ builder.append(LINK, {'href': ...}, anchor)
  245                 #~ builder.end(LISTITEM)
  246 
  247             #~ for range(1, current):
  248                 #~ builder.end(BULLETLIST)
  249             #~
  250         #~ builder.end(BULLETLIST)
  251         #~ builder.end(FORMATTEDTEXT)
  252 
  253         #~ tree = builder.get_parsetree()
  254         #~ if not tree:
  255             #~ return ''
  256 
  257         #~ print("!!!", tree.tostring())
  258         #~ dumper = self.get_dumper(None)
  259         #~ return ''.join(dumper.dump(tree))
  260 
  261     @ExpressionFunction
  262     def index_function(self, namespace=None, collapse=True, ignore_empty=True):
  263         '''Index function for export template
  264         @param namespace: the namespace to include
  265         @param collapse: if C{True} only the branch of the current page
  266         is shown, if C{False} the whole index is shown
  267         @param ignore_empty: if C{True} empty pages (placeholders) are
  268         not shown in the index
  269         '''
  270         if not self._index_generator:
  271             return ''
  272 
  273         builder = ParseTreeBuilder()
  274         builder.start(FORMATTEDTEXT)
  275         if self._index_page:
  276             expanded = [self._index_page] + list(self._index_page.parents())
  277         else:
  278             expanded = []
  279         stack = []
  280 
  281         if isinstance(namespace, PageProxy):
  282             namespace = Path(namespace.name)
  283         elif isinstance(namespace, str):
  284             namespace = Path(namespace)
  285 
  286         for path in self._index_generator(namespace):
  287             logger.info(path)
  288             if self._index_page and collapse \
  289             and not path.parent in expanded:
  290                 continue # skip since it is not part of current path
  291             #elif ignore_empty and not (path.hascontent or path.haschildren): - bug,  should be page.hascontent,  page.haschildren
  292             #   continue # skip since page is empty
  293 
  294             if not stack:
  295                 stack.append(path.parent)
  296                 builder.start(BULLETLIST)
  297             elif stack[-1] != path.parent:
  298                 if path.ischild(stack[-1]):
  299                     builder.start(BULLETLIST)
  300                     stack.append(path.parent)
  301                 else:
  302                     while stack and stack[-1] != path.parent:
  303                         builder.end(BULLETLIST)
  304                         stack.pop()
  305 
  306             builder.start(LISTITEM)
  307             if path == self._index_page:
  308                 # Current page is marked with the strong style
  309                 builder.append(STRONG, text=path.basename)
  310             else:
  311                 # links to other pages
  312                 builder.append(LINK,
  313                     {'type': 'page', 'href': ':' + path.name},
  314                     path.basename)
  315             builder.end(LISTITEM)
  316 
  317         for p in stack:
  318             builder.end(BULLETLIST)
  319         builder.end(FORMATTEDTEXT)
  320 
  321         tree = builder.get_parsetree()
  322         if not tree:
  323             return ''
  324 
  325         #~ print("!!!", tree.tostring())
  326         dumper = self.get_dumper(None)
  327         return ''.join(dumper.dump(tree))
  328 
  329     @ExpressionFunction
  330     def uri_function(self, link):
  331         if isinstance(link, UriProxy):
  332             return link.uri
  333         elif isinstance(link, NotebookPathProxy):
  334             return self.linker.page_object(link._path)
  335         elif isinstance(link, FilePathProxy):
  336             file = link._dest_file or link._file
  337             return self.linker.file_object(file)
  338         elif isinstance(link, str):
  339             return self.linker.link(link)
  340         else:
  341             return None
  342 
  343     @ExpressionFunction
  344     def anchor_function(self, page):
  345         # TODO remove prefix from anchors?
  346         if isinstance(page, (PageProxy, NotebookPathProxy)):
  347             return page.name
  348         else:
  349             return page
  350 
  351     @ExpressionFunction
  352     def resource_function(self, link):
  353         return self.linker.resource(link)
  354 
  355 
  356 class ExportTemplatePageIter(object):
  357 
  358     def __init__(self, special=None, content=None):
  359         self.special = special or []
  360         self.content = content or []
  361 
  362     def __iter__(self):
  363         for p in self.special:
  364             yield p
  365         for p in self.content:
  366             yield p
  367 
  368 
  369 class HeadingSplitter(Visitor):
  370 
  371     def __init__(self, max_level=None):
  372         self.max_level = max_level or 999
  373         self._builder = ParseTreeBuilder()
  374         self.headings = []
  375 
  376     def _split(self):
  377         self._builder.end(FORMATTEDTEXT)
  378         tree = self._builder.get_parsetree()
  379         if tree.hascontent:
  380             self.headings.append(tree)
  381         self._builder = ParseTreeBuilder()
  382         self._builder.start(FORMATTEDTEXT)
  383 
  384     def _close(self):
  385         tree = self._builder.get_parsetree()
  386         if tree.hascontent:
  387             self.headings.append(tree)
  388 
  389     def start(self, tag, attrib=None):
  390         if tag is HEADING and int(attrib['level']) <= self.max_level:
  391             self._split()
  392         self._builder.start(tag, attrib)
  393 
  394     def end(self, tag):
  395         self._builder.end(tag)
  396         if tag == FORMATTEDTEXT:
  397             self._close()
  398 
  399     def text(self, text):
  400         self._builder.text(text)
  401 
  402     def append(self, tag, attrib=None, text=None):
  403         if tag is HEADING and int(attrib['level']) <= self.max_level:
  404             self._split()
  405         self._builder.append(tag, attrib, text)
  406 
  407 
  408 class PageListProxy(object):
  409 
  410     def __init__(self, notebook, iterable, dumper_factory, linker_factory):
  411         self._notebook = notebook
  412         self._iterable = iterable
  413         self._dumper_factory = dumper_factory
  414         self._linker_factory = linker_factory
  415 
  416     def __iter__(self):
  417         for page in self._iterable:
  418             linker = self._linker_factory(source=page)
  419             dumper = self._dumper_factory(linker)
  420             yield PageProxy(self._notebook, page, dumper, linker)
  421 
  422 
  423 class ParseTreeProxy(object):
  424 
  425     @property
  426     def meta(self):
  427         return self._tree.meta or {}
  428 
  429     @property
  430     def heading(self):
  431         head, body = self._split_head()
  432         return head
  433 
  434     @property
  435     def body(self):
  436         try:
  437             head, body = self._split_head()
  438             if body:
  439                 lines = self._dumper.dump(body)
  440                 return ''.join(lines)
  441             else:
  442                 return ''
  443         except:
  444             logger.exception('Exception exporting page: %s', self._page.name)
  445             raise # will result in a "no such parameter" kind of error
  446 
  447     @property
  448     def content(self):
  449         try:
  450             if self._tree:
  451                 lines = self._dumper.dump(self._tree)
  452                 return ''.join(lines)
  453             else:
  454                 return ''
  455         except:
  456             logger.exception('Exception exporting page: %s', self._page.name)
  457             raise # will result in a "no such parameter" kind of error
  458 
  459     def _split_head(self):
  460         if not hasattr(self, '_severed_head'):
  461             if self._tree:
  462                 tree = self._tree.copy()
  463                 head, level = tree.pop_heading()
  464                 self._severed_head = (head, tree) # head can be None here
  465             else:
  466                 self._severed_head = (None, None)
  467 
  468         return self._severed_head
  469 
  470 
  471 class PageProxy(ParseTreeProxy):
  472 
  473     def __init__(self, notebook, page, dumper, linker):
  474         self._notebook = notebook
  475         self._page = page
  476         self._tree = page.get_parsetree()
  477         self._dumper = dumper
  478         self._linker = linker
  479 
  480         self.name = self._page.name
  481         self.section = self._page.namespace
  482         self.namespace = self._page.namespace # backward compat
  483         self.basename = self._page.basename
  484         self.properties = {} # undocumented field kept for backward compat
  485 
  486     @property
  487     def title(self):
  488         return self.heading or self.basename
  489 
  490     @ExpressionFunction
  491     def headings(self, max_level=None):
  492         if self._tree and self._tree.hascontent:
  493             splitter = HeadingSplitter(max_level)
  494             self._tree.visit(splitter)
  495             for subtree in splitter.headings:
  496                 yield HeadingProxy(self._page, subtree, self._dumper)
  497 
  498     @property
  499     def links(self):
  500         try:
  501             links = self._notebook.links.list_links(self._page, LINK_DIR_FORWARD)
  502             for link in links:
  503                 yield NotebookPathProxy(link.target)
  504         except IndexNotFoundError:
  505             pass # XXX needed for index_page and other specials because they do not exist in the index
  506 
  507     @property
  508     def backlinks(self):
  509         try:
  510             links = self._notebook.links.list_links(self._page, LINK_DIR_BACKWARD)
  511             for link in links:
  512                 yield NotebookPathProxy(link.source)
  513         except IndexNotFoundError:
  514             pass # XXX needed for index_page and other specials because they do not exist in the index
  515 
  516     @property
  517     def attachments(self):
  518         try:
  519             source_dir = self._notebook.get_attachments_dir(self._page)
  520             try:
  521                 for file in source_dir.list_files():
  522                     if file.exists(): # is file
  523                         href = './' + file.basename
  524                         dest_file = self._linker.resolve_dest_file(href)
  525                         yield FileProxy(file, dest_file=dest_file, relpath=href)
  526             except FileNotFoundError:
  527                 pass
  528         except IndexNotFoundError:
  529             pass # XXX needed for index_page and other specials because they do not exist in the index
  530 
  531 class HeadingProxy(ParseTreeProxy):
  532 
  533     def __init__(self, page, tree, dumper):
  534         self._page = page
  535         self._tree = tree
  536         self._dumper = dumper
  537         self.level = tree.get_heading_level() or 1
  538 
  539 
  540 class FilePathProxy(object):
  541 
  542     def __init__(self, file, dest_file=None, relpath=None):
  543         self._file = file
  544         self._dest_file = dest_file
  545         self.name = relpath or file.basename
  546         self.basename = file.basename
  547 
  548 
  549 class FileProxy(FilePathProxy):
  550 
  551     @property
  552     def mtime(self):
  553         return datetime.datetime.fromtimestamp(float(self._file.mtime()))
  554 
  555     @property
  556     def size(self):
  557         return format_file_size(self._file.size())
  558 
  559 
  560 class NotebookPathProxy(object):
  561 
  562     def __init__(self, path):
  563         self._path = path
  564         self.name = path.name
  565         self.basename = path.basename
  566         self.section = path.namespace
  567         self.namespace = path.namespace # backward compat
  568 
  569 
  570 class UriProxy(object):
  571 
  572     def __init__(self, uri):
  573         self.uri = uri
  574 
  575     def __str__(self):
  576         return self.uri