Remove the old templating system (#3445)

* Inline resolve_references in dump-swagger

Since this is the only bit of the old templating system we still use, let's
inline it.

OrederedLoader and OrderedDict are now redundant, because all dicts in Python
preserve insertion order.

* Remove the old templating system

We've now replaced the old templates with hugo, so we can get rid of this mess.
pull/977/head
Richard van der Hoff 3 years ago committed by GitHub
parent 265ebef584
commit 61ac438871
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

3
.gitignore vendored

@ -4,11 +4,8 @@ node_modules
/data/msc /data/msc
/env* /env*
/resources /resources
/scripts/gen
/scripts/continuserv/continuserv
/scripts/swagger /scripts/swagger
/scripts/tmp /scripts/tmp
/templating/out
/hugo-config.toml /hugo-config.toml
/public /public
*.pyc *.pyc

@ -30,12 +30,29 @@ import yaml
scripts_dir = os.path.dirname(os.path.abspath(__file__)) scripts_dir = os.path.dirname(os.path.abspath(__file__))
templating_dir = os.path.join(scripts_dir, "templating")
api_dir = os.path.join(os.path.dirname(scripts_dir), "data", "api") api_dir = os.path.join(os.path.dirname(scripts_dir), "data", "api")
sys.path.insert(0, templating_dir) def resolve_references(path, schema):
if isinstance(schema, dict):
# do $ref first
if '$ref' in schema:
value = schema['$ref']
path = os.path.join(os.path.dirname(path), value)
with open(path, encoding="utf-8") as f:
ref = yaml.safe_load(f)
result = resolve_references(path, ref)
del schema['$ref']
else:
result = {}
for key, value in schema.items():
result[key] = resolve_references(path, value)
return result
elif isinstance(schema, list):
return [resolve_references(path, value) for value in schema]
else:
return schema
from matrix_templates import units
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
"dump-swagger.py - assemble the Swagger specs into a single JSON file" "dump-swagger.py - assemble the Swagger specs into a single JSON file"
@ -103,7 +120,7 @@ for filename in os.listdir(cs_api_dir):
print("Reading swagger API: %s" % filepath) print("Reading swagger API: %s" % filepath)
with open(filepath, "r") as f: with open(filepath, "r") as f:
api = yaml.safe_load(f.read()) api = yaml.safe_load(f.read())
api = units.resolve_references(filepath, api) api = resolve_references(filepath, api)
basePath = api['basePath'] basePath = api['basePath']
for path, methods in api["paths"].items(): for path, methods in api["paths"].items():

@ -1,10 +1,6 @@
# no doubt older versions would be fine for many of these but these were # no doubt older versions would be fine for many of these but these were
# current at the time of writing # current at the time of writing
docutils >= 0.14
pygments >= 2.2.0
Jinja2 >= 2.9.6
# jsonschema 3.0.0 objects to the $refs in our schema file. TODO: figure out # jsonschema 3.0.0 objects to the $refs in our schema file. TODO: figure out
# why. # why.
jsonschema >= 2.6.0, < 3.0.0 jsonschema >= 2.6.0, < 3.0.0
@ -12,4 +8,3 @@ jsonschema >= 2.6.0, < 3.0.0
PyYAML >= 3.12 PyYAML >= 3.12
requests >= 2.18.4 requests >= 2.18.4
towncrier == 18.6.0 towncrier == 18.6.0
six >= 1.11.0

@ -1,88 +0,0 @@
This folder contains the templates and a home-brewed templating system called
Batesian for creating the spec. Batesian uses the templating system Jinja2 in
Python.
Installation
------------
```
$ pip install Jinja2
```
Running
-------
To pass arbitrary files (not limited to RST) through the templating system:
```
$ python build.py -i matrix_templates /random/file/path/here.rst
```
The template output can be found at ``out/here.rst``. For a full list of
options, type ``python build.py --help``.
Developing
----------
### Sections and Units
Batesian is built around the concept of Sections and Units. Sections are strings
which will be inserted into the provided document. Every section has a unique
key name which is the template variable that it represents. Units are arbitrary
python data. They are also represented by unique key names.
### Adding template variables
If you want to add a new template variable e.g. `{{foo_bar}}` which is replaced
with the text `foobar`, you need to add a new Section:
- Open `matrix_templates/sections.py`.
- Add a new function to `MatrixSections` called `render_foo_bar`. The function
name after `render_` determines the template variable name, and the return
value of this function determines what will be inserted.
```python
def render_foo_bar(self):
return "foobar"
```
- Run `build.py` with a file which has `{{foo_bar}}` in it, and it will be
replaced with `foobar`.
### Adding data for template variables
If you want to expose arbitrary data which can be used by `MatrixSections`, you
need to add a new Unit:
- Open `matrix_templates/units.py`.
- Add a new function to `MatrixUnits` called `load_some_data`. Similar to
sections, the function name after `load_` determines the unit name, and the
return value of this function determines the value of the unit.
```python
def load_some_data(self):
return {
"data": "this could be JSON from file from json.loads()",
"processed_data": "this data may have helper keys added",
"types": "it doesn't even need to be a dict. Whatever you want!"
}
```
- In `MatrixSections`, you can now call `self.units.get("some_data")` to
retrieve the value you returned.
### Using Jinja templates
Sections can use Jinja templates to return text. Batesian will attempt to load
all templates from `matrix_templates/templates/`. These can be accessed in
Section code via `template = self.env.get_template("name_of_template.tmpl")`. At
this point, the `template` is just a standard `jinja2.Template`. In fact,
`self.env` is just a `jinja2.Environment`.
### Debugging
If you don't know why your template isn't behaving as you'd expect, or you just
want to add some informative logging, use `self.log` in either the Sections
class or Units class. You'll need to add `-v` to `build.py` for these lines to
show.
About
-----
Batesian was designed to be extremely simple and just use Python as its language
rather than another intermediary language like some other templating systems.
This provides a **lot** of flexibility since you aren't contrained by a
templating language. Batesian provides a thin abstraction over Jinja which is
very useful when you want to do random bits of processing then dump the output
into a Jinja template. Its name is derived from Batesian mimicry due to how the
templating system uses Python as its language, but in a harmless way.

@ -1,38 +0,0 @@
# Copyright 2016 OpenMarket Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
class AccessKeyStore(object):
"""Storage for arbitrary data. Monitors get calls so we know if they
were used or not."""
def __init__(self, existing_data=None):
if not existing_data:
existing_data = {}
self.data = existing_data
self.accessed_set = set()
def keys(self):
return self.data.keys()
def add(self, key, unit_dict):
self.data[key] = unit_dict
def get(self, key):
self.accessed_set.add(key)
return self.data[key]
def get_unaccessed_set(self):
data_list = set(self.data.keys())
return data_list - self.accessed_set

@ -1,77 +0,0 @@
# Copyright 2016 OpenMarket Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Parent class for writing sections."""
import inspect
import os
class Sections(object):
"""A class which creates sections for each method starting with "render_".
The key for the section is the text after "render_"
e.g. "render_room_events" has the section key "room_events"
"""
def __init__(self, env, units, debug=False):
self.env = env
self.units = units
self.debug = debug
def log(self, text):
if self.debug:
print("batesian:sections: %s" % text)
def get_sections(self):
render_list = inspect.getmembers(self, predicate=inspect.ismethod)
section_dict = {}
for (func_name, func) in render_list:
if not func_name.startswith("render_"):
continue
section_key = func_name[len("render_"):]
self.log("Generating section '%s'" % section_key)
section = func()
if isinstance(section, str):
if section_key in section_dict:
raise Exception(
("%s : Section %s already exists. It must have been " +
"generated dynamically. Check which render_ methods " +
"return a dict.") %
(func_name, section_key)
)
section_dict[section_key] = section
self.log(
" Generated. Snippet => %s" % section[:60].replace("\n","")
)
elif isinstance(section, dict):
self.log(" Generated multiple sections:")
for (k, v) in section.items():
if not isinstance(k, str) or not isinstance(v, str):
raise Exception(
("Method %s returned multiple sections as a dict but " +
"expected the dict elements to be strings but they aren't.") %
(func_name, )
)
if k in section_dict:
raise Exception(
"%s tried to produce section %s which already exists." %
(func_name, k)
)
section_dict[k] = v
self.log(
" %s => %s" % (k, v[:60].replace("\n",""))
)
else:
raise Exception(
"Section function '%s' didn't return a string/dict!" % func_name
)
return section_dict

@ -1,59 +0,0 @@
# Copyright 2016 OpenMarket Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Parent class for writing units."""
import inspect
class Units(object):
@staticmethod
def prop(obj, path):
# Helper method to extract nested property values
nested_keys = path.split("/")
val = obj
for key in nested_keys:
val = val.get(key, {})
return val
def __init__(self, debug=False, substitutions=None):
self.debug = debug
if substitutions is None:
self.substitutions = {}
else:
self.substitutions = substitutions
def log(self, text):
if self.debug:
func_name = ""
trace = inspect.stack()
if len(trace) > 1 and len(trace[1]) > 2:
func_name = trace[1][3] + ":"
print("batesian:units:%s %s" % (func_name, text))
def get_units(self, debug=False):
unit_list = inspect.getmembers(self, predicate=inspect.ismethod)
unit_dict = {}
for (func_name, func) in unit_list:
if not func_name.startswith("load_"):
continue
unit_key = func_name[len("load_"):]
if len(inspect.getargs(func.__code__).args) > 1:
unit_dict[unit_key] = func(self.substitutions)
else:
unit_dict[unit_key] = func()
self.log("Generated unit '%s'" % unit_key)
return unit_dict

@ -1,286 +0,0 @@
#!/usr/bin/env python
#
# Copyright 2016 OpenMarket Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Batesian: A simple templating system using Jinja.
Architecture
============
INPUT FILE --------+
+-------+ +----------+ |
| units |-+ | sections |-+ V
+-------+ |-+ == used to create ==> +----------- | == provides vars to ==> Jinja
+-------+ | +----------+ |
+--------+ V
RAW DATA (e.g. json) Blobs of text OUTPUT FILE
Units
=====
Units are random bits of unprocessed data, e.g. schema JSON files. Anything can
be done to them, from processing it with Jinja to arbitrary python processing.
They are typically dicts.
Sections
========
Sections are strings, typically short segments of RST. They will be dropped in
to the provided input file based on their section key name (template var)
They typically use a combination of templates + units to construct bits of RST.
Input File
==========
The input file is a text file which is passed through Jinja along with the
section keys as template variables.
Processing
==========
- Execute all unit functions to load units into memory and process them.
- Execute all section functions (which can now be done because the units exist)
- Process the input file through Jinja, giving it the sections as template vars.
"""
from batesian import AccessKeyStore
from jinja2 import Environment, FileSystemLoader, StrictUndefined, Template, meta
from argparse import ArgumentParser, FileType
import importlib
import json
import logging
import os
import sys
from textwrap import TextWrapper
from matrix_templates.units import TypeTableRow
from functools import reduce
def create_from_template(template, sections):
return template.render(sections)
def check_unaccessed(name, store):
unaccessed_keys = store.get_unaccessed_set()
if len(unaccessed_keys) > 0:
log("Found %s unused %s keys." % (len(unaccessed_keys), name))
log(unaccessed_keys)
def main(input_module, files=None, out_dir=None, verbose=False, substitutions={}):
if out_dir and not os.path.exists(out_dir):
os.makedirs(out_dir)
in_mod = importlib.import_module(input_module)
# add a template filter to produce pretty pretty JSON
def jsonify(input, indent=None, pre_whitespace=0):
code = json.dumps(input, indent=indent, sort_keys=True)
if pre_whitespace:
code = code.replace("\n", ("\n" +" "*pre_whitespace))
return code
def indent_block(input, indent):
return input.replace("\n", ("\n" + " "*indent))
def indent(input, indent):
return " "*indent + input
def wrap(input, wrap=80, initial_indent=""):
if len(input) == 0:
return initial_indent
# TextWrapper collapses newlines into single spaces; we do our own
# splitting on newlines to prevent this, so that newlines can actually
# be intentionally inserted in text.
input_lines = input.split('\n\n')
wrapper = TextWrapper(initial_indent=initial_indent, width=wrap)
output_lines = [wrapper.fill(line) for line in input_lines]
for i in range(len(output_lines)):
line = output_lines[i]
in_bullet = line.startswith("- ")
if in_bullet:
output_lines[i] = line.replace("\n", "\n " + initial_indent)
return '\n\n'.join(output_lines)
def fieldwidths(input, keys, defaults=[], default_width=15):
"""
A template filter to help in the generation of tables.
Given a list of rows, returns a list giving the maximum length of the
values in each column.
:param list[TypeTableRow|dict[str,str]] input:
a list of rows
:param list[str] keys: the keys corresponding to the table columns
:param list[int] defaults: for each column, the default column width.
:param int default_width: if ``defaults`` is shorter than ``keys``, this
will be used as a fallback
"""
def getrowattribute(row, k):
# the row may be a dict (particularly the title row, which is
# generated by the template
if not isinstance(row, TypeTableRow):
return row[k]
return getattr(row, k)
def colwidth(key, default):
rowwidths = (len(getrowattribute(row, key)) for row in input)
return reduce(max, rowwidths,
default if default is not None else default_width)
results = list(map(colwidth, keys, defaults))
return results
# make Jinja aware of the templates and filters
env = Environment(
loader=FileSystemLoader(in_mod.exports["templates"]),
undefined=StrictUndefined
)
env.filters["jsonify"] = jsonify
env.filters["indent"] = indent
env.filters["indent_block"] = indent_block
env.filters["wrap"] = wrap
env.filters["fieldwidths"] = fieldwidths
# load up and parse the lowest single units possible: we don't know or care
# which spec section will use it, we just need it there in memory for when
# they want it.
units = AccessKeyStore(
existing_data=in_mod.exports["units"](
debug=verbose,
substitutions=substitutions,
).get_units()
)
# use the units to create RST sections
sections = in_mod.exports["sections"](env, units, debug=verbose).get_sections()
# print out valid section keys if no file supplied
if not files:
print("\nValid template variables:")
for key in sections.keys():
sec_text = "" if (len(sections[key]) > 75) else (
"(Value: '%s')" % sections[key]
)
sec_info = "%s characters" % len(sections[key])
if sections[key].count("\n") > 0:
sec_info += ", %s lines" % sections[key].count("\n")
print(" %s" % key)
print(" %s %s" % (sec_info, sec_text))
return
# check the input files and substitute in sections where required
for input_filename in files:
output_filename = os.path.join(out_dir,
os.path.basename(input_filename))
process_file(env, sections, input_filename, output_filename)
check_unaccessed("units", units)
def process_file(env, sections, filename, output_filename):
log("Parsing input template: %s" % filename)
with open(filename, "rb") as file_stream:
temp_str = file_stream.read().decode('UTF-8')
# do sanity checking on the template to make sure they aren't reffing things
# which will never be replaced with a section.
ast = env.parse(temp_str)
template_vars = meta.find_undeclared_variables(ast)
unused_vars = [var for var in template_vars if var not in sections]
if len(unused_vars) > 0:
raise Exception(
"You have {{ variables }} which are not found in sections: %s" %
(unused_vars,)
)
# process the template
temp = Template(temp_str)
output = create_from_template(temp, sections)
# Do these substitutions outside of the ordinary templating system because
# we want them to apply to things like the underlying swagger used to
# generate the templates, not just the top-level sections.
for old, new in substitutions.items():
output = output.replace(old, new)
with open(output_filename, "wb") as f:
f.write(output.encode('UTF-8'))
log("Output file for: %s" % output_filename)
def log(line):
print("batesian: %s" % line)
if __name__ == '__main__':
parser = ArgumentParser(
"Processes a file (typically .rst) through Jinja to replace templated "+
"areas with section information from the provided input module. For a "+
"list of possible template variables, add --show-template-vars."
)
parser.add_argument(
"files", nargs="+",
help="The input files to process. These will be passed through Jinja "+
"then output under the same name to the output directory."
)
parser.add_argument(
"--input", "-i",
help="The python module (not file) which contains the sections/units "+
"classes. This module must have an 'exports' dict which has "+
"{ 'units': UnitClass, 'sections': SectionClass, "+
"'templates': 'template/dir' }"
)
parser.add_argument(
"--out-directory", "-o", help="The directory to output the file to."+
" Default: /out",
default="out"
)
parser.add_argument(
"--show-template-vars", "-s", action="store_true",
help="Show a list of all possible variables (sections) you can use in"+
" the input file."
)
parser.add_argument(
"--verbose", "-v", action="store_true",
help="Turn on verbose mode."
)
parser.add_argument(
"--substitution", action="append",
help="Substitutions to apply to the generated output, of form NEEDLE=REPLACEMENT.",
default=[],
)
args = parser.parse_args()
if args.verbose:
logging.basicConfig(level=logging.DEBUG)
else:
logging.basicConfig(level=logging.INFO)
if not args.input:
raise Exception("Missing [i]nput python module.")
if (args.show_template_vars):
main(args.input, verbose=args.verbose)
sys.exit(0)
substitutions = {}
for substitution in args.substitution:
parts = substitution.split("=", 1)
if len(parts) != 2:
raise Exception("Invalid substitution")
substitutions[parts[0]] = parts[1]
main(
args.input, files=args.files, out_dir=args.out_directory,
substitutions=substitutions, verbose=args.verbose
)

@ -1,22 +0,0 @@
# Copyright 2016 OpenMarket Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from .sections import MatrixSections
from .units import MatrixUnits
import os
exports = {
"units": MatrixUnits,
"sections": MatrixSections,
"templates": os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates")
}

@ -1,244 +0,0 @@
# Copyright 2016 OpenMarket Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Contains all the sections for the spec."""
from batesian import AccessKeyStore
from batesian.sections import Sections
import inspect
import json
import os
import logging
import re
logger = logging.getLogger(__name__)
class MatrixSections(Sections):
# pass through git ver so it'll be dropped in the input file
def render_git_version(self):
return self.units.get("git_version")["string"]
def render_git_rev(self):
return self.units.get("git_version")["revision"]
def render_changelogs(self):
rendered = {}
changelogs = self.units.get("changelogs")
for spec, changelog_text in changelogs.items():
spec_var = "%s_changelog" % spec
logger.info("Rendering changelog for spec: %s" % spec)
rendered[spec_var] = changelog_text
return rendered
def _render_events(self, filterFn, sortFn):
template = self.env.get_template("events.tmpl")
examples = self.units.get("event_examples")
schemas = self.units.get("event_schemas")
subtitle_title_char = self.units.get("spec_targets")[
"relative_title_styles"
]["subtitle"]
sections = []
for event_name in sortFn(schemas):
if not filterFn(event_name):
continue
sections.append(template.render(
examples=examples[event_name],
event=schemas[event_name],
title_kind=subtitle_title_char
))
return "\n\n".join(sections)
def _render_http_api_group(self, group, sortPathList=None):
template = self.env.get_template("http-api.tmpl")
http_api = self.units.get("swagger_apis")[group]["__meta"]
subtitle_title_char = self.units.get("spec_targets")[
"relative_title_styles"
]["subtitle"]
sections = []
endpoints = []
if sortPathList:
# list of substrings to sort by
sorted_endpoints = []
for path_substr in sortPathList:
for e in http_api["endpoints"]:
if path_substr in e["path"]:
sorted_endpoints.append(e) # could have multiple
# dump rest
rest = [
e for e in http_api["endpoints"] if e not in sorted_endpoints
]
endpoints = sorted_endpoints + rest
else:
# sort alphabetically based on path
endpoints = http_api["endpoints"]
for endpoint in endpoints:
sections.append(template.render(
endpoint=endpoint,
title_kind=subtitle_title_char
))
return "\n\n".join(sections)
# Special function: Returning a dict will specify multiple sections where
# the key is the section name and the value is the value of the section
def render_group_http_apis(self):
# map all swagger_apis to the form $GROUP_http_api
swagger_groups = self.units.get("swagger_apis").keys()
renders = {}
for group in swagger_groups:
sortFnOrPathList = None
if group == "presence_cs":
sortFnOrPathList = ["status"]
elif group == "profile_cs":
sortFnOrPathList=["displayname", "avatar_url"]
renders[group + "_http_api"] = self._render_http_api_group(
group, sortFnOrPathList
)
return renders
# Special function: Returning a dict will specify multiple sections where
# the key is the section name and the value is the value of the section
def render_group_events(self):
# map all event schemata to the form $EVENTTYPE_event with s/.#/_/g
# e.g. m_room_topic_event or m_room_message_m_text_event
schemas = self.units.get("event_schemas")
renders = {}
for event_type in schemas:
underscored_event_type = event_type.replace(".", "_").replace("$", "_")
renders[underscored_event_type + "_event"] = self._render_events(
lambda x: x == event_type, sorted
)
return renders
def render_room_events(self):
def filterFn(eventType):
return (
eventType.startswith("m.room") and
not eventType.startswith("m.room.message$m.")
)
return self._render_events(filterFn, sorted)
def render_msgtype_events(self):
template = self.env.get_template("msgtypes.tmpl")
examples = self.units.get("event_examples")
schemas = self.units.get("event_schemas")
subtitle_title_char = self.units.get("spec_targets")[
"relative_title_styles"
]["subtitle"]
sections = []
msgtype_order = [
"m.room.message$m.text", "m.room.message$m.emote",
"m.room.message$m.notice", "m.room.message$m.image",
"m.room.message$m.file"
]
excluded_types = [
# We exclude server notices from here because we handle them in a
# dedicated module. We do not want to confuse developers this early
# in the spec.
"m.room.message$m.server_notice",
]
other_msgtypes = [
k for k in schemas.keys() if k.startswith("m.room.message$") and
k not in msgtype_order and k not in excluded_types
]
for event_name in (msgtype_order + other_msgtypes):
if not event_name.startswith("m.room.message$m."):
continue
sections.append(template.render(
example=examples[event_name][0],
event=schemas[event_name],
title_kind=subtitle_title_char
))
return "\n\n".join(sections)
def render_voip_events(self):
def filterFn(eventType):
return eventType.startswith("m.call")
def sortFn(eventTypes):
ordering = [
"m.call.invite", "m.call.candidates", "m.call.answer",
"m.call.hangup"
]
rest = [
k for k in eventTypes if k not in ordering
]
return ordering + rest
return self._render_events(filterFn, sortFn)
def render_presence_events(self):
def filterFn(eventType):
return eventType.startswith("m.presence")
return self._render_events(filterFn, sorted)
def _render_ce_type(self, type):
template = self.env.get_template("common-event-fields.tmpl")
ce_types = self.units.get("common_event_fields")
subtitle_title_char = self.units.get("spec_targets")[
"relative_title_styles"
]["subtitle"]
return template.render(
common_event=ce_types[type], title_kind=subtitle_title_char
)
def render_common_event_fields(self):
return self._render_ce_type("event")
def render_common_room_event_fields(self):
return self._render_ce_type("room_event")
def render_common_state_event_fields(self):
return self._render_ce_type("state_event")
def render_apis(self):
template = self.env.get_template("apis.tmpl")
apis = self.units.get("apis")
return template.render(apis=apis)
def render_unstable_warnings(self):
rendered = {}
blocks = self.units.get("unstable_warnings")
for var, text in blocks.items():
rendered["unstable_warning_block_" + var] = text
return rendered
def render_swagger_definition(self):
rendered = {}
template = self.env.get_template("schema-definition.tmpl")
subtitle_title_char = self.units.get("spec_targets")[
"relative_title_styles"
]["subtitle"]
definitions = self.units.get("swagger_definitions")
for group, swagger_def in definitions.items():
rendered["definition_" + group] = template.render(
definition=swagger_def['definition'],
examples=swagger_def['examples'],
title_kind=subtitle_title_char)
return rendered
def render_sas_emoji_table(self):
emoji = self.units.get("sas_emoji")
rendered = ".. csv-table::\n"
rendered += " :header: \"Number\", \"Emoji\", \"Unicode\", \"Description\"\n"
rendered += " :widths: 10, 10, 15, 20\n"
rendered += "\n"
for row in emoji:
rendered += " %d, \"%s\", \"``%s``\", \"%s\"\n" % (
row['number'],
row['emoji'],
row['unicode'],
row['description'],
)
rendered += "\n"
return rendered

@ -1,4 +0,0 @@
{% import 'tables.tmpl' as tables -%}
{{ tables.paramtable(apis.rows, ["API", "Version", "Description"]) }}

@ -1,12 +0,0 @@
{% import 'tables.tmpl' as tables -%}
{{common_event.title}} Fields
{{(7 + common_event.title | length) * title_kind}}
{{common_event.desc}}
{% for table in common_event.tables %}
{{ tables.paramtable(table.rows, ["Key", "Type", "Description"], (table.title or "")) }}
{% endfor %}

@ -1,32 +0,0 @@
{% import 'tables.tmpl' as tables -%}
{% if (event.type_with_msgtype) %}
``{{event.type_with_msgtype}}``
{{(4 + event.type_with_msgtype | length) * title_kind}}
{% endif -%}
{% if (not event.type_with_msgtype) %}
``{{event.type}}``
{{(4 + event.type | length) * title_kind}}
{% endif -%}
{% if (event.typeof | length) %}
*{{event.typeof}}*
{{event.typeof_info | indent_block(4)}}
{% endif -%}
{{event.desc}}
{% for table in event.content_fields %}
{{ tables.paramtable(table.rows, [(table.title or "Content") ~ " Key", "Type", "Description"], (table.title or "")) }}
{% endfor %}
Example{% if examples | length > 1 %}s{% endif %}:
{% for example in examples %}
.. code:: json
{{example | jsonify(4, 4)}}
{% endfor %}

@ -1,94 +0,0 @@
{% import 'tables.tmpl' as tables -%}
{% if "deprecated" in endpoint and endpoint.deprecated -%}
Deprecated: ``{{endpoint.method}} {{endpoint.path}}``
{{(17 + (endpoint.path | length) + (endpoint.method | length)) * title_kind}}
.. WARNING::
This API is deprecated and will be removed from a future release.
{% else %}
``{{endpoint.method}} {{endpoint.path}}``
{{(5 + (endpoint.path | length) + (endpoint.method | length)) * title_kind}}
{% endif -%}
{{endpoint.desc}}
{{":Rate-limited: Yes." if endpoint.rate_limited else ":Rate-limited: No." }}
{{":Requires auth: Yes." if endpoint.requires_auth else ":Requires auth: No." }}
.. class:: httpheaders
Request format:
{% if (endpoint.req_param_by_loc | length) %}
{{ tables.split_paramtable(endpoint.req_param_by_loc) }}
{% if (endpoint.req_body_tables) %}
{% for table in endpoint.req_body_tables -%}
{{ tables.paramtable(table.rows, caption=(table.title or "")) }}
{% endfor -%}
{% endif -%}
{% else %}
`No parameters`
{% endif %}
{% if endpoint.res_headers is not none -%}
.. class:: httpheaders
Response headers:
{{ tables.paramtable(endpoint.res_headers.rows) }}
{% endif -%}
{% if endpoint.res_tables|length > 0 -%}
.. class:: httpheaders
Response format:
{% for table in endpoint.res_tables -%}
{{ tables.paramtable(table.rows, caption=(table.title or "")) }}
{% endfor %}
{% endif -%}
.. class:: httpheaders
Example request:
.. code:: http
{{endpoint.example.req | indent_block(2)}}
{% if endpoint.responses|length > 0 -%}
.. class:: httpheaders
Response{{"s" if endpoint.responses|length > 1 else "" }}:
{% endif -%}
{% for res in endpoint.responses -%}
**Status code {{res["code"]}}:**
{{res["description"]}}
{% if res["example"] -%}
.. class:: httpheaders
Example
.. code:: json
{{res["example"] | indent_block(2)}}
{% endif -%}
{% endfor %}

@ -1,15 +0,0 @@
{% import 'tables.tmpl' as tables -%}
``{{event.msgtype}}``
{{(4 + event.msgtype | length) * title_kind}}
{{event.desc | wrap(80)}}
{% for table in event.content_fields -%}
{{ tables.paramtable(table.rows, [(table.title or "Content") ~ " Key", "Type", "Description"], (table.title or "")) }}
{% endfor %}
Example:
.. code:: json
{{example | jsonify(4, 4)}}

@ -1,21 +0,0 @@
{% import 'tables.tmpl' as tables -%}
``{{definition.title}}`` schema
{{(11 + definition.title | length) * title_kind}}
{% if 'description' in definition %}
{{definition.description}}
{% endif %}
{% for table in definition.tables -%}
{{"``"+table.title+"``" if table.title else "" }}
{{ tables.paramtable(table.rows) }}
{% endfor %}
Example{% if examples | length > 1 %}s{% endif %}:
{% for example in examples %}
.. code:: json
{{example | jsonify(4, 4)}}
{% endfor %}

@ -1,106 +0,0 @@
{#
# A set of macros for generating RST tables
#}
{#
# write a table for a list of parameters.
#
# 'rows' is the list of parameters. Each row should be a TypeTableRow.
#}
{% macro paramtable(rows, titles=["Parameter", "Type", "Description"], caption="") -%}
{{ split_paramtable({None: rows}, titles, caption) }}
{% endmacro %}
{#
# write a table for the request parameters, split by location.
# 'rows_by_loc' is a map from location to a list of parameters.
#
# As a special case, if a key of 'rows_by_loc' is 'None', no title row is
# written for that location. This is used by the standard 'paramtable' macro.
#}
{% macro split_paramtable(rows_by_loc,
titles=["Parameter", "Type", "Description"], caption="") -%}
{% set rowkeys = ['key', 'title', 'desc'] %}
{% set titlerow = {'key': titles[0], 'title': titles[1], 'desc': titles[2]} %}
{# We need the rows flattened into a single list. Abuse the 'sum' filter to
# join arrays instead of add numbers. -#}
{% set flatrows = rows_by_loc.values()|sum(start=[]) -%}
{# Figure out the widths of the columns. The last column is always 50 characters
# wide; the others default to 10, but stretch if there is wider text in the
# column. -#}
{% set fieldwidths = (([titlerow] + flatrows) |
fieldwidths(rowkeys[0:-1], [10, 10])) + [50] -%}
{{".. table:: "}}{{ caption }}
{{" :widths: auto"}}
{{""}}
{{ tableheader(fieldwidths) }}
{{ tablerow(fieldwidths, titlerow, rowkeys) }}
{{ tableheader(fieldwidths) }}
{% for loc in rows_by_loc -%}
{% if loc != None -%}
{{ tablespan(fieldwidths, "*" ~ loc ~ " parameters*") }}
{% endif -%}
{% for row in rows_by_loc[loc] -%}
{{ tablerow(fieldwidths, row, rowkeys) }}
{% endfor -%}
{% endfor -%}
{{ tableheader(fieldwidths) }}
{% endmacro %}
{#
# Write a table header row, for the given column widths
#}
{% macro tableheader(widths) -%}
{{" "}}{% for arg in widths -%}
{{"="*arg}} {% endfor -%}
{% endmacro %}
{#
# Write a normal table row. Each of 'widths' and 'keys' should be sequences
# of the same length; 'widths' defines the column widths, and 'keys' the
# attributes of 'row' to look up for values to put in the columns.
#}
{% macro tablerow(widths, row, keys) -%}
{{" "}}{% for key in keys -%}
{% set value=row[key] -%}
{% if not loop.last -%}
{# the first few columns need space after them -#}
{{ value }}{{" "*(1+widths[loop.index0]-value|length) -}}
{% else -%}
{# the last column needs wrapping and indenting (by the sum of the widths of
the preceding columns, plus the number of preceding columns (for the
separators)) -#}
{{ value | wrap(widths[loop.index0]) |
indent_block(widths[0:-1]|sum + loop.index0 + 2) -}}
{% endif -%}
{% endfor -%}
{% endmacro %}
{#
# write a tablespan row. This is a single value which spans the entire table.
#}
{% macro tablespan(widths, value) -%}
{{" "}}{{value}}
{# we write a trailing space to stop the separator being misinterpreted
# as a header line. -#}
{{" "}}{{"-"*(widths|sum + widths|length -1)}} {% endmacro %}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save