aboutsummaryrefslogtreecommitdiff
path: root/tools/addon-sdk-1.5/python-lib/cuddlefish/docs/apiparser.py
blob: b6ccf22623383e42f23b7b97d4cbe5d4d00f3da9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
# 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)