diff options
author | Rogan Creswick <creswick@galois.com> | 2012-03-30 17:07:02 -0700 |
---|---|---|
committer | Rogan Creswick <creswick@galois.com> | 2012-03-30 17:07:02 -0700 |
commit | f6ab6622aab00fe7c2f4c3dc41f786ebbe0f0d73 (patch) | |
tree | 870111038542cd27153e1396ebdc063573249689 /tools/addon-sdk-1.3/python-lib/cuddlefish/docs |
initial revision
Diffstat (limited to 'tools/addon-sdk-1.3/python-lib/cuddlefish/docs')
6 files changed, 1318 insertions, 0 deletions
diff --git a/tools/addon-sdk-1.3/python-lib/cuddlefish/docs/__init__.py b/tools/addon-sdk-1.3/python-lib/cuddlefish/docs/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tools/addon-sdk-1.3/python-lib/cuddlefish/docs/__init__.py diff --git a/tools/addon-sdk-1.3/python-lib/cuddlefish/docs/apiparser.py b/tools/addon-sdk-1.3/python-lib/cuddlefish/docs/apiparser.py new file mode 100644 index 0000000..05a24fc --- /dev/null +++ b/tools/addon-sdk-1.3/python-lib/cuddlefish/docs/apiparser.py @@ -0,0 +1,388 @@ +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) diff --git a/tools/addon-sdk-1.3/python-lib/cuddlefish/docs/apirenderer.py b/tools/addon-sdk-1.3/python-lib/cuddlefish/docs/apirenderer.py new file mode 100644 index 0000000..2488aa9 --- /dev/null +++ b/tools/addon-sdk-1.3/python-lib/cuddlefish/docs/apirenderer.py @@ -0,0 +1,299 @@ +import sys, os +import markdown +import apiparser +import time + +# list of all the 'class' and 'id' attributes assigned to +# <div> and <span> tags by the renderer. +API_REFERENCE = 'api_reference' +MODULE_API_DOCS_CLASS = 'module_api_docs' +MODULE_API_DOCS_ID = '_module_api_docs' +API_HEADER = 'api_header' +API_NAME = 'api_name' +API_COMPONENT_GROUP = 'api_component_group' +API_COMPONENT = 'api_component' +DATATYPE = 'datatype' +RETURNS = 'returns' +PARAMETER_SET = 'parameter_set' +MODULE_DESCRIPTION = 'module_description' + +HTML_HEADER = ''' +<!DOCTYPE html>\n +<html>\n +<head>\n + <meta http-equiv="Content-type" content="text/html; charset=utf-8" />\n + <base target="_blank"/>\n + <link rel="stylesheet" type="text/css" media="all"\n + href="../../../css/base.css" />\n + <link rel="stylesheet" type="text/css" media="all"\n + href="../../../css/apidocs.css" />\n + <title>Add-on SDK Documentation</title>\n + <style type="text/css">\n + body {\n + border: 50px solid #FFFFFF;\n + }\n + </style>\n +\n + <script type="text/javascript">\n + function rewrite_links() {\n + var images = document.getElementsByTagName("img");\n + for (var i = 0; i < images.length; i++) {\n + var before = images[i].src.split("packages/")[0];\n + var after = images[i].src.split("/docs")[1];\n + images[i].src = before + after;\n + }\n + }\n + </script>\n +</head>\n +\n +<body onload = "rewrite_links()">\n''' + +HTML_FOOTER = ''' +</body>\n +\n +</html>\n''' + +def indent(text_in): + text_out = '' + lines = text_in.splitlines(True) + indentation_level = 0 + indentation_depth = 2 + for line in lines: + if (line.startswith('<div')): + text_out += ((' ' * indentation_depth) * indentation_level) + line + if not '</div>' in line: + indentation_level += 1 + else: + if (line.startswith('</div>')): + indentation_level -= 1 + text_out += ((' ' * indentation_depth) * indentation_level) + line + return text_out + +def tag_wrap_id(text, classname, id, tag = 'div'): + return ''.join(['\n<'+ tag + ' id="', id, '" class="', \ + classname, '">\n', text + '\n</' + tag +'>\n']) + +def tag_wrap(text, classname, tag = 'div', inline = False): + if inline: + return ''.join(['\n<' + tag + ' class="', classname, '">', \ + text, '</'+ tag + '>\n']) + else: + return ''.join(['\n<' + tag + ' class="', classname, '">', \ + text, '\n</'+ tag + '>\n']) + +def tag_wrap_inline(text, classname, tag = 'div'): + return ''.join(['\n<' + tag + ' class="', classname, '">', \ + text, '</'+ tag + '>\n']) + +def span_wrap(text, classname): + return ''.join(['<span class="', classname, '">', \ + text, '</span>']) + +class API_Renderer(object): + def __init__(self, json, tag): + self.name = json.get('name', None) + self.tag = tag + self.description = json.get('description', '') + self.json = json + + def render_name(self): + raise Exception('not implemented in this class') + + def render_description(self): + return markdown.markdown(self.description) + + def render_subcomponents(self): + raise Exception('not implemented in this class') + + def get_tag(self): + return self.tag + +class Class_Doc(API_Renderer): + def __init__(self, json, tag): + API_Renderer.__init__(self, json, tag) + + def render_name(self): + return self.name + + def render_subcomponents(self): + return render_object_contents(self.json, 'h5', 'h6') + +class Event_Doc(API_Renderer): + def __init__(self, json, tag): + API_Renderer.__init__(self, json, tag) + self.arguments_json = json.get('arguments', None) + + def render_name(self): + return self.name + + def render_subcomponents(self): + if not self.arguments_json: + return '' + text = ''.join([render_comp(Argument_Doc(argument_json, 'div')) \ + for argument_json in self.arguments_json]) + return tag_wrap(text, PARAMETER_SET) + +class Argument_Doc(API_Renderer): + def __init__(self, json, tag): + API_Renderer.__init__(self, json, tag) + self.datatype = json.get('datatype', None) + + def render_name(self): + return span_wrap(self.datatype, DATATYPE) + + def render_subcomponents(self): + return '' + +class Function_Doc(API_Renderer): + def __init__(self, json, tag): + API_Renderer.__init__(self, json, tag) + self.signature = json['signature'] + self.returns = json.get('returns', None) + self.parameters_json = json.get('params', None) + + def render_name(self): + return self.signature + + def render_subcomponents(self): + return self._render_parameters() + self._render_returns() + + def _render_parameters(self): + if not self.parameters_json: + return '' + text = ''.join([render_comp(Parameter_Doc(parameter_json, 'div')) \ + for parameter_json in self.parameters_json]) + return tag_wrap(text, PARAMETER_SET) + + def _render_returns(self): + if not self.returns: + return '' + text = 'Returns: ' + span_wrap(self.returns['datatype'], DATATYPE) + text += markdown.markdown(self.returns['description']) + return tag_wrap(text, RETURNS) + +class Property_Doc(API_Renderer): + def __init__(self, json, tag): + API_Renderer.__init__(self, json, tag) + self.datatype = json.get('datatype', None) + self.required = json.get('required', True) + self.default = json.get('default', False) + + def render_name(self): + rendered = self.name + if self.default: + rendered = rendered + " = " + self.default + if self.datatype: + rendered = rendered + ' : ' + span_wrap(self.datatype, DATATYPE) + if not self.required: + rendered = '[ ' + rendered + ' ]' + return rendered + + def render_subcomponents(self): + return render_object_contents(self.json) + +class Parameter_Doc(Property_Doc): + def __init__(self, json, tag): + Property_Doc.__init__(self, json, tag) + self.properties_json = json.get('props', None) + + def render_subcomponents(self): + if not self.properties_json: + return '' + text = ''.join([render_comp(Property_Doc(property_json, 'div')) \ + for property_json in self.properties_json]) + return text + +def render_object_contents(json, tag = 'div', comp_tag = 'div'): + ctors = json.get('constructors', None) + text = render_comp_group(ctors, 'Constructors', Function_Doc, tag, comp_tag) + methods = json.get('methods', None) + text += render_comp_group(methods, 'Methods', Function_Doc, tag, comp_tag) + properties = json.get('properties', None) + text += render_comp_group(properties, 'Properties', Property_Doc, tag, comp_tag) + events = json.get('events', None) + text += render_comp_group(events, 'Events', Event_Doc, tag, comp_tag) + return text + +def render_comp(component): + # a component is wrapped inside a single div marked 'API_COMPONENT' + # containing: + # 1) the component name, marked 'API_NAME' + text = tag_wrap(component.render_name(), API_NAME, component.get_tag(), True) + # 2) the component description + text += component.render_description() + # 3) the component contents + text += component.render_subcomponents() + return tag_wrap(text, API_COMPONENT) + +def render_comp_group(group, group_name, ctor, tag = 'div', comp_tag = 'div'): + if not group: + return '' + # component group is a list of components in a single div called + # 'API_COMPONENT_GROUP' containing: + # 1) a title for the group marked with 'API_HEADER' + text = tag_wrap(group_name, API_HEADER, tag, True) + # 2) each component + text += ''.join([render_comp(ctor(api, comp_tag)) for api in group]) + return tag_wrap(text, API_COMPONENT_GROUP) + +def render_descriptions(descriptions_md): + text = ''.join([description_md for description_md in descriptions_md]) + return tag_wrap(markdown.markdown(text), MODULE_DESCRIPTION) + +def render_api_reference(api_docs): + if (len(api_docs) == 0): + return '' + # at the top level api reference is in a single div marked 'API_REFERENCE', + # containing: + # 1) a title 'API Reference' marked with 'API_HEADER' + text = tag_wrap('API Reference', API_HEADER, 'h2', True) + # 2) a component group called 'Classes' containing any class elements + classes = [api for api in api_docs if api['type'] == 'class'] + text += render_comp_group(classes, 'Classes', Class_Doc, 'h3', 'h4') + # 3) a component group called 'Functions' containing any global functions + functions = [api for api in api_docs if api['type'] == 'function'] + text += render_comp_group(functions, 'Functions', Function_Doc, 'h3', 'h4') + # 4) a component group called 'Properties' containing any global properties + properties = [api for api in api_docs if api['type'] == 'property'] + text += render_comp_group(properties, 'Properties', Property_Doc, 'h3', 'h4') + # 5) a component group called 'Events' containing any global events + events = [api for api in api_docs if api['type'] == 'event'] + text += render_comp_group(events, 'Events', Event_Doc, 'h3', 'h4') + return tag_wrap(text, API_REFERENCE) + +# take the JSON output of apiparser +# return the HTML DIV containing the rendered component +def json_to_div(json, markdown_filename): + module_name, ext = os.path.splitext(os.path.basename(markdown_filename)) + descriptions = [hunk[1] for hunk in json if hunk[0]=='markdown'] + api_docs = [hunk[1] for hunk in json if hunk[0]=='api-json'] + text = "<h1>" + module_name + "</h1>" + text += render_descriptions(descriptions) + text += render_api_reference(api_docs) + text = tag_wrap_id(text, MODULE_API_DOCS_CLASS, \ + module_name + MODULE_API_DOCS_ID) + return text.encode('utf8') + +# take the JSON output of apiparser +# return standalone HTML containing the rendered component +def json_to_html(json, markdown_filename): + return indent(HTML_HEADER + \ + json_to_div(json, markdown_filename) + HTML_FOOTER) + +# take the name of a Markdown file +# return the HTML DIV containing the rendered component +def md_to_div(markdown_filename): + markdown_contents = open(markdown_filename).read().decode('utf8') + json = list(apiparser.parse_hunks(markdown_contents)) + return json_to_div(json, markdown_filename) + +# take the name of a Markdown file +# return standalone HTML containing the rendered component +def md_to_html(markdown_filename): + return indent(HTML_HEADER + md_to_div(markdown_filename) + HTML_FOOTER) + +if __name__ == '__main__': + if (len(sys.argv) == 0): + print 'Supply the name of a docs file to parse' + else: + print md_to_html(sys.argv[1]) diff --git a/tools/addon-sdk-1.3/python-lib/cuddlefish/docs/generate.py b/tools/addon-sdk-1.3/python-lib/cuddlefish/docs/generate.py new file mode 100644 index 0000000..1c0fbf9 --- /dev/null +++ b/tools/addon-sdk-1.3/python-lib/cuddlefish/docs/generate.py @@ -0,0 +1,230 @@ +import os +import sys +import shutil +import hashlib +import tarfile +import StringIO + +from cuddlefish import packaging +from cuddlefish import Bunch +from cuddlefish.docs import apiparser +from cuddlefish.docs import apirenderer +from cuddlefish.docs import webdocs +import simplejson as json + +DOCS_DIR = "doc" +DIGEST = "status.md5" +TGZ_FILENAME = "addon-sdk-docs.tgz" + +def clean_generated_docs(docs_dir): + status_file = os.path.join(docs_dir, "status.md5") + if os.path.exists(status_file): + os.remove(status_file) + index_file = os.path.join(docs_dir, "index.html") + if os.path.exists(index_file): + os.remove(index_file) + dev_guide_dir = os.path.join(docs_dir, "dev-guide") + if os.path.exists(dev_guide_dir): + shutil.rmtree(dev_guide_dir) + api_doc_dir = os.path.join(docs_dir, "packages") + if os.path.exists(api_doc_dir): + shutil.rmtree(api_doc_dir) + +def generate_static_docs(env_root, base_url=None): + docs_dir = os.path.join(env_root, DOCS_DIR) + clean_generated_docs(docs_dir) + generate_docs(env_root, base_url=base_url, stdout=StringIO.StringIO()) + tgz = tarfile.open(TGZ_FILENAME, 'w:gz') + tgz.add(docs_dir, DOCS_DIR) + tgz.close() + return TGZ_FILENAME + +def generate_docs(env_root, base_url=None, filename=None, stdout=sys.stdout): + docs_dir = os.path.join(env_root, DOCS_DIR) + base_url = calculate_base_url(base_url, docs_dir) + # if we were given a filename, just generate the named file + # and return its URL + if filename: + return generate_named_file(env_root, base_url, filename) + # if the generated docs don't exist, generate everything + if not os.path.exists(os.path.join(docs_dir, "index.html")): + print >>stdout, "Generating documentation..." + generate_docs_from_scratch(env_root, base_url, docs_dir) + current_status = calculate_current_status(env_root) + open(os.path.join(env_root, DOCS_DIR, DIGEST), "w").write(current_status) + else: + current_status = calculate_current_status(env_root) + previous_status_file = os.path.join(env_root, DOCS_DIR, DIGEST) + docs_are_up_to_date = False + if os.path.exists(previous_status_file): + docs_are_up_to_date = current_status == open(previous_status_file, "r").read() + # if the docs are not up to date, generate everything + if not docs_are_up_to_date: + print >>stdout, "Regenerating documentation..." + generate_docs_from_scratch(env_root, base_url, docs_dir) + open(os.path.join(env_root, DOCS_DIR, DIGEST), "w").write(current_status) + return base_url + "index.html" + +def calculate_base_url(base_url, docs_dir): + if base_url == None: + base_url_path = docs_dir + # this is to ensure the path starts with "/" + # whether or not it's on Windows + # there might be a better way + if not docs_dir.startswith("/"): + base_url_path = "/" + base_url_path + base_url_path_pieces = base_url_path.split(os.sep) + base_url = "file://" + "/".join(base_url_path_pieces) + "/" + return base_url + +def generate_named_file(env_root, base_url, filename): + docs_dir = os.path.join(env_root, DOCS_DIR) + web_docs = webdocs.WebDocs(env_root, base_url) + + # next, generate api doc or guide doc + abs_path = os.path.abspath(filename) + if abs_path.startswith(os.path.join(env_root, 'packages')): + return generate_api_doc(env_root, abs_path, web_docs) + elif abs_path.startswith(os.path.join(env_root, DOCS_DIR, 'dev-guide-source')): + return generate_guide_doc(env_root, abs_path, web_docs) + else: + raise ValueError("Not a valid path to a documentation file") + +# this function builds a hash of the name and last modification date of: +# * every file in "packages" which ends in ".md" +# * every file in "static-files" which does not start with "." +def calculate_current_status(env_root): + current_status = hashlib.md5() + package_src_dir = os.path.join(env_root, "packages") + for (dirpath, dirnames, filenames) in os.walk(package_src_dir): + for filename in filenames: + if filename.endswith(".md"): + current_status.update(filename) + current_status.update(str(os.path.getmtime(os.path.join(dirpath, filename)))) + guide_src_dir = os.path.join(env_root, DOCS_DIR, "dev-guide-source") + for (dirpath, dirnames, filenames) in os.walk(guide_src_dir): + for filename in filenames: + if filename.endswith(".md"): + current_status.update(filename) + current_status.update(str(os.path.getmtime(os.path.join(dirpath, filename)))) + base_html_file = os.path.join(env_root, DOCS_DIR, "static-files", "base.html") + current_status.update(base_html_file) + current_status.update(str(os.path.getmtime(os.path.join(dirpath, base_html_file)))) + return current_status.digest() + +def generate_docs_from_scratch(env_root, base_url, docs_dir): + web_docs = webdocs.WebDocs(env_root, base_url) + clean_generated_docs(docs_dir) + + # py2.5 doesn't have ignore=, so we delete tempfiles afterwards. If we + # required >=py2.6, we could use ignore=shutil.ignore_patterns("*~") + for (dirpath, dirnames, filenames) in os.walk(docs_dir): + for n in filenames: + if n.endswith("~"): + os.unlink(os.path.join(dirpath, n)) + + # generate api docs from all packages + os.mkdir(os.path.join(docs_dir, "packages")) + # create the index file and save that + pkg_cfg = packaging.build_pkg_cfg(env_root) + index = json.dumps(packaging.build_pkg_index(pkg_cfg)) + index_path = os.path.join(docs_dir, "packages", 'index.json') + open(index_path, 'w').write(index) + + # for each package, generate its docs + for pkg_name, pkg in pkg_cfg['packages'].items(): + src_dir = pkg.root_dir + package_dirname = os.path.basename(src_dir) + dest_dir = os.path.join(docs_dir, "packages", package_dirname) + os.mkdir(dest_dir) + + src_readme = os.path.join(src_dir, "README.md") + if os.path.exists(src_readme): + shutil.copyfile(src_readme, + os.path.join(dest_dir, "README.md")) + + # create the package page + package_filename = os.path.join(dest_dir, pkg_name + ".html") + if not os.path.exists(package_filename): + package_doc_html = web_docs.create_package_page(pkg_name) + open(package_filename, "w").write(package_doc_html) + + # generate all the API docs + docs_src_dir = os.path.join(src_dir, "doc") + if os.path.isdir(os.path.join(src_dir, "docs")): + docs_src_dir = os.path.join(src_dir, "docs") + generate_file_tree(env_root, docs_src_dir, web_docs, generate_api_doc) + + # generate all the guide docs + dev_guide_src = os.path.join(env_root, DOCS_DIR, "dev-guide-source") + generate_file_tree(env_root, dev_guide_src, web_docs, generate_guide_doc) + + # make /md/dev-guide/welcome.html the top level index file + shutil.copy(os.path.join(env_root, DOCS_DIR, 'dev-guide', 'welcome.html'), \ + os.path.join(docs_dir, 'index.html')) + +def generate_file_tree(env_root, src_dir, web_docs, generate_file): + for (dirpath, dirnames, filenames) in os.walk(src_dir): + assert dirpath.startswith(src_dir) # what is this for?? + for filename in filenames: + if filename.endswith("~"): + continue + src_path = os.path.join(dirpath, filename) + generate_file(env_root, src_path, web_docs) + +def generate_api_doc(env_root, src_dir, web_docs): + if src_dir.endswith(".md"): + dest_dir, filename = get_api_doc_dest_path(env_root, src_dir) + if not os.path.exists(dest_dir): + os.makedirs(dest_dir) + + # parse and JSONify the API docs + docs_md = open(src_dir, 'r').read() + docs_parsed = list(apiparser.parse_hunks(docs_md)) + docs_json = json.dumps(docs_parsed) + dest_path_json = os.path.join(dest_dir, filename) + ".json" + replace_file(dest_path_json, docs_json) + + # write the HTML div files + docs_div = apirenderer.json_to_div(docs_parsed, src_dir) + dest_path_div = os.path.join(dest_dir, filename) + ".div" + replace_file(dest_path_div, docs_div) + + # write the standalone HTML files + docs_html = web_docs.create_module_page(src_dir) + dest_path_html = os.path.join(dest_dir, filename) + ".html" + replace_file(dest_path_html, docs_html) + + return dest_path_html + +def generate_guide_doc(env_root, src_dir, web_docs): + if src_dir.endswith(".md"): + dest_dir, filename = get_guide_doc_dest_path(env_root, src_dir) + if not os.path.exists(dest_dir): + os.makedirs(dest_dir) + # write the standalone HTML files + docs_html = web_docs.create_guide_page(src_dir) + dest_path_html = os.path.join(dest_dir, filename) + ".html" + replace_file(dest_path_html, docs_html) + return dest_path_html + +def replace_file(dest_path, file_contents): + if os.path.exists(dest_path): + os.remove(dest_path) + open(dest_path, "w").write(file_contents) + +# Given the full path to an API source file, and the root, +# return a tuple of: +# 1) the full path to the corresponding HTML file, without the filename +# 2) the filename without the extension +def get_guide_doc_dest_path(env_root, src_dir): + src_dir_relative = src_dir[len(os.path.join(env_root, DOCS_DIR, "dev-guide-source")) + 1:] + return os.path.split(os.path.join(env_root, DOCS_DIR, "dev-guide", src_dir_relative)[:-3]) + +# Given the full path to a dev guide source file, and the root, +# return a tuple of: +# 1) the full path to the corresponding HTML file, without the filename +# 2) the filename without the extension +def get_api_doc_dest_path(env_root, src_dir): + src_dir_relative = src_dir[len(env_root) + 1:] + return os.path.split(os.path.join(env_root, DOCS_DIR, src_dir_relative)[:-3])
\ No newline at end of file diff --git a/tools/addon-sdk-1.3/python-lib/cuddlefish/docs/renderapi.readme.md b/tools/addon-sdk-1.3/python-lib/cuddlefish/docs/renderapi.readme.md new file mode 100644 index 0000000..7086fe1 --- /dev/null +++ b/tools/addon-sdk-1.3/python-lib/cuddlefish/docs/renderapi.readme.md @@ -0,0 +1,206 @@ + +This document describes the structure of the HTML generated by the renderapi.py +tool, both for use in the API docs shown by "cfx docs" and as exported by +"cfx sdocs". The particular HTML id and class attributes embedded in the files, +as well as their organization, represent the interface between the tool and any +front-end code wanting to style the docs in some particular way. + +renderapi generates two sorts of files: + +- a file called "<module-name>.div": this is the contents of the parsed +Markdown file rendered inside a well-defined DIV tag + +- a file called "<module-name>.html": this is the DIV from above inserted into +a simple HTML template that references a sample CSS file which styles the +contents of the DIV. This CSS file is the same as the one used by the SDK +itself. + +DIV tags +-------- +The following class and id attributes are used in the DIV: + +renderapi uses a number of class attributes and a single id attribute in the DIV: + +id attribute <module_name>"_module_api_docs" +class attribute "api_reference" +class attribute "module_api_docs" +class attribute "api_header" +class attribute "api_name" +class attribute "api_component_group" +class attribute "api_component" +class attribute "datatype" +class attribute "returns" +class attribute "parameter_set" +class attribute "module_description" + +DIV structure +------------- +The top level DIV is marked with the id attribute and the "module_api_docs" class +attribute: + + <div id='tabs_module_api_docs' class='module_api_docs'> + //module doc contents + </div> + + +Inside this: + +- the first item is an <h1> heading containing the name of the module: + +- all "markdown" hunks (that is, all descriptive text not occurring +inside <api></api> tags) are rendered inside a DIV marked with the +"module-description" class attribute + +- all <api></api> content is rendered, enclosed in a single tag marked +with the "api_reference" class attribute: + + <div id='tabs_module_api_docs' class='module_api_docs'> + <div class='module_description'> + //descriptions + </div> + <div class='api_reference'> + //api reference + </div> + </div> + +If there is no <api></api> content, then the "api-reference" section is absent. + +### API Reference structure ### + +The first item in API reference is an <h2> heading title marked with the +"api_header" attribute. This might have the text content "API Reference" +(but you should not rely on that): + + <div class='api_reference'> + + <h2 class='api_header'>API Reference</h2> + + //api contents + + </div> + +After the title come one or more component groups. + +#### Component Group #### + +A component group is marked with the "api_component_group" attribute. The +component group is a collection of some sort of component: for example, a group +of classes, a group of functions, or a group of events. + +Each component group starts off with a header marked with the +"api_header" attribute and is followed by one or more sections marked with the +"api_component" attribute. +At the top level (that is, when they are directly under the "API Reference" +heading), the "api_header" items are <h3> headings, otherwise they are divs. + + <div class='api_reference'> + + <h2 class='api_header'>API Reference</h2> + + <div class='api_component_group'> + + <h3 class='api_header'>Classes</h3> + + <div class='api_component'> + // the first class + </div> + + <div class='api_component'> + // another class + </div> + + </div> + + <div class='api_component_group'> + //some different components + + <h3 class='api_header'>Functions</h3> + + <div class='api_component'> + the first function + </div> + + <div class='api_component'> + another function + </div> + + </div> + + </div> + +#### Component #### + +API components represent actual objects in the API like classes, functions, +properties and events. + +Each component starts with a section marked with the +"api_name" tag, which includes the name of the component in the API: for +example "postMessage(message)". + +Components at the top level (i.e., directly under h3 headings) are <h4> +headings, otherwise they are divs. + +After the name, the component's contents are listed. Different sorts of +components may have different sorts of contents: for example, a function might +have parameters. If the component is composite then it may contain its own +component group. For example, a class may contain methods and properties, +which might be grouped together. + + <div class='api_component'> + + <h4 class='api_name'>Panel</h4> + + <div class='api_component_group'> + + <div class='api_header'> + Methods + </div> + + <div class='api_component'> + show() + </div> + + </div> + + </div> + +Other attributes +----------------------------- + +### Datatype ### +All primitive data types, like "string" and "number", are marked with the +"datatype" class attribute: + + <div class="api_component"> + + <div class="api_name"> + label : <span class="datatype">string</span> + </div> + + <p>A required string description of the widget used for accessibility, + title bars, and error reporting.</p> + + </div> + +### Returns ### + +Functions mark return values with the "returns" class attribute. + + <div class="api_component"> + + <div class="api_name"> + get() + </div> + + Make a `GET` request. + + <div class="returns"> + Returns: <span class="datatype">Request</span> + </div> + + </div> + +### Parameter_set ### + +Functions that take parameters mark them with the parameter_set class +attribute. diff --git a/tools/addon-sdk-1.3/python-lib/cuddlefish/docs/webdocs.py b/tools/addon-sdk-1.3/python-lib/cuddlefish/docs/webdocs.py new file mode 100644 index 0000000..3528ac3 --- /dev/null +++ b/tools/addon-sdk-1.3/python-lib/cuddlefish/docs/webdocs.py @@ -0,0 +1,195 @@ +import sys, os, re, errno +import markdown +import simplejson as json + +from cuddlefish import packaging +from cuddlefish import Bunch +from cuddlefish.docs import apiparser +from cuddlefish.docs import apirenderer + +INDEX_PAGE = '/doc/static-files/base.html' +BASE_URL_INSERTION_POINT = '<base ' +HIGH_LEVEL_PACKAGE_SUMMARIES = '<li id="high-level-package-summaries">' +LOW_LEVEL_PACKAGE_SUMMARIES = '<li id="low-level-package-summaries">' +CONTENT_ID = '<div id="main-content">' +TITLE_ID = '<title>' +DEFAULT_TITLE = 'Add-on SDK Documentation' + +def get_modules(modules_json): + modules = [] + for name in modules_json: + typ = modules_json[name][0] + if typ == "directory": + sub_modules = get_modules(modules_json[name][1]) + for sub_module in sub_modules: + modules.append([name, sub_module[0]]) + elif typ == "file": + if not name.startswith(".") and name.endswith('.js'): + modules.append([name[:-3]]) + return modules + +def get_documented_modules(package_name, modules_json, doc_path): + modules = get_modules(modules_json) + documented_modules = [] + for module in modules: + path = os.path.join(*module) + if module_md_exists(doc_path, path): + documented_modules.append(module) + if package_name == "addon-kit": + # hack for bug 664001, self-maker.js is in api-utils, self.md is in + # addon-kit. Real fix is for this function to look for all .md files, + # not for .js files with matching .md file in the same package. + documented_modules.append(["self"]) + return documented_modules + +def module_md_exists(root, module_name): + module_md_path = os.path.join(root, module_name + '.md') + return os.path.exists(module_md_path) + +def tag_wrap(text, tag, attributes={}): + result = '\n<' + tag + for name in attributes.keys(): + result += ' ' + name + '=' + '"' + attributes[name] + '"' + result +='>' + text + '</'+ tag + '>\n' + return result + +def is_high_level(package_json): + return not is_low_level(package_json) + +def is_low_level(package_json): + return 'jetpack-low-level' in package_json.get('keywords', []) + +def insert_after(target, insertion_point_id, text_to_insert): + insertion_point = target.find(insertion_point_id) + len(insertion_point_id) + return target[:insertion_point] + text_to_insert + target[insertion_point:] + +class WebDocs(object): + def __init__(self, root, base_url = '/'): + self.root = root + self.pkg_cfg = packaging.build_pkg_cfg(root) + self.packages_json = packaging.build_pkg_index(self.pkg_cfg) + self.base_page = self._create_base_page(root, base_url) + + def create_guide_page(self, path): + path, ext = os.path.splitext(path) + md_path = path + '.md' + md_content = unicode(open(md_path, 'r').read(), 'utf8') + guide_content = markdown.markdown(md_content) + return self._create_page(guide_content) + + def create_module_page(self, path): + path, ext = os.path.splitext(path) + md_path = path + '.md' + module_content = apirenderer.md_to_div(md_path) + return self._create_page(module_content) + + def create_package_page(self, package_name): + package_content = self._create_package_detail(package_name) + return self._create_page(package_content) + + def _create_page(self, page_content): + page = self._insert_title(self.base_page, page_content) + page = insert_after(page, CONTENT_ID, page_content) + return page.encode('utf8') + + def _create_module_list(self, package_json): + package_name = package_json['name'] + libs = package_json['files'][1]['lib'][1] + doc_path = package_json.get('doc', None) + if not doc_path: + return '' + modules = get_documented_modules(package_name, libs, doc_path) + modules.sort() + module_items = '' + relative_doc_path = doc_path[len(self.root) + 1:] + relative_doc_URL = "/".join(relative_doc_path.split(os.sep)) + for module in modules: + module_link = tag_wrap('/'.join(module), 'a', \ + {'href': relative_doc_URL + '/' + '/'.join(module) + '.html'}) + module_items += tag_wrap(module_link, 'li', {'class':'module'}) + return tag_wrap(module_items, 'ul', {'class':'modules'}) + + def _create_package_summaries(self, packages_json, include): + packages = '' + for package_name in packages_json.keys(): + package_json = packages_json[package_name] + if not include(package_json): + continue + package_path = self.pkg_cfg["packages"][package_name]["root_dir"] + package_directory = package_path[len(self.root) + 1:] + package_directory = "/".join(package_directory.split(os.sep)) + package_link = tag_wrap(package_name, 'a', {'href': \ + package_directory + "/" \ + + package_name + '.html'}) + text = tag_wrap(package_link, 'h4') + text += self._create_module_list(package_json) + packages += tag_wrap(text, 'div', {'class':'package-summary', \ + 'style':'display: block;'}) + return packages + + def _create_base_page(self, root, base_url): + base_page = unicode(open(root + INDEX_PAGE, 'r').read(), 'utf8') + base_tag = 'href="' + base_url + '"' + base_page = insert_after(base_page, BASE_URL_INSERTION_POINT, base_tag) + high_level_summaries = \ + self._create_package_summaries(self.packages_json, is_high_level) + base_page = insert_after(base_page, \ + HIGH_LEVEL_PACKAGE_SUMMARIES, high_level_summaries) + low_level_summaries = \ + self._create_package_summaries(self.packages_json, is_low_level) + base_page = insert_after(base_page, \ + LOW_LEVEL_PACKAGE_SUMMARIES, low_level_summaries) + return base_page + + def _create_package_detail_row(self, field_value, \ + field_descriptor, field_name): + meta = tag_wrap(tag_wrap(field_descriptor, 'span', \ + {'class':'meta-header'}), 'td') + value = tag_wrap(tag_wrap(field_value, 'span', \ + {'class':field_name}), 'td') + return tag_wrap(meta + value, 'tr') + + def _create_package_detail_table(self, package_json): + table_contents = '' + if package_json.get('author', None): + table_contents += self._create_package_detail_row(\ + package_json['author'], 'Author', 'author') + if package_json.get('version', None): + table_contents += self._create_package_detail_row(\ + package_json['version'], 'Version', 'version') + if package_json.get('license', None): + table_contents += self._create_package_detail_row(\ + package_json['license'], 'License', 'license') + if package_json.get('dependencies', None): + table_contents += self._create_package_detail_row(\ + ', '.join(package_json['dependencies']), \ + 'Dependencies', 'dependencies') + table_contents += self._create_package_detail_row(\ + self._create_module_list(package_json), 'Modules', 'modules') + return tag_wrap(tag_wrap(table_contents, 'tbody'), 'table', \ + {'class':'meta-table'}) + + def _create_package_detail(self, package_name): + package_json = self.packages_json.get(package_name, None) + if not package_json: + raise IOError(errno.ENOENT, 'Package not found') + # pieces of the package detail: 1) title, 2) table, 3) description + package_title = tag_wrap(package_name, 'h1') + table = self._create_package_detail_table(package_json) + description = '' + if package_json.get('readme', None): + description += tag_wrap(tag_wrap(\ + markdown.markdown(\ + package_json['readme']), 'p'), 'div', {'class':'docs'}) + return tag_wrap(package_title + table + description, 'div', \ + {'class':'package-detail'}) + + def _insert_title(self, target, content): + match = re.search('<h1>.*</h1>', content) + if match: + title = match.group(0)[len('<h1>'):-len('</h1>')] + ' - ' + \ + DEFAULT_TITLE + else: + title = DEFAULT_TITLE + target = insert_after(target, TITLE_ID, title) + return target |