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