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