1"""
2This file opens the XML `index.xml` file generated by Doxygen
3which has the following structure:
4
5    <doxygenindex ...>
6      <compound ... >
7      </compound>
8      ...
9    </doxygenindex>
10
11Each <compound> element has a 'kind' attribute that is one of the following:
12
13    - define
14    - dir
15    - enum
16    - enumvalue
17    - example
18    - file
19    - function
20    - page
21    - struct
22    - typedef
23    - union
24    - variable
25
26Most <compound> elements have child <member> elements with their own contents
27depending on the 'kind' of <compound> element they are in.
28
29This file defines classes for each of these except for
30
31    - dir
32    - page
33    - example
34
35The remaining 'kind' values are:
36
37    - define
38    - enum
39    - enumvalue
40    - file
41    - function
42    - struct
43    - typedef
44    - union
45    - variable
46
47The list of classes is:
48
49    Class     | Adds Self to  Dictionary in __init__()
50    --------- | --------------------------------------
51    DEFINE    => `defines`
52    ENUM      => `enums`
53    VARIABLE  => `variables`
54    NAMESPACE => `namespaces`
55    STRUCT    => `structures`
56    UNION    appears to have a different purpose
57    TYPEDEF   => `typedefs`
58    FUNCTION  => `functions`
59    GROUP     => `groups`
60    FILE      => `files`
61    CLASS     => `classes`
62
63Additional classes:
64
65    - NAMESPACE(object):
66    - FUNC_ARG(object):      (becomes members of FUNCTION objects)
67    - STRUCT_FIELD(object):  (becomes members of STRUCT objects)
68    - GROUP(object):
69    - CLASS(object):
70    - XMLSearch(object):
71
72Each of the above Dictionary variables has entries with
73
74    - keys   = actual name of the code elements Doxygen found in the .H files.
75    - values = XML node generated by `xml.etree::ElementTree`
76
77Samples:
78
79    'defines': {'ZERO_MEM_SENTINEL': <doc_builder.DEFINE object at 0x000001FB5D866420>,
80                'LV_GLOBAL_DEFAULT': <doc_builder.DEFINE object at 0x000001FB5D866210>,
81                'LV_ASSERT_OBJ': <doc_builder.DEFINE object at 0x000001FB5D1EC080>,
82                'LV_TRACE_OBJ_CREATE': <doc_builder.DEFINE object at 0x000001FB5D8660F0>,
83
84    'enums': {'lv_key_t': <doc_builder.ENUM object at 0x000001FB5D1EEB40>,
85              'lv_group_refocus_policy_t': <doc_builder.ENUM object at 0x000001FB5D1E3DA0>,
86              'lv_obj_flag_t': <doc_builder.ENUM object at 0x000001FB5D29F830>,
87              'lv_obj_class_editable_t': <doc_builder.ENUM object at 0x000001FB5D29E300>,
88
89    'variables': {'lv_global': <doc_builder.VARIABLE object at 0x000001FB5D1E3FE0>,
90                  'lv_obj_class': <doc_builder.VARIABLE object at 0x000001FB5D1EE1E0>,
91                  'lv_font_montserrat_8': <doc_builder.VARIABLE object at 0x000001FB5DAB41A0>,
92                  'lv_font_montserrat_10': <doc_builder.VARIABLE object at 0x000001FB5D99D040>,
93
94    'namespaces': {},
95
96    'structures': {'_lv_anim_t::_lv_anim_path_para_t': <doc_builder.UNION object at 0x000001FB5C4240E0>,
97                   '_lv_anim_t': <doc_builder.STRUCT object at 0x000001FB5C45F680>,
98                   '_lv_animimg_t': <doc_builder.STRUCT object at 0x000001FB5C4FE390>,
99                   '_lv_arc_t': <doc_builder.STRUCT object at 0x000001FB59D350A0>,
100
101    'unions': {},
102
103    'typedefs': {'lv_global_t': <doc_builder.TYPEDEF object at 0x000001FB5D1EFFE0>,
104                 'lv_group_focus_cb_t': <doc_builder.TYPEDEF object at 0x000001FB5D1F1CA0>,
105                 'lv_group_edge_cb_t': <doc_builder.TYPEDEF object at 0x000001FB5D1EE7E0>,
106
107    'functions': {'lv_group_create': <doc_builder.FUNCTION object at 0x000001FB5D1E0470>,
108                  'lv_group_delete': <doc_builder.FUNCTION object at 0x000001FB5D1F3800>,
109                  'lv_group_set_default': <doc_builder.FUNCTION object at 0x000001FB5D1ECAA0>,
110
111Additional dictionaries:
112    'files': {'lv_global.h': <doc_builder.FILE object at 0x000001FB5D864E00>,
113              'lv_group.h': <doc_builder.FILE object at 0x000001FB5D1EFD40>,
114              'lv_group_private.h': <doc_builder.FILE object at 0x000001FB5D0D7DD0>,
115
116    'html_files': {'lvgl': 'lvgl.html',
117                   'lv_api_map_v8': 'lv_api_map_v8.html',
118                   'lv_api_map_v9_0': 'lv_api_map_v9_0.html',
119                   'lv_api_map_v9_1': 'lv_api_map_v9_1.html',
120
121"""
122import os
123import sys
124from xml.etree import ElementTree as ET
125
126base_path = ''
127xml_path = ''
128
129EMIT_WARNINGS = True
130DOXYGEN_OUTPUT = True
131
132MISSING_FUNC = 'MissingFunctionDoc'
133MISSING_FUNC_ARG = 'MissingFunctionArgDoc'
134MISSING_FUNC_RETURN = 'MissingFunctionReturnDoc'
135MISSING_FUNC_ARG_MISMATCH = 'FunctionArgMissing'
136MISSING_STRUCT = 'MissingStructureDoc'
137MISSING_STRUCT_FIELD = 'MissingStructureFieldDoc'
138MISSING_UNION = 'MissingUnionDoc'
139MISSING_UNION_FIELD = 'MissingUnionFieldDoc'
140MISSING_ENUM = 'MissingEnumDoc'
141MISSING_ENUM_ITEM = 'MissingEnumItemDoc'
142MISSING_TYPEDEF = 'MissingTypedefDoc'
143MISSING_VARIABLE = 'MissingVariableDoc'
144MISSING_MACRO = 'MissingMacroDoc'
145
146
147def warn(warning_type, *args):
148    if EMIT_WARNINGS:
149        args = ' '.join(str(arg) for arg in args)
150
151        if warning_type is None:
152            output = f'\033[31;1m    {args}\033[0m\n'
153        else:
154            output = f'\033[31;1m{warning_type}: {args}\033[0m\n'
155
156        sys.stdout.write(output)
157        sys.stdout.flush()
158
159
160def build_docstring(element):
161    docstring = None
162    if element.tag == 'parameterlist':
163        return None
164
165    if element.text:
166        docstring = element.text.strip()
167
168    for item in element:
169        ds = build_docstring(item)
170        if ds:
171            if docstring:
172                docstring += ' ' + ds
173            else:
174                docstring = ds.strip()
175
176    if element.tag == 'para':
177        if docstring:
178            docstring = '\n\n' + docstring
179
180    if element.tag == 'ref':
181        docstring = f':ref:`{docstring}`'
182
183    if element.tail:
184        if docstring:
185            docstring += ' ' + element.tail.strip()
186        else:
187            docstring = element.tail.strip()
188
189    return docstring
190
191
192def read_as_xml(d):
193    try:
194        return ET.fromstring(d)
195    except:  # NOQA
196        return None
197
198
199def load_xml(fle):
200    fle = os.path.join(xml_path, fle + '.xml')
201
202    with open(fle, 'rb') as f:
203        d = f.read().decode('utf-8')
204
205    # This code is to correct a bug in Doxygen. That bug incorrectly parses
206    # a typedef and it causes an error to occur building the docs. The Error
207    # doesn't stop the documentation from being generated, I just don't want
208    # to see the ugly red output.
209    #
210    # if 'typedef void() lv_lru_free_t(void *v)' in d:
211    #     d = d.replace(
212    #         '<type>void()</type>\n        '
213    #         '<definition>typedef void() lv_lru_free_t(void *v)</definition>',
214    #         '<type>void</type>\n        '
215    #         '<definition>typedef void(lv_lru_free_t)(void *v)</definition>'
216    #     )
217    #     with open(fle, 'wb') as f:
218    #         f.write(d.encode('utf-8'))
219
220    return ET.fromstring(d)
221
222
223structures = {}
224functions = {}
225enums = {}
226typedefs = {}
227variables = {}
228unions = {}
229namespaces = {}
230files = {}
231
232
233# things to remove from description
234# <para> </para>
235
236
237class STRUCT_FIELD(object):
238
239    def __init__(self, name, type, description, file_name, line_no):
240        self.name = name
241        self.type = type
242        self.description = description
243        self.file_name = file_name
244        self.line_no = line_no
245
246
247class STRUCT(object):
248    _missing = MISSING_STRUCT
249    _missing_field = MISSING_STRUCT_FIELD
250
251    template = '''\
252.. doxygenstruct:: {name}
253   :project: lvgl
254   :members:
255   :protected-members:
256   :private-members:
257   :undoc-members:
258'''
259
260    def __init__(self, parent, refid, name, **_):
261        if name in structures:
262            self.__dict__.update(structures[name].__dict__)
263        else:
264            structures[name] = self
265            self.parent = parent
266            self.refid = refid
267            self.name = name
268            self.types = set()
269            self._deps = None
270            self.header_file = ''
271            self.description = None
272            self.fields = []
273            self.file_name = None
274            self.line_no = None
275
276        if parent and refid:
277            root = load_xml(refid)
278
279            for compounddef in root:
280                if compounddef.attrib['id'] != self.refid:
281                    continue
282
283                for child in compounddef:
284                    if child.tag == 'includes':
285                        self.header_file = os.path.splitext(child.text)[0]
286                        continue
287
288                    elif child.tag == 'location':
289                        self.file_name = child.attrib['file']
290                        self.line_no = child.attrib['line']
291
292                    elif child.tag == 'detaileddescription':
293                        self.description = build_docstring(child)
294
295                    elif child.tag == 'sectiondef':
296                        for memberdef in child:
297                            t = get_type(memberdef)
298                            description = None
299                            name = ''
300                            file_name = None
301                            line_no = None
302
303                            for element in memberdef:
304                                if element.tag == 'location':
305                                    file_name = element.attrib['file']
306                                    line_no = element.attrib['line']
307
308                                elif element.tag == 'name':
309                                    name = element.text
310
311                                elif element.tag == 'detaileddescription':
312                                    description = build_docstring(element)
313
314                            field = STRUCT_FIELD(name, t, description, file_name, line_no)
315                            self.fields.append(field)
316
317                            if t is None:
318                                continue
319
320                            self.types.add(t)
321
322            if not self.description:
323                warn(self._missing, self.name)
324                warn(None, 'FILE:', self.file_name)
325                warn(None, 'LINE:', self.line_no)
326                warn(None)
327
328            for field in self.fields:
329                if not field.description:
330                    warn(self._missing_field, self.name)
331                    warn(None, 'FIELD:', field.name)
332                    warn(None, 'FILE:', field.file_name)
333                    warn(None, 'LINE:', field.line_no)
334                    warn(None)
335
336    def get_field(self, name):
337        for field in self.fields:
338            if field.name == name:
339                return field
340
341    @property
342    def deps(self):
343        if self._deps is None:
344            self._deps = dict(
345                typedefs=set(),
346                functions=set(),
347                enums=set(),
348                structures=set(),
349                unions=set(),
350                namespaces=set(),
351                variables=set(),
352            )
353            for type_ in self.types:
354                if type_ in typedefs:
355                    self._deps['typedefs'].add(typedefs[type_])
356                elif type_ in structures:
357                    self._deps['structures'].add(structures[type_])
358                elif type_ in unions:
359                    self._deps['unions'].add(unions[type_])
360                elif type_ in enums:
361                    self._deps['enums'].add(enums[type_])
362                elif type_ in functions:
363                    self._deps['functions'].add(functions[type_])
364                elif type_ in variables:
365                    self._deps['variables'].add(variables[type_])
366                elif type_ in namespaces:
367                    self._deps['namespaces'].add(namespaces[type_])
368        return self._deps
369
370    def __str__(self):
371        return self.template.format(name=self.name)
372
373
374class UNION(STRUCT):
375    _missing = MISSING_UNION
376    _missing_field = MISSING_UNION_FIELD
377
378    template = '''\
379.. doxygenunion:: {name}
380   :project: lvgl
381'''
382
383
384def get_type(node):
385    def gt(n):
386        for c in n:
387            if c.tag == 'ref':
388                t = c.text.strip()
389                break
390        else:
391            t = node.text.strip()
392
393        return t.replace('*', '').replace('(', '').replace(')', '').strip()
394
395    for child in node:
396        if child.tag == 'type':
397            return gt(child)
398
399
400class VARIABLE(object):
401    template = '''\
402.. doxygenvariable:: {name}
403   :project: lvgl
404'''
405
406    def __init__(self, parent, refid, name, **_):
407        if name in variables:
408            self.__dict__.update(variables[name].__dict__)
409        else:
410            variables[name] = self
411            self.parent = parent
412            self.refid = refid
413            self.name = name
414            self.description = None
415            self.type = ''
416            self.file_name = None
417            self.line_no = None
418
419        if parent is not None:
420            root = load_xml(parent.refid)
421
422            for compounddef in root:
423                if compounddef.attrib['id'] != parent.refid:
424                    continue
425
426                for child in compounddef:
427                    if (
428                        child.tag == 'sectiondef' and
429                        child.attrib['kind'] == 'var'
430                    ):
431                        for memberdef in child:
432                            if memberdef.attrib['id'] == refid:
433                                break
434                        else:
435                            continue
436
437                        self.type = get_type(memberdef)
438
439                        for element in memberdef:
440                            if element.tag == 'location':
441                                self.file_name = element.attrib['file']
442                                self.line_no = element.attrib['line']
443                            elif element.tag == 'detaileddescription':
444                                self.description = build_docstring(element)
445
446            if not self.description:
447                warn(MISSING_VARIABLE, self.name)
448                warn(None, 'FILE:', self.file_name)
449                warn(None, 'LINE:', self.line_no)
450                warn(None)
451
452    def __str__(self):
453        return self.template.format(name=self.name)
454
455
456class NAMESPACE(object):
457    template = '''\
458.. doxygennamespace:: {name}
459   :project: lvgl
460   :members:
461   :protected-members:
462   :private-members:
463   :undoc-members:
464'''
465
466    def __init__(self, parent, refid, name, **_):
467        if name in namespaces:
468            self.__dict__.update(namespaces[name].__dict__)
469        else:
470            namespaces[name] = self
471            self.parent = parent
472            self.refid = refid
473            self.name = name
474            self.description = None
475            self.line_no = None
476            self.file_name = None
477            self.enums = []
478            self.funcs = []
479            self.vars = []
480            self.typedefs = []
481            self.structs = []
482            self.unions = []
483            self.classes = []
484
485        # root = load_xml(refid)
486        #
487        # for compounddef in root:
488        #     if compounddef.attrib['id'] != refid:
489        #         continue
490        #
491        #     for sectiondef in compounddef:
492        #         if sectiondef.tag != 'sectiondef':
493        #             continue
494        #
495        #         enum
496        #         typedef
497        #         func
498        #         struct
499        #         union
500        #
501        #
502        #         cls = globals()[sectiondef.attrib['kind'].upper()]
503        #         if cls == ENUM:
504        #             if sectiondef[0].text:
505        #                 sectiondef.attrib['name'] = sectiondef[0].text.strip()
506        #                 enums_.append(cls(self, **sectiondef.attrib))
507        #             else:
508        #                 sectiondef.attrib['name'] = None
509        #                 enums_.append(cls(self, **sectiondef.attrib))
510        #
511        #         elif cls == ENUMVALUE:
512        #             if enums_[-1].is_member(sectiondef):
513        #                 enums_[-1].add_member(sectiondef)
514        #
515        #         else:
516        #             sectiondef.attrib['name'] = sectiondef[0].text.strip()
517        #             cls(self, **sectiondef.attrib)
518
519    def __str__(self):
520        return self.template.format(name=self.name)
521
522
523class FUNC_ARG(object):
524
525    def __init__(self, name, type):
526        self.name = name
527        self.type = type
528        self.description = None
529
530
531groups = {}
532
533
534class GROUP(object):
535    template = '''\
536.. doxygengroup:: {name}
537    :project: lvgl
538'''
539
540    def __init__(self, parent, refid, name, **_):
541        if name in groups:
542            self.__dict__.update(functions[name].__dict__)
543        else:
544            functions[name] = self
545            self.parent = parent
546            self.refid = refid
547            self.name = name
548            self.description = None
549
550    def __str__(self):
551        return self.template.format(name=self.name)
552
553
554
555class FUNCTION(object):
556    template = '''\
557.. doxygenfunction:: {name}
558   :project: lvgl
559'''
560
561    def __init__(self, parent, refid, name, **_):
562        if name in functions:
563            self.__dict__.update(functions[name].__dict__)
564        else:
565            functions[name] = self
566            self.parent = parent
567            self.refid = refid
568            self.name = name
569            self.types = set()
570            self.restype = None
571            self.args = []
572            self._deps = None
573            self.description = None
574            self.res_description = None
575            self.file_name = None
576            self.line_no = None
577            self.void_return = False
578
579        if parent is not None:
580            root = load_xml(parent.refid)
581
582            for compounddef in root:
583                if compounddef.attrib['id'] != parent.refid:
584                    continue
585
586                for child in compounddef:
587                    if child.tag != 'sectiondef':
588                        continue
589
590                    if child.attrib['kind'] != 'func':
591                        continue
592
593                    for memberdef in child:
594                        if 'id' not in memberdef.attrib:
595                            continue
596
597                        if memberdef.attrib['id'] == refid:
598                            break
599                    else:
600                        continue
601
602                    break
603                else:
604                    continue
605
606                break
607            else:
608                return
609
610            self.restype = get_type(memberdef)
611
612            for child in memberdef:
613                if child.tag == 'type':
614                    if child.text and child.text.strip() == 'void':
615                        self.void_return = True
616
617                if child.tag == 'param':
618                    t = get_type(child)
619                    if t is not None:
620                        self.types.add(t)
621
622                    for element in child:
623                        if element.tag == 'declname':
624                            arg = FUNC_ARG(element.text, t)
625                            self.args.append(arg)
626
627            for child in memberdef:
628                if child.tag == 'location':
629                    self.file_name = child.attrib['file']
630                    self.line_no = child.attrib['line']
631
632                elif child.tag == 'detaileddescription':
633                    self.description = build_docstring(child)
634                    for element in child:
635                        if element.tag != 'para':
636                            continue
637
638                        for desc_element in element:
639                            if desc_element.tag == 'simplesect' and desc_element.attrib['kind'] == 'return':
640                                self.res_description = build_docstring(desc_element)
641
642                            if desc_element.tag != 'parameterlist':
643                                continue
644
645                            for parameter_item in desc_element:
646                                parameternamelist = parameter_item[0]
647                                if parameternamelist.tag != 'parameternamelist':
648                                    continue
649
650                                parameter_name = parameternamelist[0].text
651
652                                try:
653                                    parameterdescription = parameter_item[1]
654                                    if parameterdescription.tag == 'parameterdescription':
655                                        parameter_description = build_docstring(parameterdescription)
656                                    else:
657                                        parameter_description = None
658                                except IndexError:
659                                    parameter_description = None
660
661                                if parameter_name is not None:
662                                    for arg in self.args:
663                                        if arg.name != parameter_name:
664                                            continue
665
666                                        arg.description = parameter_description
667                                        break
668                                    else:
669                                        warn(MISSING_FUNC_ARG_MISMATCH, self.name)
670                                        warn(None, 'ARG:', parameter_name)
671                                        warn(None, 'FILE:', self.file_name)
672                                        warn(None, 'LINE:', self.line_no)
673                                        warn(None)
674
675            if not self.description:
676                warn(MISSING_FUNC, self.name)
677                warn(None, 'FILE:', self.file_name)
678                warn(None, 'LINE:', self.line_no)
679                warn(None)
680            else:
681                for arg in self.args:
682                    if not arg.description:
683                        warn(MISSING_FUNC_ARG, self.name)
684                        warn(None, 'ARG:', arg.name)
685                        warn(None, 'FILE:', self.file_name)
686                        warn(None, 'LINE:', self.line_no)
687                        warn(None)
688
689                if not self.res_description and not self.void_return:
690                    warn(MISSING_FUNC_RETURN, self.name)
691                    warn(None, 'FILE:', self.file_name)
692                    warn(None, 'LINE:', self.line_no)
693                    warn(None)
694
695        if self.restype in self.types:
696            self.restype = None
697
698    @property
699    def deps(self):
700        if self._deps is None:
701            self._deps = dict(
702                typedefs=set(),
703                functions=set(),
704                enums=set(),
705                structures=set(),
706                unions=set(),
707                namespaces=set(),
708                variables=set(),
709            )
710            if self.restype is not None:
711                self.types.add(self.restype)
712
713            for type_ in self.types:
714                if type_ in typedefs:
715                    self._deps['typedefs'].add(typedefs[type_])
716                elif type_ in structures:
717                    self._deps['structures'].add(structures[type_])
718                elif type_ in unions:
719                    self._deps['unions'].add(unions[type_])
720                elif type_ in enums:
721                    self._deps['enums'].add(enums[type_])
722                elif type_ in functions:
723                    self._deps['functions'].add(functions[type_])
724                elif type_ in variables:
725                    self._deps['variables'].add(variables[type_])
726                elif type_ in namespaces:
727                    self._deps['namespaces'].add(namespaces[type_])
728        return self._deps
729
730    def __str__(self):
731        return self.template.format(name=self.name)
732
733
734class FILE(object):
735
736    def __init__(self, _, refid, name, node, **__):
737        if name in files:
738            self.__dict__.update(files[name].__dict__)
739            return
740
741        files[name] = self
742
743        self.refid = refid
744        self.name = name
745        self.header_file = os.path.splitext(name)[0]
746
747        enums_ = []
748
749        for member in node:
750            if member.tag != 'member':
751                continue
752
753            cls = globals()[member.attrib['kind'].upper()]
754            if cls == ENUM:
755                if member[0].text:
756                    member.attrib['name'] = member[0].text.strip()
757                    enums_.append(cls(self, **member.attrib))
758                else:
759                    member.attrib['name'] = None
760                    enums_.append(cls(self, **member.attrib))
761
762            elif cls == ENUMVALUE:
763                if enums_[-1].is_member(member):
764                    enums_[-1].add_member(member)
765
766            else:
767                member.attrib['name'] = member[0].text.strip()
768                cls(self, **member.attrib)
769
770
771class ENUM(object):
772    template = '''\
773.. doxygenenum:: {name}
774   :project: lvgl
775'''
776
777    def __init__(self, parent, refid, name, **_):
778        if name in enums:
779            self.__dict__.update(enums[name].__dict__)
780        else:
781
782            enums[name] = self
783
784            self.parent = parent
785            self.refid = refid
786            self.name = name
787            self.members = []
788            self.description = None
789            self.file_name = None
790            self.line_no = None
791
792        if parent is not None:
793            root = load_xml(parent.refid)
794
795            for compounddef in root:
796                if compounddef.attrib['id'] != parent.refid:
797                    continue
798
799                for child in compounddef:
800                    if child.tag != 'sectiondef':
801                        continue
802
803                    if child.attrib['kind'] != 'enum':
804                        continue
805
806                    for memberdef in child:
807                        if 'id' not in memberdef.attrib:
808                            continue
809
810                        if memberdef.attrib['id'] == refid:
811                            break
812                    else:
813                        continue
814
815                    break
816                else:
817                    continue
818
819                break
820            else:
821                return
822                # raise RuntimeError(f'not able to locate enum {name} ({refid})')
823
824            for element in memberdef:
825                if element.tag == 'location':
826                    self.file_name = element.attrib['file']
827                    self.line_no = element.attrib['line']
828
829                if element.tag == 'detaileddescription':
830                    self.description = build_docstring(element)
831                elif element.tag == 'enumvalue':
832                    item_name = None
833                    item_description = None
834                    item_file_name = None
835                    item_line_no = None
836
837                    for s_element in element:
838                        if s_element.tag == 'name':
839                            item_name = s_element.text
840                        elif s_element.tag == 'detaileddescription':
841                            item_description = build_docstring(s_element)
842
843                        elif s_element.tag == 'location':
844                            item_file_name = child.attrib['file']
845                            item_line_no = child.attrib['line']
846
847                    if item_name is not None:
848                        for ev in self.members:
849                            if ev.name != item_name:
850                                continue
851                            break
852                        else:
853                            ev = ENUMVALUE(
854                                self,
855                                element.attrib['id'],
856                                item_name
857                            )
858
859                            self.members.append(ev)
860
861                        ev.description = item_description
862
863            if not self.description:
864                warn(MISSING_ENUM, self.name)
865                warn(None, 'FILE:', self.file_name)
866                warn(None, 'LINE:', self.line_no)
867                warn(None)
868
869            for member in self.members:
870                if not member.description:
871                    warn(MISSING_ENUM_ITEM, self.name)
872                    warn(None, 'MEMBER:', member.name)
873                    warn(None, 'FILE:', self.file_name)
874                    warn(None, 'LINE:', self.line_no)
875                    warn(None)
876
877    def is_member(self, member):
878        return (
879            member.attrib['kind'] == 'enumvalue' and
880            member.attrib['refid'].startswith(self.refid)
881        )
882
883    def add_member(self, member):
884        name = member[0].text.strip()
885        for ev in self.members:
886            if ev.name == name:
887                return
888
889        self.members.append(
890            ENUMVALUE(
891                self,
892                member.attrib['refid'],
893                name
894            )
895        )
896
897    def __str__(self):
898        template = [self.template.format(name=self.name)]
899        template.extend(list(str(member) for member in self.members))
900
901        return '\n'.join(template)
902
903
904defines = {}
905
906
907def build_define(element):
908    define = None
909
910    if element.text:
911        define = element.text.strip()
912
913    for item in element:
914        ds = build_define(item)
915        if ds:
916            if define:
917                define += ' ' + ds
918            else:
919                define = ds.strip()
920
921    if element.tail:
922        if define:
923            define += ' ' + element.tail.strip()
924        else:
925            define = element.tail.strip()
926
927    return define
928
929
930class DEFINE(object):
931    template = '''\
932.. doxygendefine:: {name}
933   :project: lvgl
934'''
935
936    def __init__(self, parent, refid, name, **_):
937        if name in defines:
938            self.__dict__.update(defines[name].__dict__)
939        else:
940            defines[name] = self
941
942            self.parent = parent
943            self.refid = refid
944            self.name = name
945            self.description = None
946            self.file_name = None
947            self.line_no = None
948            self.params = None
949            self.initializer = None
950
951        if parent is not None:
952            root = load_xml(parent.refid)
953
954            for compounddef in root:
955                if compounddef.attrib['id'] != parent.refid:
956                    continue
957
958                for child in compounddef:
959                    if child.tag != 'sectiondef':
960                        continue
961
962                    if child.attrib['kind'] != 'define':
963                        continue
964
965                    for memberdef in child:
966                        if memberdef.attrib['id'] == refid:
967                            break
968                    else:
969                        continue
970
971                    break
972                else:
973                    continue
974
975                break
976            else:
977                return
978
979            for element in memberdef:
980                if element.tag == 'location':
981                    self.file_name = element.attrib['file']
982                    self.line_no = element.attrib['line']
983
984                elif element.tag == 'detaileddescription':
985                    self.description = build_docstring(element)
986
987                elif element.tag == 'param':
988                    for child in element:
989                        if child.tag == 'defname':
990                            if self.params is None:
991                                self.params = []
992
993                            if child.text:
994                                self.params.append(child.text)
995
996                elif element.tag == 'initializer':
997                    initializer = build_define(element)
998                    if initializer is None:
999                        self.initializer = ''
1000                    else:
1001                        self.initializer = initializer
1002
1003            if not self.description:
1004                warn(MISSING_MACRO, self.name)
1005                warn(None, 'FILE:', self.file_name)
1006                warn(None, 'LINE:', self.line_no)
1007                warn(None)
1008
1009    def __str__(self):
1010        return self.template.format(name=self.name)
1011
1012
1013class ENUMVALUE(object):
1014    template = '''\
1015.. doxygenenumvalue:: {name}
1016   :project: lvgl
1017'''
1018
1019    def __init__(self, parent, refid, name, **_):
1020        self.parent = parent
1021        self.refid = refid
1022        self.name = name
1023        self.description = None
1024        self.file_name = None
1025        self.line_no = None
1026
1027    def __str__(self):
1028        return self.template.format(name=self.name)
1029
1030
1031class TYPEDEF(object):
1032    template = '''\
1033.. doxygentypedef:: {name}
1034   :project: lvgl
1035'''
1036
1037    def __init__(self, parent, refid, name, **_):
1038        if name in typedefs:
1039            self.__dict__.update(typedefs[name].__dict__)
1040        else:
1041            typedefs[name] = self
1042
1043            self.parent = parent
1044            self.refid = refid
1045            self.name = name
1046            self.type = None
1047            self._deps = None
1048            self.description = None
1049            self.file_name = None
1050            self.line_no = None
1051
1052        if parent is not None:
1053            root = load_xml(parent.refid)
1054
1055            for compounddef in root:
1056                if compounddef.attrib['id'] != parent.refid:
1057                    continue
1058
1059                for child in compounddef:
1060                    if child.tag != 'sectiondef':
1061                        continue
1062                    if child.attrib['kind'] != 'typedef':
1063                        continue
1064
1065                    for memberdef in child:
1066                        if 'id' not in memberdef.attrib:
1067                            continue
1068
1069                        if memberdef.attrib['id'] == refid:
1070                            break
1071                    else:
1072                        continue
1073
1074                    break
1075                else:
1076                    continue
1077
1078                break
1079            else:
1080                return
1081
1082            for element in memberdef:
1083                if element.tag == 'location':
1084                    self.file_name = element.attrib['file']
1085                    self.line_no = element.attrib['line']
1086
1087                if element.tag == 'detaileddescription':
1088                    self.description = build_docstring(element)
1089
1090            if not self.description:
1091                warn(MISSING_TYPEDEF, self.name)
1092                warn(None, 'FILE:', self.file_name)
1093                warn(None, 'LINE:', self.line_no)
1094                warn(None)
1095
1096            self.type = get_type(memberdef)
1097
1098    @property
1099    def deps(self):
1100        if self._deps is None:
1101            self._deps = dict(
1102                typedefs=set(),
1103                functions=set(),
1104                enums=set(),
1105                structures=set(),
1106                unions=set(),
1107                namespaces=set(),
1108                variables=set(),
1109            )
1110            if self.type is not None:
1111                type_ = self.type
1112
1113                if type_ in typedefs:
1114                    self._deps['typedefs'].add(typedefs[type_])
1115                elif type_ in structures:
1116                    self._deps['structures'].add(structures[type_])
1117                elif type_ in unions:
1118                    self._deps['unions'].add(unions[type_])
1119                elif type_ in enums:
1120                    self._deps['enums'].add(enums[type_])
1121                elif type_ in functions:
1122                    self._deps['functions'].add(functions[type_])
1123                elif type_ in variables:
1124                    self._deps['variables'].add(variables[type_])
1125                elif type_ in namespaces:
1126                    self._deps['namespaces'].add(namespaces[type_])
1127
1128        return self._deps
1129
1130    def __str__(self):
1131        return self.template.format(name=self.name)
1132
1133
1134classes = {}
1135
1136
1137class CLASS(object):
1138
1139    def __init__(self, _, refid, name, node, **__):
1140        if name in classes:
1141            self.__dict__.update(classes[name].__dict__)
1142            return
1143
1144        classes[name] = self
1145
1146        self.refid = refid
1147        self.name = name
1148
1149        enums_ = []
1150
1151        for member in node:
1152            if member.tag != 'member':
1153                continue
1154
1155            cls = globals()[member.attrib['kind'].upper()]
1156            if cls == ENUM:
1157                member.attrib['name'] = member[0].text.strip()
1158                enums_.append(cls(self, **member.attrib))
1159            elif cls == ENUMVALUE:
1160                if enums_[-1].is_member(member):
1161                    enums_[-1].add_member(member)
1162
1163            else:
1164                member.attrib['name'] = member[0].text.strip()
1165                cls(self, **member.attrib)
1166
1167
1168lvgl_src_path = ''
1169api_path = ''
1170html_files = {}
1171
1172
1173def iter_src(n, p):
1174    if p:
1175        out_path = os.path.join(api_path, p)
1176    else:
1177        out_path = api_path
1178
1179    index_file = None
1180
1181    if p:
1182        src_path = os.path.join(lvgl_src_path, p)
1183    else:
1184        src_path = lvgl_src_path
1185
1186    folders = []
1187
1188    for file in os.listdir(src_path):
1189        if 'private' in file:
1190            continue
1191
1192        if os.path.isdir(os.path.join(src_path, file)):
1193            folders.append((file, os.path.join(p, file)))
1194            continue
1195
1196        if not file.endswith('.h'):
1197            continue
1198
1199        if not os.path.exists(out_path):
1200            os.makedirs(out_path)
1201
1202        if index_file is None:
1203            index_file = open(os.path.join(out_path, 'index.rst'), 'w')
1204            if n:
1205                index_file.write('=' * len(n))
1206                index_file.write('\n' + n + '\n')
1207                index_file.write('=' * len(n))
1208                index_file.write('\n\n\n')
1209
1210            index_file.write('.. toctree::\n    :maxdepth: 2\n\n')
1211
1212        name = os.path.splitext(file)[0]
1213        index_file.write('    ' + name + '\n')
1214
1215        rst_file = os.path.join(out_path, name + '.rst')
1216        html_file = os.path.join(p, name + '.html')
1217        html_files[name] = html_file
1218
1219        with open(rst_file, 'w') as f:
1220            f.write('.. _{0}_h:'.format(name))
1221            f.write('\n\n')
1222            f.write('=' * len(file))
1223            f.write('\n')
1224            f.write(file)
1225            f.write('\n')
1226            f.write('=' * len(file))
1227            f.write('\n\n\n')
1228
1229            f.write('.. doxygenfile:: ' + file)
1230            f.write('\n')
1231            f.write('    :project: lvgl')
1232            f.write('\n\n')
1233
1234    for name, folder in folders:
1235        if iter_src(name, folder):
1236            if index_file is None:
1237                index_file = open(os.path.join(out_path, 'index.rst'), 'w')
1238
1239                if n:
1240                    index_file.write('=' * len(n))
1241                    index_file.write('\n' + n + '\n')
1242                    index_file.write('=' * len(n))
1243                    index_file.write('\n\n\n')
1244
1245                index_file.write('.. toctree::\n    :maxdepth: 2\n\n')
1246
1247            index_file.write('    ' + os.path.split(folder)[-1] + '/index\n')
1248
1249    if index_file is not None:
1250        index_file.write('\n')
1251        index_file.close()
1252        return True
1253
1254    return False
1255
1256
1257def clean_name(nme):
1258    # Handle error:
1259    #     AttributeError: 'NoneType' object has no attribute 'startswith'
1260    if nme is None:
1261        return nme
1262
1263    if nme.startswith('_lv_'):
1264        nme = nme[4:]
1265    elif nme.startswith('lv_'):
1266        nme = nme[3:]
1267
1268    if nme.endswith('_t'):
1269        nme = nme[:-2]
1270
1271    return nme
1272
1273
1274# Definitions:
1275# - "section" => The name "abc_def" has 2 sections.
1276# - N = number of sections in `item_name`.
1277# After removing leading '_lv_', 'lv_' and trailing '_t' from `obj_name`,
1278# do the remaining first N "sections" of `obj_name` match `item_name`
1279# (case sensitive)?
1280def is_name_match(item_name, obj_name):
1281    # Handle error:
1282    #     AttributeError: 'NoneType' object has no attribute 'split'
1283    if obj_name is None:
1284        return False
1285
1286    u_num = item_name.count('_') + 1
1287
1288    obj_name = obj_name.split('_')
1289
1290    # Reject (False) if `obj_name` doesn't have as many sections as `item_name`.
1291    if len(obj_name) < u_num:
1292        return False
1293
1294    obj_name = '_'.join(obj_name[:u_num])
1295
1296    return item_name == obj_name
1297
1298
1299def get_includes(name1, name2, obj, includes):
1300    name2 = clean_name(name2)
1301
1302    if not is_name_match(name1, name2):
1303        return
1304
1305    if obj.parent is not None and hasattr(obj.parent, 'header_file'):
1306        header_file = obj.parent.header_file
1307    elif hasattr(obj, 'header_file'):
1308        header_file = obj.header_file
1309    else:
1310        return
1311
1312    if not header_file:
1313        return
1314
1315    if header_file not in html_files:
1316        return
1317
1318    includes.add((header_file, html_files[header_file]))
1319
1320
1321class XMLSearch(object):
1322
1323    def __init__(self, temp_directory):
1324        global xml_path
1325        import subprocess
1326        import re
1327        import sys
1328
1329        bp = os.path.abspath(os.path.dirname(__file__))
1330
1331        lvgl_path = os.path.join(temp_directory, 'lvgl')
1332        src_path = os.path.join(lvgl_path, 'src')
1333
1334        doxy_path = os.path.join(bp, 'Doxyfile')
1335
1336        with open(doxy_path, 'rb') as f:
1337            data = f.read().decode('utf-8')
1338
1339        data = data.replace(
1340            '#*#*LV_CONF_PATH*#*#',
1341            os.path.join(temp_directory, 'lv_conf.h')
1342        )
1343        data = data.replace('*#*#SRC#*#*', '"{0}"'.format(src_path))
1344
1345        with open(os.path.join(temp_directory, 'Doxyfile'), 'wb') as f:
1346            f.write(data.encode('utf-8'))
1347
1348        # -----------------------------------------------------------------
1349        # Populate LVGL_URLPATH and LVGL_GITCOMMIT environment variables:
1350        #   - LVGL_URLPATH   <= 'master' or '8.4' '9.2' etc.
1351        #   - LVGL_GITCOMMIT <= commit hash of HEAD.
1352        # The previous version of this was populating LVGL_URLPATH with
1353        # the multi-line list of all existing branches in the repository,
1354        # which was not what was intended.
1355        # -----------------------------------------------------------------
1356        status, branch = subprocess.getstatusoutput("git branch --show-current")
1357        _, gitcommit = subprocess.getstatusoutput("git rev-parse HEAD")
1358
1359        # If above failed (i.e. `branch` not valid), default to 'master'.
1360        if status != 0:
1361            branch = 'master'
1362        elif branch == 'master':
1363            # Expected in most cases.  Nothing to change.
1364            pass
1365        else:
1366            # `branch` is valid.  Capture release version if in a 'release/' branch.
1367            if branch.startswith('release/'):
1368                branch = branch[8:]
1369            else:
1370                # Default to 'master'.
1371                branch = 'master'
1372
1373        os.environ['LVGL_URLPATH'] = branch
1374        os.environ['LVGL_GITCOMMIT'] = gitcommit
1375
1376        # ---------------------------------------------------------------------
1377        # Provide a way to run an external command and abort build on error.
1378        #
1379        # This is necessary because when tempdir created by tempfile.mkdtemp()`
1380        # is on a different drive, the "cd tmpdir && doxygen Doxyfile" syntax
1381        # fails because of the different semantics of the `cd` command on
1382        # Windows:  it doesn't change the default DRIVE if `cd` is executed
1383        # from a different drive.  The result, when this is the case, is that
1384        # Doxygen runs in the current working directory instead of in the
1385        # temporary directory as was intended.
1386        # ---------------------------------------------------------------------
1387        def cmd(cmd_str, start_dir=None):
1388            if start_dir is None:
1389                start_dir = os.getcwd()
1390
1391            saved_dir = os.getcwd()
1392            os.chdir(start_dir)
1393
1394            # This method of running Doxygen is used because if it
1395            # succeeds, we do not want anything going to STDOUT.
1396            # Running it via `os.system()` would send its output
1397            # to STDOUT.
1398            p = subprocess.Popen(
1399                cmd_str,
1400                stdout=subprocess.PIPE,
1401                stderr=subprocess.PIPE,
1402                shell=True
1403            )
1404
1405            out, err = p.communicate()
1406            if p.returncode:
1407                if out:
1408                    # Note the `.decode("utf-8")` is required here
1409                    # because `sys.stdout.write()` requires a string,
1410                    # and `out` by itself is a byte array -- it causes
1411                    # it to generate an exception and abort the script.
1412                    sys.stdout.write(out.decode("utf-8"))
1413                    sys.stdout.flush()
1414                if err:
1415                    sys.stderr.write(err.decode("utf-8"))
1416                    sys.stdout.flush()
1417
1418                sys.exit(p.returncode)
1419
1420            # If execution arrived here, Doxygen exited with code 0.
1421            os.chdir(saved_dir)
1422
1423        # -----------------------------------------------------------------
1424        # Run Doxygen in temporary directory.
1425        # -----------------------------------------------------------------
1426        cmd('doxygen Doxyfile', temp_directory)
1427
1428        xml_path = os.path.join(temp_directory, 'xml')
1429
1430        self.index = load_xml('index')
1431
1432        for compound in self.index:
1433            compound.attrib['name'] = compound[0].text.strip()
1434            if compound.attrib['kind'] in ('example', 'page', 'dir'):
1435                continue
1436
1437            globals()[compound.attrib['kind'].upper()](
1438                None,
1439                node=compound,
1440                **compound.attrib
1441            )
1442
1443    def get_macros(self):
1444        return list(defines.values())
1445
1446    def get_enum_item(self, e_name):
1447        for enum, obj in enums.items():
1448            for enum_item in obj.members:
1449                if enum_item.name == e_name:
1450                    return enum_item
1451
1452    def get_enum(self, e_name):
1453        return enums.get(e_name, None)
1454
1455    def get_function(self, f_name):
1456        return functions.get(f_name, None)
1457
1458    def get_variable(self, v_name):
1459        return variables.get(v_name, None)
1460
1461    def get_union(self, u_name):
1462        return unions.get(u_name, None)
1463
1464    def get_structure(self, s_name):
1465        return structures.get(s_name, None)
1466
1467    def get_typedef(self, t_name):
1468        return typedefs.get(t_name, None)
1469
1470    def get_macro(self, m_name):
1471        return defines.get(m_name, None)
1472
1473
1474def announce(*args):
1475    args = ' '.join(repr(arg) for arg in args)
1476    print(f'{os.path.basename(__file__)}: ', args)
1477
1478
1479def run(project_path, temp_directory, *doc_paths):
1480    """
1481    This function does 2 things:
1482    1.  Generates .RST files for the LVGL header files that will have API
1483        pages generated for them.  It places these in <tmp_dir>/API/...
1484        following the <project_path>/src/ directory structure.
1485    2.  Add Sphinx hyperlinks to the end of source .RST files found
1486        in the `doc_paths` array directories, whose file-name stems
1487        match code-element names found by Doxygen.
1488
1489    :param project_path:  platform-appropriate path to LVGL root directory
1490    :param temp_directory:  platform-appropriate path to temp dir being operated on
1491    :param doc_paths:  list of platform-appropriate paths to find source .RST files.
1492    :return:  n/a
1493    """
1494    global base_path
1495    global xml_path
1496    global lvgl_src_path
1497    global api_path
1498
1499    base_path = temp_directory
1500    xml_path = os.path.join(base_path, 'xml')
1501    api_path = os.path.join(base_path, 'API')
1502    lvgl_src_path = os.path.join(project_path, 'src')
1503
1504    announce("Generating API documentation .RST files...")
1505
1506    if not os.path.exists(api_path):
1507        os.makedirs(api_path)
1508
1509    # Generate .RST files for API pages.
1510    iter_src('API', '')
1511    # Load index.xml -- core of what was generated by Doxygen.
1512    index = load_xml('index')
1513
1514    # Populate these dictionaries.
1515    #     Keys  :  C-code-element names (str) found by Doxygen.
1516    #     Values:  The <compound> XML-node created by `xml.etree::ElementTree` in `load_xml()` above.
1517    #
1518    #    - defines,
1519    #    - enums,
1520    #    - variables,
1521    #    - namespaces,
1522    #    - structures,
1523    #    - unions,
1524    #    - typedefs,
1525    #    - functions.
1526    announce("Building source-code symbol tables...")
1527
1528    for compound in index:
1529        compound.attrib['name'] = compound[0].text.strip()
1530        if compound.attrib['kind'] in ('example', 'page', 'dir'):
1531            continue
1532
1533        # This below highly-compressed command effectively does this:
1534        #
1535        #     namespace_dict = globals()
1536        #     compound_elem_kind_upper = compound.attrib['kind'].upper()
1537        #         e.g. 'FUNCTION'
1538        #     class_obj = namespace_dict['FUNCTION']
1539        #         # In each case of `class_obj`, the __init__ args are:
1540        #         #     (self, parent, refid, name, **_)
1541        #         # So we get...
1542        #     attrib_keyword_args = **compound.attrib
1543        #         # Passing (**compound.attrib) as an argument creates and
1544        #         # passes a set of keyword arguments produced from the
1545        #         # dictionary `compound.attrib`.
1546        #     new_obj = class_obj(None, node=compound, attrib_keyword_args)
1547        #
1548        # Note carefully that `new_obj` gets thrown away, but the new object created
1549        # doesn't go away because during its execution of __init__(), the new object
1550        # adds itself to the global dictionary matching its "kind":
1551        #
1552        # Class          Dictionary New Object Adds Itself To
1553        # ------------ | ------------------------------------
1554        # - DEFINE    => `defines`
1555        # - ENUM      => `enums`
1556        # - VARIABLE  => `variables`
1557        # - NAMESPACE => `namespaces`
1558        # - STRUCT    => `structures`
1559        # - UNION    appears to have a different purpose
1560        # - TYPEDEF   => `typedefs`
1561        # - FUNCTION  => `functions`
1562        # - GROUP     => `groups`
1563        # - FILE      => `files`
1564        # - CLASS     => `classes`
1565        #
1566        # Populating these dictionaries takes quite a while:
1567        # ~18-seconds on a medium-speed system.
1568        globals()[compound.attrib['kind'].upper()](
1569            None,
1570            node=compound,
1571            **compound.attrib
1572        )
1573
1574    # For each directory entry in `doc_paths` array...
1575    announce("Adding API-page hyperlinks to source docs...")
1576
1577    for folder in doc_paths:
1578        # Fetch a list of '.rst' files excluding 'index.rst'.
1579        rst_files = list(
1580            (os.path.splitext(item)[0], os.path.join(folder, item))
1581            for item in os.listdir(folder)
1582            if item.endswith('.rst') and 'index.rst' not in item
1583        )
1584
1585        # For each .RST file in that directory...
1586        for stem, path in rst_files:
1587            # Start with an empty set.
1588            html_includes = set()
1589
1590            # Build `html_includes` set as a list of tuples containing
1591            # (name, html_file).  Example:  "draw.rst" has `stem` == 'draw',
1592            # and generates a list of tuples from .H files where matching
1593            # C-code-element names were found.  Example:
1594            # {('lv_draw_line', 'draw\\lv_draw_line.html'),
1595            #  ('lv_draw_sdl', 'draw\\sdl\\lv_draw_sdl.html'),
1596            #  ('lv_draw_sw_blend_to_i1', 'draw\\sw\\blend\\lv_draw_sw_blend_to_i1.html'),
1597            #  etc.}
1598            for container in (
1599                defines,
1600                enums,
1601                variables,
1602                namespaces,
1603                structures,
1604                unions,
1605                typedefs,
1606                functions
1607            ):
1608                for n, o in container.items():
1609                    get_includes(stem, n, o, html_includes)
1610
1611            if html_includes:
1612                # Convert `html_includes` set to a list of strings containing the
1613                # Sphinx hyperlink syntax "link references".  Example from above:
1614                # [':ref:`lv_draw_line_h`\n',
1615                #  ':ref:`lv_draw_sdl_h`\n',
1616                #  ':ref:`lv_draw_sw_blend_to_i1_h`\n',
1617                #  etc.]
1618                html_includes = list(
1619                    ':ref:`{0}_h`\n'.format(inc)
1620                    for inc, _ in html_includes
1621                )
1622
1623                # Convert that list to a single string of Sphinx hyperlink
1624                # references with blank lines between them.
1625                # :ref:`lv_draw_line_h`
1626                #
1627                # :ref:`lv_draw_sdl_h`
1628                #
1629                # :ref:`lv_draw_sw_blend_to_i1_h`
1630                #
1631                # etc.
1632                output = ('\n'.join(html_includes)) + '\n'
1633
1634                # Append that string to the  source .RST file being processed.
1635                with open(path, 'rb') as f:
1636                    try:
1637                        data = f.read().decode('utf-8')
1638                    except UnicodeDecodeError:
1639                        print(path)
1640                        raise
1641
1642                data = data.split('.. Autogenerated', 1)[0]
1643
1644                data += '.. Autogenerated\n\n'
1645                data += output
1646
1647                with open(path, 'wb') as f:
1648                    f.write(data.encode('utf-8'))
1649