blob: ce70be5b3998a2d20582580207c9167e0646ad9d [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,
Paul Vinciguerra582eac52020-04-03 12:18:40 -040085 autoescape=True,
Chris Lukec3f92ad2016-10-05 15:45:19 -040086 keep_trailing_newline=True)
87
88 # Convenience, get a reference to the internal escape and
Paul Vinciguerra464e5e02019-11-01 15:07:32 -040089 # unescape methods in html.parser. These then become
Chris Lukec3f92ad2016-10-05 15:45:19 -040090 # available to templates to use, if needed.
Paul Vinciguerra464e5e02019-11-01 15:07:32 -040091 self._h = html.parser.HTMLParser()
92 self.escape = html.escape
93 self.unescape = html.unescape
Chris Luke90f52bf2016-09-12 08:55:13 -040094
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 Vinciguerra464e5e02019-11-01 15:07:32 -0400177 tpl = self._tplenv.get_template(name + self._format.extension)
178 return tpl.render(
Chris Luke90f52bf2016-09-12 08:55:13 -0400179 this=self,
180 **kwargs)
181
Chris Luke90f52bf2016-09-12 08:55:13 -0400182 # 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 Vinciguerra464e5e02019-11-01 15:07:32 -0400204 except Exception:
205 self.log.error("Exception parsing item: %s\n%s"
206 % (json.dumps(item, separators=(',', ': '),
207 indent=4),
208 item['block']))
Chris Luke90f52bf2016-09-12 08:55:13 -0400209 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 Vinciguerra464e5e02019-11-01 15:07:32 -0400255 self.log.info("Processing items in group \"%s\" (%s)." %
256 (group, group_sort_key(group)))
Chris Luke90f52bf2016-09-12 08:55:13 -0400257
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 Vinciguerra464e5e02019-11-01 15:07:32 -0400268 self.log.debug("--- Processing key \"%s\" (%s)." %
269 (key, item_sort_key(key)))
Chris Luke90f52bf2016-09-12 08:55:13 -0400270
271 o = self._cmds[group][key]
272 meta = {
273 "directory": o['meta']['directory'],
274 "file": o['meta']['file'],
275 "macro": o['macro'],
Chris Lukeaf405f72016-09-26 15:51:56 -0700276 "name": o['name'],
Chris Luke90f52bf2016-09-12 08:55:13 -0400277 "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 Lukec3f92ad2016-10-05 15:45:19 -0400289
290
Chris Lukec3f92ad2016-10-05 15:45:19 -0400291class Format(object):
Paul Vinciguerra464e5e02019-11-01 15:07:32 -0400292 """Output format class"""
Chris Lukec3f92ad2016-10-05 15:45:19 -0400293
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 Lukec3f92ad2016-10-05 15:45:19 -0400301class FormatMarkdown(Format):
Paul Vinciguerra464e5e02019-11-01 15:07:32 -0400302 """Markdown output format"""
Chris Lukec3f92ad2016-10-05 15:45:19 -0400303 name = "markdown"
304 extension = ".md"
305
Paul Vinciguerra464e5e02019-11-01 15:07:32 -0400306
Chris Lukec3f92ad2016-10-05 15:45:19 -0400307# Register 'markdown'
308formats["markdown"] = FormatMarkdown
309
310
Chris Lukec3f92ad2016-10-05 15:45:19 -0400311class FormatItemlist(Format):
Paul Vinciguerra464e5e02019-11-01 15:07:32 -0400312 """Itemlist output format"""
Chris Lukec3f92ad2016-10-05 15:45:19 -0400313 name = "itemlist"
314 extension = ".itemlist"
315
Paul Vinciguerra464e5e02019-11-01 15:07:32 -0400316
Chris Lukec3f92ad2016-10-05 15:45:19 -0400317# Register 'itemlist'
318formats["itemlist"] = FormatItemlist