| # Copyright (c) 2016 Comcast Cable Communications Management, LLC. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at: |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| # Generation template class |
| |
| import html.parser |
| import json |
| import logging |
| import os |
| import sys |
| import re |
| |
| import jinja2 |
| |
| # Classes register themselves in this dictionary |
| """Mapping of known processors to their classes""" |
| siphons = {} |
| |
| """Mapping of known output formats to their classes""" |
| formats = {} |
| |
| |
| class Siphon(object): |
| """Generate rendered output for siphoned data.""" |
| |
| # Set by subclasses |
| """Our siphon name""" |
| name = None |
| |
| # Set by subclasses |
| """Name of an identifier used by this siphon""" |
| identifier = None |
| |
| # Set by subclasses |
| """The pyparsing object to use to parse with""" |
| _parser = None |
| |
| """The input data""" |
| _cmds = None |
| |
| """Group key to (directory,file) mapping""" |
| _group = None |
| |
| """Logging handler""" |
| log = None |
| |
| """Directory to look for siphon rendering templates""" |
| template_directory = None |
| |
| """Directory to output parts in""" |
| outdir = None |
| |
| """Template environment, if we're using templates""" |
| _tplenv = None |
| |
| def __init__(self, template_directory, format, outdir, repository_link): |
| super(Siphon, self).__init__() |
| self.log = logging.getLogger("siphon.process.%s" % self.name) |
| |
| # Get our output format details |
| fmt_klass = formats[format] |
| fmt = fmt_klass() |
| self._format = fmt |
| |
| # Sort out the template search path |
| def _tpldir(name): |
| return os.sep.join((template_directory, fmt.name, name)) |
| |
| self.template_directory = template_directory |
| searchpath = [ |
| _tpldir(self.name), |
| _tpldir("default"), |
| ] |
| self.outdir = outdir |
| loader = jinja2.FileSystemLoader(searchpath=searchpath) |
| self._tplenv = jinja2.Environment( |
| loader=loader, |
| trim_blocks=True, |
| autoescape=False, |
| keep_trailing_newline=True) |
| |
| # Convenience, get a reference to the internal escape and |
| # unescape methods in html.parser. These then become |
| # available to templates to use, if needed. |
| self._h = html.parser.HTMLParser() |
| self.escape = html.escape |
| self.unescape = html.unescape |
| |
| # TODO: customize release |
| self.repository_link = repository_link |
| |
| # Output renderers |
| |
| """Returns an object to be used as the sorting key in the item index.""" |
| def index_sort_key(self, group): |
| return group |
| |
| """Returns a string to use as the header at the top of the item index.""" |
| def index_header(self): |
| return self.template("index_header") |
| |
| """Returns the string fragment to use for each section in the item |
| index.""" |
| def index_section(self, group): |
| return self.template("index_section", group=group) |
| |
| """Returns the string fragment to use for each entry in the item index.""" |
| def index_entry(self, meta, item): |
| return self.template("index_entry", meta=meta, item=item) |
| |
| """Returns an object, typically a string, to be used as the sorting key |
| for items within a section.""" |
| def item_sort_key(self, item): |
| return item['name'] |
| |
| """Returns a key for grouping items together.""" |
| def group_key(self, directory, file, macro, name): |
| _global = self._cmds['_global'] |
| |
| if file in _global and 'group_label' in _global[file]: |
| self._group[file] = (directory, file) |
| return file |
| |
| self._group[directory] = (directory, None) |
| return directory |
| |
| """Returns a key for identifying items within a grouping.""" |
| def item_key(self, directory, file, macro, name): |
| return name |
| |
| """Returns a string to use as the header when rendering the item.""" |
| def item_header(self, group): |
| return self.template("item_header", group=group) |
| |
| """Returns a string to use as the body when rendering the item.""" |
| def item_format(self, meta, item): |
| return self.template("item_format", meta=meta, item=item) |
| |
| """Returns a string to use as the label for the page reference.""" |
| def page_label(self, group): |
| return "_".join(( |
| self.name, |
| self.sanitize_label(group) |
| )) |
| |
| """Returns a title to use for a page.""" |
| def page_title(self, group): |
| _global = self._cmds['_global'] |
| (directory, file) = self._group[group] |
| |
| if file and file in _global and 'group_label' in _global[file]: |
| return _global[file]['group_label'] |
| |
| if directory in _global and 'group_label' in _global[directory]: |
| return _global[directory]['group_label'] |
| |
| return directory |
| |
| """Returns a string to use as the label for the section reference.""" |
| def item_label(self, group, item): |
| return "__".join(( |
| self.name, |
| item |
| )) |
| |
| """Label sanitizer; for creating Doxygen references""" |
| def sanitize_label(self, value): |
| return value.replace(" ", "_") \ |
| .replace("/", "_") \ |
| .replace(".", "_") |
| |
| """Template processor""" |
| def template(self, name, **kwargs): |
| tpl = self._tplenv.get_template(name + self._format.extension) |
| return tpl.render( |
| this=self, |
| **kwargs) |
| |
| # Processing methods |
| |
| """Parse the input file into a more usable dictionary structure.""" |
| def load_json(self, files): |
| self._cmds = {} |
| self._group = {} |
| |
| line_num = 0 |
| line_start = 0 |
| for filename in files: |
| filename = os.path.relpath(filename) |
| self.log.info("Parsing items in file \"%s\"." % filename) |
| data = None |
| with open(filename, "r") as fd: |
| data = json.load(fd) |
| |
| self._cmds['_global'] = data['global'] |
| |
| # iterate the items loaded and regroup it |
| for item in data["items"]: |
| try: |
| o = self._parser.parse(item['block']) |
| except Exception: |
| self.log.error("Exception parsing item: %s\n%s" |
| % (json.dumps(item, separators=(',', ': '), |
| indent=4), |
| item['block'])) |
| raise |
| |
| # Augment the item with metadata |
| o["meta"] = {} |
| for key in item: |
| if key == 'block': |
| continue |
| o['meta'][key] = item[key] |
| |
| # Load some interesting fields |
| directory = item['directory'] |
| file = item['file'] |
| macro = o["macro"] |
| name = o["name"] |
| |
| # Generate keys to group items by |
| group_key = self.group_key(directory, file, macro, name) |
| item_key = self.item_key(directory, file, macro, name) |
| |
| if group_key not in self._cmds: |
| self._cmds[group_key] = {} |
| |
| self._cmds[group_key][item_key] = o |
| |
| """Iterate over the input data, calling render methods to generate the |
| output.""" |
| def process(self, out=None): |
| |
| if out is None: |
| out = sys.stdout |
| |
| # Accumulated body contents |
| contents = "" |
| |
| # Write the header for this siphon type |
| out.write(self.index_header()) |
| |
| # Sort key helper for the index |
| def group_sort_key(group): |
| return self.index_sort_key(group) |
| |
| # Iterate the dictionary and process it |
| for group in sorted(self._cmds.keys(), key=group_sort_key): |
| if group.startswith('_'): |
| continue |
| |
| self.log.info("Processing items in group \"%s\" (%s)." % |
| (group, group_sort_key(group))) |
| |
| # Generate the section index entry (write it now) |
| out.write(self.index_section(group)) |
| |
| # Generate the item header (save for later) |
| contents += self.item_header(group) |
| |
| def item_sort_key(key): |
| return self.item_sort_key(self._cmds[group][key]) |
| |
| for key in sorted(self._cmds[group].keys(), key=item_sort_key): |
| self.log.debug("--- Processing key \"%s\" (%s)." % |
| (key, item_sort_key(key))) |
| |
| o = self._cmds[group][key] |
| meta = { |
| "directory": o['meta']['directory'], |
| "file": o['meta']['file'], |
| "macro": o['macro'], |
| "name": o['name'], |
| "key": key, |
| "label": self.item_label(group, key), |
| } |
| |
| # Generate the index entry for the item (write it now) |
| out.write(self.index_entry(meta, o)) |
| |
| # Generate the item itself (save for later) |
| contents += self.item_format(meta, o) |
| |
| page_name = self.separate_page_names(group) |
| if page_name != "": |
| path = os.path.join(self.outdir, page_name) |
| with open(path, "w+") as page: |
| page.write(contents) |
| contents = "" |
| |
| # Deliver the accumulated body output |
| out.write(contents) |
| |
| def do_cliexstart(self, matchobj): |
| title = matchobj.group(1) |
| title = ' '.join(title.splitlines()) |
| content = matchobj.group(2) |
| content = re.sub(r"\n", r"\n ", content) |
| return "\n\n.. code-block:: console\n\n %s\n %s\n\n" % (title, content) |
| |
| def do_clistart(self, matchobj): |
| content = matchobj.group(1) |
| content = re.sub(r"\n", r"\n ", content) |
| return "\n\n.. code-block:: console\n\n %s\n\n" % content |
| |
| def do_cliexcmd(self, matchobj): |
| content = matchobj.group(1) |
| content = ' '.join(content.splitlines()) |
| return "\n\n.. code-block:: console\n\n %s\n\n" % content |
| |
| def process_list(self, matchobj): |
| content = matchobj.group(1) |
| content = self.reindent(content, 2) |
| return "@@@@%s\nBBBB" % content |
| |
| def process_special(self, s): |
| # ----------- markers to remove |
| s = re.sub(r"@cliexpar\s*", r"", s) |
| s = re.sub(r"@parblock\s*", r"", s) |
| s = re.sub(r"@endparblock\s*", r"", s) |
| s = re.sub(r"<br>", "", s) |
| # ----------- emphasis |
| # <b><em> |
| s = re.sub(r"<b><em>\s*", "``", s) |
| s = re.sub(r"\s*</b></em>", "``", s) |
| s = re.sub(r"\s*</em></b>", "``", s) |
| # <b> |
| s = re.sub(r"<b>\s*", "**", s) |
| s = re.sub(r"\s*</b>", "**", s) |
| # <code> |
| s = re.sub(r"<code>\s*", "``", s) |
| s = re.sub(r"\s*</code>", "``", s) |
| # <em> |
| s = re.sub(r"'?<em>\s*", r"``", s) |
| s = re.sub(r"\s*</em>'?", r"``", s) |
| # @c <something> |
| s = re.sub(r"@c\s(\S+)", r"``\1``", s) |
| # ----------- todos |
| s = re.sub(r"@todo[^\n]*", "", s) |
| s = re.sub(r"@TODO[^\n]*", "", s) |
| # ----------- code blocks |
| s = re.sub(r"@cliexcmd{(.+?)}", self.do_cliexcmd, s, flags=re.DOTALL) |
| s = re.sub(r"@cliexstart{(.+?)}(.+?)@cliexend", self.do_cliexstart, s, flags=re.DOTALL) |
| s = re.sub(r"@clistart(.+?)@cliend", self.do_clistart, s, flags=re.DOTALL) |
| # ----------- lists |
| s = re.sub(r"^\s*-", r"\n@@@@", s, flags=re.MULTILINE) |
| s = re.sub(r"@@@@(.*?)\n\n+", self.process_list, s, flags=re.DOTALL) |
| s = re.sub(r"BBBB@@@@", r"-", s) |
| s = re.sub(r"@@@@", r"-", s) |
| s = re.sub(r"BBBB", r"\n\n", s) |
| # ----------- Cleanup remains |
| s = re.sub(r"@cliexend\s*", r"", s) |
| return s |
| |
| def separate_page_names(self, group): |
| return "" |
| |
| # This push the given textblock <indent> spaces right |
| def reindent(self, s, indent): |
| ind = " " * indent |
| s = re.sub(r"\n", "\n" + ind, s) |
| return s |
| |
| # This aligns the given textblock left (no indent) |
| def noindent(self, s): |
| s = re.sub(r"\n[ \f\v\t]*", "\n", s) |
| return s |
| |
| class Format(object): |
| """Output format class""" |
| |
| """Name of this output format""" |
| name = None |
| |
| """Expected file extension of templates that build this format""" |
| extension = None |
| |
| |
| class FormatMarkdown(Format): |
| """Markdown output format""" |
| name = "markdown" |
| extension = ".md" |
| |
| |
| # Register 'markdown' |
| formats["markdown"] = FormatMarkdown |
| |
| |
| class FormatItemlist(Format): |
| """Itemlist output format""" |
| name = "itemlist" |
| extension = ".itemlist" |
| |
| |
| # Register 'itemlist' |
| formats["itemlist"] = FormatItemlist |