blob: 57e323e48acbf2dfba729b9a0d9c9d68d95693b7 [file] [log] [blame]
Chris Luke90f52bf2016-09-12 08:55:13 -04001# 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 Vinciguerra464e5e02019-11-01 15:07:32 -040017import html.parser
18import json
19import logging
20import os
21import sys
22
23import jinja2
Chris Luke90f52bf2016-09-12 08:55:13 -040024
25# Classes register themselves in this dictionary
26"""Mapping of known processors to their classes"""
27siphons = {}
28
Chris Lukec3f92ad2016-10-05 15:45:19 -040029"""Mapping of known output formats to their classes"""
30formats = {}
31
Chris Luke90f52bf2016-09-12 08:55:13 -040032
Chris Luke90f52bf2016-09-12 08:55:13 -040033class Siphon(object):
Paul Vinciguerra464e5e02019-11-01 15:07:32 -040034 """Generate rendered output for siphoned data."""
Chris Luke90f52bf2016-09-12 08:55:13 -040035
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 Lukec3f92ad2016-10-05 15:45:19 -040063 def __init__(self, template_directory, format):
Chris Luke90f52bf2016-09-12 08:55:13 -040064 super(Siphon, self).__init__()
65 self.log = logging.getLogger("siphon.process.%s" % self.name)
66
Chris Lukec3f92ad2016-10-05 15:45:19 -040067 # Get our output format details
68 fmt_klass = formats[format]
69 fmt = fmt_klass()
70 self._format = fmt
Chris Luke90f52bf2016-09-12 08:55:13 -040071
Chris Lukec3f92ad2016-10-05 15:45:19 -040072 # 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,
85 keep_trailing_newline=True)
86
87 # Convenience, get a reference to the internal escape and
Paul Vinciguerra464e5e02019-11-01 15:07:32 -040088 # unescape methods in html.parser. These then become
Chris Lukec3f92ad2016-10-05 15:45:19 -040089 # available to templates to use, if needed.
Paul Vinciguerra464e5e02019-11-01 15:07:32 -040090 self._h = html.parser.HTMLParser()
91 self.escape = html.escape
92 self.unescape = html.unescape
Chris Luke90f52bf2016-09-12 08:55:13 -040093
94 # Output renderers
95
96 """Returns an object to be used as the sorting key in the item index."""
97 def index_sort_key(self, group):
98 return group
99
100 """Returns a string to use as the header at the top of the item index."""
101 def index_header(self):
102 return self.template("index_header")
103
104 """Returns the string fragment to use for each section in the item
105 index."""
106 def index_section(self, group):
107 return self.template("index_section", group=group)
108
109 """Returns the string fragment to use for each entry in the item index."""
110 def index_entry(self, meta, item):
111 return self.template("index_entry", meta=meta, item=item)
112
113 """Returns an object, typically a string, to be used as the sorting key
114 for items within a section."""
115 def item_sort_key(self, item):
116 return item['name']
117
118 """Returns a key for grouping items together."""
119 def group_key(self, directory, file, macro, name):
120 _global = self._cmds['_global']
121
122 if file in _global and 'group_label' in _global[file]:
123 self._group[file] = (directory, file)
124 return file
125
126 self._group[directory] = (directory, None)
127 return directory
128
129 """Returns a key for identifying items within a grouping."""
130 def item_key(self, directory, file, macro, name):
131 return name
132
133 """Returns a string to use as the header when rendering the item."""
134 def item_header(self, group):
135 return self.template("item_header", group=group)
136
137 """Returns a string to use as the body when rendering the item."""
138 def item_format(self, meta, item):
139 return self.template("item_format", meta=meta, item=item)
140
141 """Returns a string to use as the label for the page reference."""
142 def page_label(self, group):
143 return "_".join((
144 self.name,
145 self.sanitize_label(group)
146 ))
147
148 """Returns a title to use for a page."""
149 def page_title(self, group):
150 _global = self._cmds['_global']
151 (directory, file) = self._group[group]
152
153 if file and file in _global and 'group_label' in _global[file]:
154 return _global[file]['group_label']
155
156 if directory in _global and 'group_label' in _global[directory]:
157 return _global[directory]['group_label']
158
159 return directory
160
161 """Returns a string to use as the label for the section reference."""
162 def item_label(self, group, item):
163 return "__".join((
164 self.name,
165 item
166 ))
167
168 """Label sanitizer; for creating Doxygen references"""
169 def sanitize_label(self, value):
170 return value.replace(" ", "_") \
171 .replace("/", "_") \
172 .replace(".", "_")
173
174 """Template processor"""
175 def template(self, name, **kwargs):
Paul Vinciguerra464e5e02019-11-01 15:07:32 -0400176 tpl = self._tplenv.get_template(name + self._format.extension)
177 return tpl.render(
Chris Luke90f52bf2016-09-12 08:55:13 -0400178 this=self,
179 **kwargs)
180
Chris Luke90f52bf2016-09-12 08:55:13 -0400181 # Processing methods
182
183 """Parse the input file into a more usable dictionary structure."""
184 def load_json(self, files):
185 self._cmds = {}
186 self._group = {}
187
188 line_num = 0
189 line_start = 0
190 for filename in files:
191 filename = os.path.relpath(filename)
192 self.log.info("Parsing items in file \"%s\"." % filename)
193 data = None
194 with open(filename, "r") as fd:
195 data = json.load(fd)
196
197 self._cmds['_global'] = data['global']
198
199 # iterate the items loaded and regroup it
200 for item in data["items"]:
201 try:
202 o = self._parser.parse(item['block'])
Paul Vinciguerra464e5e02019-11-01 15:07:32 -0400203 except Exception:
204 self.log.error("Exception parsing item: %s\n%s"
205 % (json.dumps(item, separators=(',', ': '),
206 indent=4),
207 item['block']))
Chris Luke90f52bf2016-09-12 08:55:13 -0400208 raise
209
210 # Augment the item with metadata
211 o["meta"] = {}
212 for key in item:
213 if key == 'block':
214 continue
215 o['meta'][key] = item[key]
216
217 # Load some interesting fields
218 directory = item['directory']
219 file = item['file']
220 macro = o["macro"]
221 name = o["name"]
222
223 # Generate keys to group items by
224 group_key = self.group_key(directory, file, macro, name)
225 item_key = self.item_key(directory, file, macro, name)
226
227 if group_key not in self._cmds:
228 self._cmds[group_key] = {}
229
230 self._cmds[group_key][item_key] = o
231
232 """Iterate over the input data, calling render methods to generate the
233 output."""
234 def process(self, out=None):
235
236 if out is None:
237 out = sys.stdout
238
239 # Accumulated body contents
240 contents = ""
241
242 # Write the header for this siphon type
243 out.write(self.index_header())
244
245 # Sort key helper for the index
246 def group_sort_key(group):
247 return self.index_sort_key(group)
248
249 # Iterate the dictionary and process it
250 for group in sorted(self._cmds.keys(), key=group_sort_key):
251 if group.startswith('_'):
252 continue
253
Paul Vinciguerra464e5e02019-11-01 15:07:32 -0400254 self.log.info("Processing items in group \"%s\" (%s)." %
255 (group, group_sort_key(group)))
Chris Luke90f52bf2016-09-12 08:55:13 -0400256
257 # Generate the section index entry (write it now)
258 out.write(self.index_section(group))
259
260 # Generate the item header (save for later)
261 contents += self.item_header(group)
262
263 def item_sort_key(key):
264 return self.item_sort_key(self._cmds[group][key])
265
266 for key in sorted(self._cmds[group].keys(), key=item_sort_key):
Paul Vinciguerra464e5e02019-11-01 15:07:32 -0400267 self.log.debug("--- Processing key \"%s\" (%s)." %
268 (key, item_sort_key(key)))
Chris Luke90f52bf2016-09-12 08:55:13 -0400269
270 o = self._cmds[group][key]
271 meta = {
272 "directory": o['meta']['directory'],
273 "file": o['meta']['file'],
274 "macro": o['macro'],
Chris Lukeaf405f72016-09-26 15:51:56 -0700275 "name": o['name'],
Chris Luke90f52bf2016-09-12 08:55:13 -0400276 "key": key,
277 "label": self.item_label(group, key),
278 }
279
280 # Generate the index entry for the item (write it now)
281 out.write(self.index_entry(meta, o))
282
283 # Generate the item itself (save for later)
284 contents += self.item_format(meta, o)
285
286 # Deliver the accumulated body output
287 out.write(contents)
Chris Lukec3f92ad2016-10-05 15:45:19 -0400288
289
Chris Lukec3f92ad2016-10-05 15:45:19 -0400290class Format(object):
Paul Vinciguerra464e5e02019-11-01 15:07:32 -0400291 """Output format class"""
Chris Lukec3f92ad2016-10-05 15:45:19 -0400292
293 """Name of this output format"""
294 name = None
295
296 """Expected file extension of templates that build this format"""
297 extension = None
298
299
Chris Lukec3f92ad2016-10-05 15:45:19 -0400300class FormatMarkdown(Format):
Paul Vinciguerra464e5e02019-11-01 15:07:32 -0400301 """Markdown output format"""
Chris Lukec3f92ad2016-10-05 15:45:19 -0400302 name = "markdown"
303 extension = ".md"
304
Paul Vinciguerra464e5e02019-11-01 15:07:32 -0400305
Chris Lukec3f92ad2016-10-05 15:45:19 -0400306# Register 'markdown'
307formats["markdown"] = FormatMarkdown
308
309
Chris Lukec3f92ad2016-10-05 15:45:19 -0400310class FormatItemlist(Format):
Paul Vinciguerra464e5e02019-11-01 15:07:32 -0400311 """Itemlist output format"""
Chris Lukec3f92ad2016-10-05 15:45:19 -0400312 name = "itemlist"
313 extension = ".itemlist"
314
Paul Vinciguerra464e5e02019-11-01 15:07:32 -0400315
Chris Lukec3f92ad2016-10-05 15:45:19 -0400316# Register 'itemlist'
317formats["itemlist"] = FormatItemlist