393 lines
15 KiB
Python
393 lines
15 KiB
Python
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
import sys, re, textwrap
|
|
|
|
VERSION = 4
|
|
|
|
class ParseError(Exception):
|
|
# args[1] is the line number that caused the problem
|
|
def __init__(self, why, lineno):
|
|
self.why = why
|
|
self.lineno = lineno
|
|
def __str__(self):
|
|
return ("ParseError: the JS API docs were unparseable on line %d: %s" %
|
|
(self.lineno, self.why))
|
|
|
|
class Accumulator:
|
|
def __init__(self, holder, firstline):
|
|
self.holder = holder
|
|
self.firstline = firstline
|
|
self.otherlines = []
|
|
def addline(self, line):
|
|
self.otherlines.append(line)
|
|
def finish(self):
|
|
# take a list of strings like:
|
|
# "initial stuff" (this is in firstline)
|
|
# " more stuff" (this is in lines[0])
|
|
# " yet more stuff"
|
|
# " indented block"
|
|
# " indented block"
|
|
# " nonindented stuff" (lines[-1])
|
|
#
|
|
# calculate the indentation level by looking at all but the first
|
|
# line, and removing the whitespace they all have in common. Then
|
|
# join the results with newlines and return a single string.
|
|
pieces = []
|
|
if self.firstline:
|
|
pieces.append(self.firstline)
|
|
if self.otherlines:
|
|
pieces.append(textwrap.dedent("\n".join(self.otherlines)))
|
|
self.holder["description"] = "\n".join(pieces)
|
|
|
|
|
|
class APIParser:
|
|
def parse(self, lines, lineno):
|
|
api = {"line_number": lineno + 1}
|
|
# assign the name from the first line, of the form "<api name="API_NAME">"
|
|
title_line = lines[lineno].rstrip("\n")
|
|
api["name"] = self._parse_title_line(title_line, lineno + 1)
|
|
lineno += 1
|
|
# finished with the first line, assigned the name
|
|
working_set = self._initialize_working_set()
|
|
props = []
|
|
currentPropHolder = api
|
|
# fetch the next line, of the form "@tag [name] {datatype} description"
|
|
# and parse it into tag, info, description
|
|
tag, info, firstline = self._parseTypeLine(lines[lineno], lineno + 1)
|
|
api["type"] = tag
|
|
# if this API element is a property then datatype must be set
|
|
if tag == 'property':
|
|
api['datatype'] = info['datatype']
|
|
# info is ignored
|
|
currentAccumulator = Accumulator(api, firstline)
|
|
lineno += 1
|
|
while (lineno) < len(lines):
|
|
line = lines[lineno].rstrip("\n")
|
|
# accumulate any multiline descriptive text belonging to
|
|
# the preceding "@" section
|
|
if self._is_description_line(line):
|
|
currentAccumulator.addline(line)
|
|
else:
|
|
currentAccumulator.finish()
|
|
if line.startswith("<api"):
|
|
# then we should recursively handle a nested element
|
|
nested_api, lineno = self.parse(lines, lineno)
|
|
self._update_working_set(nested_api, working_set)
|
|
elif line.startswith("</api"):
|
|
# then we have finished parsing this api element
|
|
currentAccumulator.finish()
|
|
if props and currentPropHolder:
|
|
currentPropHolder["props"] = props
|
|
self._assemble_api_element(api, working_set)
|
|
return api, lineno
|
|
else:
|
|
# then we are looking at a subcomponent of an <api> element
|
|
tag, info, desc = self._parseTypeLine(line, lineno + 1)
|
|
currentAccumulator = Accumulator(info, desc)
|
|
if tag == "prop":
|
|
# build up props[]
|
|
props.append(info)
|
|
elif tag == "returns":
|
|
# close off the @prop list
|
|
if props and currentPropHolder:
|
|
currentPropHolder["props"] = props
|
|
props = []
|
|
api["returns"] = info
|
|
currentPropHolder = info
|
|
elif tag == "param":
|
|
# close off the @prop list
|
|
if props and currentPropHolder:
|
|
currentPropHolder["props"] = props
|
|
props = []
|
|
working_set["params"].append(info)
|
|
currentPropHolder = info
|
|
elif tag == "argument":
|
|
# close off the @prop list
|
|
if props and currentPropHolder:
|
|
currentPropHolder["props"] = props
|
|
props = []
|
|
working_set["arguments"].append(info)
|
|
currentPropHolder = info
|
|
else:
|
|
raise ParseError("unknown '@' section header %s in \
|
|
'%s'" % (tag, line), lineno + 1)
|
|
lineno += 1
|
|
raise ParseError("closing </api> tag not found for <api name=\"" +
|
|
api["name"] + "\">", lineno + 1)
|
|
|
|
def _parse_title_line(self, title_line, lineno):
|
|
if "name" not in title_line:
|
|
raise ParseError("Opening <api> tag must have a name attribute.",
|
|
lineno)
|
|
m = re.search("name=['\"]{0,1}([-\w\.]*?)['\"]", title_line)
|
|
if not m:
|
|
raise ParseError("No value for name attribute found in "
|
|
"opening <api> tag.", lineno)
|
|
return m.group(1)
|
|
|
|
def _is_description_line(self, line):
|
|
return not ( (line.lstrip().startswith("@")) or
|
|
(line.lstrip().startswith("<api")) or
|
|
(line.lstrip().startswith("</api")) )
|
|
|
|
def _initialize_working_set(self):
|
|
# working_set accumulates api elements
|
|
# that might belong to a parent api element
|
|
working_set = {}
|
|
working_set["constructors"] = []
|
|
working_set["methods"] = []
|
|
working_set["properties"] = []
|
|
working_set["params"] = []
|
|
working_set["events"] = []
|
|
working_set["arguments"] = []
|
|
return working_set
|
|
|
|
def _update_working_set(self, nested_api, working_set):
|
|
# add this api element to whichever list is appropriate
|
|
if nested_api["type"] == "constructor":
|
|
working_set["constructors"].append(nested_api)
|
|
if nested_api["type"] == "method":
|
|
working_set["methods"].append(nested_api)
|
|
if nested_api["type"] == "property":
|
|
working_set["properties"].append(nested_api)
|
|
if nested_api["type"] == "event":
|
|
working_set["events"].append(nested_api)
|
|
|
|
def _assemble_signature(self, api_element, params):
|
|
signature = api_element["name"] + "("
|
|
if len(params) > 0:
|
|
signature += params[0]["name"]
|
|
for param in params[1:]:
|
|
signature += ", " + param["name"]
|
|
signature += ")"
|
|
api_element["signature"] = signature
|
|
|
|
def _assemble_api_element(self, api_element, working_set):
|
|
# if any of this working set's lists are non-empty,
|
|
# add it to the current api element
|
|
if (api_element["type"] == "constructor") or \
|
|
(api_element["type"] == "function") or \
|
|
(api_element["type"] == "method"):
|
|
self._assemble_signature(api_element, working_set["params"])
|
|
if len(working_set["params"]) > 0:
|
|
api_element["params"] = working_set["params"]
|
|
if len(working_set["properties"]) > 0:
|
|
api_element["properties"] = working_set["properties"]
|
|
if len(working_set["constructors"]) > 0:
|
|
api_element["constructors"] = working_set["constructors"]
|
|
if len(working_set["methods"]) > 0:
|
|
api_element["methods"] = working_set["methods"]
|
|
if len(working_set["events"]) > 0:
|
|
api_element["events"] = working_set["events"]
|
|
if len(working_set["arguments"]) > 0:
|
|
api_element["arguments"] = working_set["arguments"]
|
|
|
|
def _validate_info(self, tag, info, line, lineno):
|
|
if tag == 'property':
|
|
if not 'datatype' in info:
|
|
raise ParseError("No type found for @property.", lineno)
|
|
elif tag == "param":
|
|
if info.get("required", False) and "default" in info:
|
|
raise ParseError(
|
|
"required parameters should not have defaults: '%s'"
|
|
% line, lineno)
|
|
elif tag == "prop":
|
|
if "datatype" not in info:
|
|
raise ParseError("@prop lines must include {type}: '%s'" %
|
|
line, lineno)
|
|
if "name" not in info:
|
|
raise ParseError("@prop lines must provide a name: '%s'" %
|
|
line, lineno)
|
|
|
|
def _parseTypeLine(self, line, lineno):
|
|
# handle these things:
|
|
# @method
|
|
# @returns description
|
|
# @returns {string} description
|
|
# @param NAME {type} description
|
|
# @param NAME
|
|
# @prop NAME {type} description
|
|
# @prop NAME
|
|
# returns:
|
|
# tag: type of api element
|
|
# info: linenumber, required, default, name, datatype
|
|
# description
|
|
|
|
info = {"line_number": lineno}
|
|
line = line.rstrip("\n")
|
|
pieces = line.split()
|
|
|
|
if not pieces:
|
|
raise ParseError("line is too short: '%s'" % line, lineno)
|
|
if not pieces[0].startswith("@"):
|
|
raise ParseError("type line should start with @: '%s'" % line,
|
|
lineno)
|
|
tag = pieces[0][1:]
|
|
skip = 1
|
|
|
|
expect_name = tag in ("param", "prop")
|
|
|
|
if len(pieces) == 1:
|
|
description = ""
|
|
else:
|
|
if pieces[1].startswith("{"):
|
|
# NAME is missing, pieces[1] is TYPE
|
|
pass
|
|
else:
|
|
if expect_name:
|
|
info["required"] = not pieces[1].startswith("[")
|
|
name = pieces[1].strip("[ ]")
|
|
if "=" in name:
|
|
name, info["default"] = name.split("=")
|
|
info["name"] = name
|
|
skip += 1
|
|
|
|
if len(pieces) > skip and pieces[skip].startswith("{"):
|
|
info["datatype"] = pieces[skip].strip("{ }")
|
|
skip += 1
|
|
|
|
# we've got the metadata, now extract the description
|
|
pieces = line.split(None, skip)
|
|
if len(pieces) > skip:
|
|
description = pieces[skip]
|
|
else:
|
|
description = ""
|
|
self._validate_info(tag, info, line, lineno)
|
|
return tag, info, description
|
|
|
|
def parse_hunks(text):
|
|
# return a list of tuples. Each is one of:
|
|
# ("raw", string) : non-API blocks
|
|
# ("api-json", dict) : API blocks
|
|
yield ("version", VERSION)
|
|
lines = text.splitlines(True)
|
|
line_number = 0
|
|
markdown_string = ""
|
|
while line_number < len(lines):
|
|
line = lines[line_number]
|
|
if line.startswith("<api"):
|
|
if len(markdown_string) > 0:
|
|
yield ("markdown", markdown_string)
|
|
markdown_string = ""
|
|
api, line_number = APIParser().parse(lines, line_number)
|
|
# this business with 'leftover' is a horrible thing to do,
|
|
# and exists only to collect the \n after the closing /api tag.
|
|
# It's not needed probably, except to help keep compatibility
|
|
# with the previous behaviour
|
|
leftover = lines[line_number].lstrip("</api>")
|
|
if len(leftover) > 0:
|
|
markdown_string += leftover
|
|
line_number = line_number + 1
|
|
yield ("api-json", api)
|
|
else:
|
|
markdown_string += line
|
|
line_number = line_number + 1
|
|
if len(markdown_string) > 0:
|
|
yield ("markdown", markdown_string)
|
|
|
|
class TestRenderer:
|
|
# render docs for test purposes
|
|
|
|
def getm(self, d, key):
|
|
return d.get(key, "<MISSING>")
|
|
|
|
def join_lines(self, text):
|
|
return " ".join([line.strip() for line in text.split("\n")])
|
|
|
|
def render_prop(self, p):
|
|
s = "props[%s]: " % self.getm(p, "name")
|
|
pieces = []
|
|
for k in ("type", "description", "required", "default"):
|
|
if k in p:
|
|
pieces.append("%s=%s" % (k, self.join_lines(str(p[k]))))
|
|
return s + ", ".join(pieces)
|
|
|
|
def render_param(self, p):
|
|
pieces = []
|
|
for k in ("name", "type", "description", "required", "default"):
|
|
if k in p:
|
|
pieces.append("%s=%s" % (k, self.join_lines(str(p[k]))))
|
|
yield ", ".join(pieces)
|
|
for prop in p.get("props", []):
|
|
yield " " + self.render_prop(prop)
|
|
|
|
def render_method(self, method):
|
|
yield "name= %s" % self.getm(method, "name")
|
|
yield "type= %s" % self.getm(method, "type")
|
|
yield "description= %s" % self.getm(method, "description")
|
|
signature = method.get("signature")
|
|
if signature:
|
|
yield "signature= %s" % self.getm(method, "signature")
|
|
params = method.get("params", [])
|
|
if params:
|
|
yield "parameters:"
|
|
for p in params:
|
|
for pline in self.render_param(p):
|
|
yield " " + pline
|
|
r = method.get("returns", None)
|
|
if r:
|
|
yield "returns:"
|
|
if "type" in r:
|
|
yield " type= %s" % r["type"]
|
|
if "description" in r:
|
|
yield " description= %s" % self.join_lines(r["description"])
|
|
props = r.get("props", [])
|
|
for p in props:
|
|
yield " " + self.render_prop(p)
|
|
|
|
def format_api(self, api):
|
|
for mline in self.render_method(api):
|
|
yield mline
|
|
constructors = api.get("constructors", [])
|
|
if constructors:
|
|
yield "constructors:"
|
|
for m in constructors:
|
|
for mline in self.render_method(m):
|
|
yield " " + mline
|
|
methods = api.get("methods", [])
|
|
if methods:
|
|
yield "methods:"
|
|
for m in methods:
|
|
for mline in self.render_method(m):
|
|
yield " " + mline
|
|
properties = api.get("properties", [])
|
|
if properties:
|
|
yield "properties:"
|
|
for p in properties:
|
|
yield " " + self.render_prop(p)
|
|
|
|
def render_docs(self, docs_json, outf=sys.stdout):
|
|
|
|
for (t,data) in docs_json:
|
|
if t == "api-json":
|
|
for line in self.format_api(data):
|
|
line = line.rstrip("\n")
|
|
outf.write("API: " + line + "\n")
|
|
else:
|
|
for line in str(data).split("\n"):
|
|
outf.write("MD :" + line + "\n")
|
|
|
|
def hunks_to_dict(docs_json):
|
|
exports = {}
|
|
for (t,data) in docs_json:
|
|
if t != "api-json":
|
|
continue
|
|
if data["name"]:
|
|
exports[data["name"]] = data
|
|
return exports
|
|
|
|
if __name__ == "__main__":
|
|
json = False
|
|
if sys.argv[1] == "--json":
|
|
json = True
|
|
del sys.argv[1]
|
|
docs_text = open(sys.argv[1]).read()
|
|
docs_parsed = list(parse_hunks(docs_text))
|
|
if json:
|
|
import simplejson
|
|
print simplejson.dumps(docs_parsed, indent=2)
|
|
else:
|
|
TestRenderer().render_docs(docs_parsed)
|