Chris Luke | 90f52bf | 2016-09-12 08:55:13 -0400 | [diff] [blame] | 1 | # Copyright (c) 2016 Comcast Cable Communications Management, LLC. |
| 2 | # |
| 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | # you may not use this file except in compliance with the License. |
| 5 | # You may obtain a copy of the License at: |
| 6 | # |
| 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | # |
| 9 | # Unless required by applicable law or agreed to in writing, software |
| 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | # See the License for the specific language governing permissions and |
| 13 | # limitations under the License. |
| 14 | |
| 15 | # Generation template class |
| 16 | |
Paul Vinciguerra | 464e5e0 | 2019-11-01 15:07:32 -0400 | [diff] [blame] | 17 | import html.parser |
| 18 | import json |
| 19 | import logging |
| 20 | import os |
| 21 | import sys |
| 22 | |
| 23 | import jinja2 |
Chris Luke | 90f52bf | 2016-09-12 08:55:13 -0400 | [diff] [blame] | 24 | |
| 25 | # Classes register themselves in this dictionary |
| 26 | """Mapping of known processors to their classes""" |
| 27 | siphons = {} |
| 28 | |
Chris Luke | c3f92ad | 2016-10-05 15:45:19 -0400 | [diff] [blame] | 29 | """Mapping of known output formats to their classes""" |
| 30 | formats = {} |
| 31 | |
Chris Luke | 90f52bf | 2016-09-12 08:55:13 -0400 | [diff] [blame] | 32 | |
Chris Luke | 90f52bf | 2016-09-12 08:55:13 -0400 | [diff] [blame] | 33 | class Siphon(object): |
Paul Vinciguerra | 464e5e0 | 2019-11-01 15:07:32 -0400 | [diff] [blame] | 34 | """Generate rendered output for siphoned data.""" |
Chris Luke | 90f52bf | 2016-09-12 08:55:13 -0400 | [diff] [blame] | 35 | |
| 36 | # Set by subclasses |
| 37 | """Our siphon name""" |
| 38 | name = None |
| 39 | |
| 40 | # Set by subclasses |
| 41 | """Name of an identifier used by this siphon""" |
| 42 | identifier = None |
| 43 | |
| 44 | # Set by subclasses |
| 45 | """The pyparsing object to use to parse with""" |
| 46 | _parser = None |
| 47 | |
| 48 | """The input data""" |
| 49 | _cmds = None |
| 50 | |
| 51 | """Group key to (directory,file) mapping""" |
| 52 | _group = None |
| 53 | |
| 54 | """Logging handler""" |
| 55 | log = None |
| 56 | |
| 57 | """Directory to look for siphon rendering templates""" |
| 58 | template_directory = None |
| 59 | |
| 60 | """Template environment, if we're using templates""" |
| 61 | _tplenv = None |
| 62 | |
Chris Luke | c3f92ad | 2016-10-05 15:45:19 -0400 | [diff] [blame] | 63 | def __init__(self, template_directory, format): |
Chris Luke | 90f52bf | 2016-09-12 08:55:13 -0400 | [diff] [blame] | 64 | super(Siphon, self).__init__() |
| 65 | self.log = logging.getLogger("siphon.process.%s" % self.name) |
| 66 | |
Chris Luke | c3f92ad | 2016-10-05 15:45:19 -0400 | [diff] [blame] | 67 | # Get our output format details |
| 68 | fmt_klass = formats[format] |
| 69 | fmt = fmt_klass() |
| 70 | self._format = fmt |
Chris Luke | 90f52bf | 2016-09-12 08:55:13 -0400 | [diff] [blame] | 71 | |
Chris Luke | c3f92ad | 2016-10-05 15:45:19 -0400 | [diff] [blame] | 72 | # Sort out the template search path |
| 73 | def _tpldir(name): |
| 74 | return os.sep.join((template_directory, fmt.name, name)) |
| 75 | |
| 76 | self.template_directory = template_directory |
| 77 | searchpath = [ |
| 78 | _tpldir(self.name), |
| 79 | _tpldir("default"), |
| 80 | ] |
| 81 | loader = jinja2.FileSystemLoader(searchpath=searchpath) |
| 82 | self._tplenv = jinja2.Environment( |
| 83 | loader=loader, |
| 84 | trim_blocks=True, |
Paul Vinciguerra | 582eac5 | 2020-04-03 12:18:40 -0400 | [diff] [blame] | 85 | autoescape=True, |
Chris Luke | c3f92ad | 2016-10-05 15:45:19 -0400 | [diff] [blame] | 86 | keep_trailing_newline=True) |
| 87 | |
| 88 | # Convenience, get a reference to the internal escape and |
Paul Vinciguerra | 464e5e0 | 2019-11-01 15:07:32 -0400 | [diff] [blame] | 89 | # unescape methods in html.parser. These then become |
Chris Luke | c3f92ad | 2016-10-05 15:45:19 -0400 | [diff] [blame] | 90 | # available to templates to use, if needed. |
Paul Vinciguerra | 464e5e0 | 2019-11-01 15:07:32 -0400 | [diff] [blame] | 91 | self._h = html.parser.HTMLParser() |
| 92 | self.escape = html.escape |
| 93 | self.unescape = html.unescape |
Chris Luke | 90f52bf | 2016-09-12 08:55:13 -0400 | [diff] [blame] | 94 | |
| 95 | # Output renderers |
| 96 | |
| 97 | """Returns an object to be used as the sorting key in the item index.""" |
| 98 | def index_sort_key(self, group): |
| 99 | return group |
| 100 | |
| 101 | """Returns a string to use as the header at the top of the item index.""" |
| 102 | def index_header(self): |
| 103 | return self.template("index_header") |
| 104 | |
| 105 | """Returns the string fragment to use for each section in the item |
| 106 | index.""" |
| 107 | def index_section(self, group): |
| 108 | return self.template("index_section", group=group) |
| 109 | |
| 110 | """Returns the string fragment to use for each entry in the item index.""" |
| 111 | def index_entry(self, meta, item): |
| 112 | return self.template("index_entry", meta=meta, item=item) |
| 113 | |
| 114 | """Returns an object, typically a string, to be used as the sorting key |
| 115 | for items within a section.""" |
| 116 | def item_sort_key(self, item): |
| 117 | return item['name'] |
| 118 | |
| 119 | """Returns a key for grouping items together.""" |
| 120 | def group_key(self, directory, file, macro, name): |
| 121 | _global = self._cmds['_global'] |
| 122 | |
| 123 | if file in _global and 'group_label' in _global[file]: |
| 124 | self._group[file] = (directory, file) |
| 125 | return file |
| 126 | |
| 127 | self._group[directory] = (directory, None) |
| 128 | return directory |
| 129 | |
| 130 | """Returns a key for identifying items within a grouping.""" |
| 131 | def item_key(self, directory, file, macro, name): |
| 132 | return name |
| 133 | |
| 134 | """Returns a string to use as the header when rendering the item.""" |
| 135 | def item_header(self, group): |
| 136 | return self.template("item_header", group=group) |
| 137 | |
| 138 | """Returns a string to use as the body when rendering the item.""" |
| 139 | def item_format(self, meta, item): |
| 140 | return self.template("item_format", meta=meta, item=item) |
| 141 | |
| 142 | """Returns a string to use as the label for the page reference.""" |
| 143 | def page_label(self, group): |
| 144 | return "_".join(( |
| 145 | self.name, |
| 146 | self.sanitize_label(group) |
| 147 | )) |
| 148 | |
| 149 | """Returns a title to use for a page.""" |
| 150 | def page_title(self, group): |
| 151 | _global = self._cmds['_global'] |
| 152 | (directory, file) = self._group[group] |
| 153 | |
| 154 | if file and file in _global and 'group_label' in _global[file]: |
| 155 | return _global[file]['group_label'] |
| 156 | |
| 157 | if directory in _global and 'group_label' in _global[directory]: |
| 158 | return _global[directory]['group_label'] |
| 159 | |
| 160 | return directory |
| 161 | |
| 162 | """Returns a string to use as the label for the section reference.""" |
| 163 | def item_label(self, group, item): |
| 164 | return "__".join(( |
| 165 | self.name, |
| 166 | item |
| 167 | )) |
| 168 | |
| 169 | """Label sanitizer; for creating Doxygen references""" |
| 170 | def sanitize_label(self, value): |
| 171 | return value.replace(" ", "_") \ |
| 172 | .replace("/", "_") \ |
| 173 | .replace(".", "_") |
| 174 | |
| 175 | """Template processor""" |
| 176 | def template(self, name, **kwargs): |
Paul Vinciguerra | 464e5e0 | 2019-11-01 15:07:32 -0400 | [diff] [blame] | 177 | tpl = self._tplenv.get_template(name + self._format.extension) |
| 178 | return tpl.render( |
Chris Luke | 90f52bf | 2016-09-12 08:55:13 -0400 | [diff] [blame] | 179 | this=self, |
| 180 | **kwargs) |
| 181 | |
Chris Luke | 90f52bf | 2016-09-12 08:55:13 -0400 | [diff] [blame] | 182 | # Processing methods |
| 183 | |
| 184 | """Parse the input file into a more usable dictionary structure.""" |
| 185 | def load_json(self, files): |
| 186 | self._cmds = {} |
| 187 | self._group = {} |
| 188 | |
| 189 | line_num = 0 |
| 190 | line_start = 0 |
| 191 | for filename in files: |
| 192 | filename = os.path.relpath(filename) |
| 193 | self.log.info("Parsing items in file \"%s\"." % filename) |
| 194 | data = None |
| 195 | with open(filename, "r") as fd: |
| 196 | data = json.load(fd) |
| 197 | |
| 198 | self._cmds['_global'] = data['global'] |
| 199 | |
| 200 | # iterate the items loaded and regroup it |
| 201 | for item in data["items"]: |
| 202 | try: |
| 203 | o = self._parser.parse(item['block']) |
Paul Vinciguerra | 464e5e0 | 2019-11-01 15:07:32 -0400 | [diff] [blame] | 204 | except Exception: |
| 205 | self.log.error("Exception parsing item: %s\n%s" |
| 206 | % (json.dumps(item, separators=(',', ': '), |
| 207 | indent=4), |
| 208 | item['block'])) |
Chris Luke | 90f52bf | 2016-09-12 08:55:13 -0400 | [diff] [blame] | 209 | raise |
| 210 | |
| 211 | # Augment the item with metadata |
| 212 | o["meta"] = {} |
| 213 | for key in item: |
| 214 | if key == 'block': |
| 215 | continue |
| 216 | o['meta'][key] = item[key] |
| 217 | |
| 218 | # Load some interesting fields |
| 219 | directory = item['directory'] |
| 220 | file = item['file'] |
| 221 | macro = o["macro"] |
| 222 | name = o["name"] |
| 223 | |
| 224 | # Generate keys to group items by |
| 225 | group_key = self.group_key(directory, file, macro, name) |
| 226 | item_key = self.item_key(directory, file, macro, name) |
| 227 | |
| 228 | if group_key not in self._cmds: |
| 229 | self._cmds[group_key] = {} |
| 230 | |
| 231 | self._cmds[group_key][item_key] = o |
| 232 | |
| 233 | """Iterate over the input data, calling render methods to generate the |
| 234 | output.""" |
| 235 | def process(self, out=None): |
| 236 | |
| 237 | if out is None: |
| 238 | out = sys.stdout |
| 239 | |
| 240 | # Accumulated body contents |
| 241 | contents = "" |
| 242 | |
| 243 | # Write the header for this siphon type |
| 244 | out.write(self.index_header()) |
| 245 | |
| 246 | # Sort key helper for the index |
| 247 | def group_sort_key(group): |
| 248 | return self.index_sort_key(group) |
| 249 | |
| 250 | # Iterate the dictionary and process it |
| 251 | for group in sorted(self._cmds.keys(), key=group_sort_key): |
| 252 | if group.startswith('_'): |
| 253 | continue |
| 254 | |
Paul Vinciguerra | 464e5e0 | 2019-11-01 15:07:32 -0400 | [diff] [blame] | 255 | self.log.info("Processing items in group \"%s\" (%s)." % |
| 256 | (group, group_sort_key(group))) |
Chris Luke | 90f52bf | 2016-09-12 08:55:13 -0400 | [diff] [blame] | 257 | |
| 258 | # Generate the section index entry (write it now) |
| 259 | out.write(self.index_section(group)) |
| 260 | |
| 261 | # Generate the item header (save for later) |
| 262 | contents += self.item_header(group) |
| 263 | |
| 264 | def item_sort_key(key): |
| 265 | return self.item_sort_key(self._cmds[group][key]) |
| 266 | |
| 267 | for key in sorted(self._cmds[group].keys(), key=item_sort_key): |
Paul Vinciguerra | 464e5e0 | 2019-11-01 15:07:32 -0400 | [diff] [blame] | 268 | self.log.debug("--- Processing key \"%s\" (%s)." % |
| 269 | (key, item_sort_key(key))) |
Chris Luke | 90f52bf | 2016-09-12 08:55:13 -0400 | [diff] [blame] | 270 | |
| 271 | o = self._cmds[group][key] |
| 272 | meta = { |
| 273 | "directory": o['meta']['directory'], |
| 274 | "file": o['meta']['file'], |
| 275 | "macro": o['macro'], |
Chris Luke | af405f7 | 2016-09-26 15:51:56 -0700 | [diff] [blame] | 276 | "name": o['name'], |
Chris Luke | 90f52bf | 2016-09-12 08:55:13 -0400 | [diff] [blame] | 277 | "key": key, |
| 278 | "label": self.item_label(group, key), |
| 279 | } |
| 280 | |
| 281 | # Generate the index entry for the item (write it now) |
| 282 | out.write(self.index_entry(meta, o)) |
| 283 | |
| 284 | # Generate the item itself (save for later) |
| 285 | contents += self.item_format(meta, o) |
| 286 | |
| 287 | # Deliver the accumulated body output |
| 288 | out.write(contents) |
Chris Luke | c3f92ad | 2016-10-05 15:45:19 -0400 | [diff] [blame] | 289 | |
| 290 | |
Chris Luke | c3f92ad | 2016-10-05 15:45:19 -0400 | [diff] [blame] | 291 | class Format(object): |
Paul Vinciguerra | 464e5e0 | 2019-11-01 15:07:32 -0400 | [diff] [blame] | 292 | """Output format class""" |
Chris Luke | c3f92ad | 2016-10-05 15:45:19 -0400 | [diff] [blame] | 293 | |
| 294 | """Name of this output format""" |
| 295 | name = None |
| 296 | |
| 297 | """Expected file extension of templates that build this format""" |
| 298 | extension = None |
| 299 | |
| 300 | |
Chris Luke | c3f92ad | 2016-10-05 15:45:19 -0400 | [diff] [blame] | 301 | class FormatMarkdown(Format): |
Paul Vinciguerra | 464e5e0 | 2019-11-01 15:07:32 -0400 | [diff] [blame] | 302 | """Markdown output format""" |
Chris Luke | c3f92ad | 2016-10-05 15:45:19 -0400 | [diff] [blame] | 303 | name = "markdown" |
| 304 | extension = ".md" |
| 305 | |
Paul Vinciguerra | 464e5e0 | 2019-11-01 15:07:32 -0400 | [diff] [blame] | 306 | |
Chris Luke | c3f92ad | 2016-10-05 15:45:19 -0400 | [diff] [blame] | 307 | # Register 'markdown' |
| 308 | formats["markdown"] = FormatMarkdown |
| 309 | |
| 310 | |
Chris Luke | c3f92ad | 2016-10-05 15:45:19 -0400 | [diff] [blame] | 311 | class FormatItemlist(Format): |
Paul Vinciguerra | 464e5e0 | 2019-11-01 15:07:32 -0400 | [diff] [blame] | 312 | """Itemlist output format""" |
Chris Luke | c3f92ad | 2016-10-05 15:45:19 -0400 | [diff] [blame] | 313 | name = "itemlist" |
| 314 | extension = ".itemlist" |
| 315 | |
Paul Vinciguerra | 464e5e0 | 2019-11-01 15:07:32 -0400 | [diff] [blame] | 316 | |
Chris Luke | c3f92ad | 2016-10-05 15:45:19 -0400 | [diff] [blame] | 317 | # Register 'itemlist' |
| 318 | formats["itemlist"] = FormatItemlist |