File indexing completed on 2024-04-21 05:41:05

0001 import logging
0002 import re
0003 import typing
0004 
0005 TYPE_RX = r"(?P<prefix>\s+type:)(?P<type>[\w\*.<>|]+)"
0006 
0007 BASE_NAME_DICT = {
0008     "QtObject": "QtQml.QtObject",
0009     "Item": "QtQuick.Item",
0010 }
0011 
0012 
0013 def post_process_type(rx, text, type):
0014     match = rx.search(text)
0015     if match:
0016         type = match.group("type")
0017         text = text[:match.start("prefix")] + text[match.end("type"):]
0018     return text, type
0019 
0020 def is_cxx_comment(text):
0021     if not isinstance(text, str):
0022         text = str(text)
0023     return text.startswith("//")
0024 
0025 
0026 class QmlBaseComponent():
0027     def __init__(self, name, version = None, should_separate_blocks = True):
0028         self.name = name
0029         self.base_name = ""
0030         self.elements = []
0031         self.should_separate_blocks = should_separate_blocks
0032 
0033         lst = name.split(".")
0034         self.class_name = lst[-1]
0035         self.namespaces = lst[:-1]
0036 
0037     def get_attributes(self):
0038         return [x for x in self.elements if isinstance(x, QmlAttribute)]
0039 
0040     def get_properties(self):
0041         return [x for x in self.elements if isinstance(x, QmlProperty)]
0042 
0043     def get_functions(self):
0044         return [x for x in self.elements if isinstance(x, QmlFunction)]
0045 
0046     def get_signals(self):
0047         return [x for x in self.elements if isinstance(x, QmlSignal)]
0048 
0049     def add_element(self, element):
0050         self.elements.append(element)
0051 
0052     def starts_with_cxx_comment(self):
0053         if not hasattr(self, "doc_is_inline") or not hasattr(self, "doc"):
0054             return False
0055         if self.doc_is_inline:
0056             return False
0057         return self.doc.startswith("//")
0058 
0059     def __str__(self):
0060         lst = []
0061         self._export_content(lst)
0062         return "\n".join(lst)
0063 
0064     def _export_element(self, element, lst):
0065         doc = str(element)
0066         if doc:
0067             lst.append(doc)
0068 
0069     def _export_elements(self, input_list, lst, filter=None):
0070         for element in input_list:
0071             if filter and not filter(element):
0072                 continue
0073             self._export_element(element, lst)
0074 
0075     def _export_element_w_access(self, element, lst, is_public,
0076             last_was_public, last_was_cxx_comment):
0077         if is_public != last_was_public:
0078             if is_public:
0079                 lst.append("public:")
0080             else:
0081                 lst.append("private:")
0082         elif last_was_cxx_comment and is_cxx_comment(element) and self.should_separate_blocks:
0083             lst.append("")
0084         self._export_element(element, lst)
0085 
0086     def _start_class(self, lst):
0087         class_decl = "class " + self.class_name
0088         if self.base_name:
0089             for alias, replacement in self.alias.items():
0090                 self.base_name = re.sub(alias, replacement, self.base_name)
0091             self.base_name = BASE_NAME_DICT.get(self.base_name, self.base_name)
0092 
0093             class_decl += " : public " + self.base_name
0094 
0095         class_decl += " {"
0096         lst.append(class_decl)
0097 
0098     def _end_class(self, lst):
0099         lst.append("};")
0100 
0101 
0102 class QmlClass(QmlBaseComponent):
0103     SINGLETON_COMMENT = "/** @remark This component is a singleton */"
0104     VERSION_COMMENT = "/** @version %s */"
0105     IMPORT_STATEMENT_COMMENT = "/** {} <br><b>Import Statement</b> \\n @code import {} @endcode */"
0106 
0107     def __init__(self, name, version=None, modulename=None, should_separate_blocks = True):
0108         QmlBaseComponent.__init__(self, name, version, should_separate_blocks)
0109         self.header_comments = []
0110         self.footer_comments = []
0111         self.imports = []
0112         self.alias = {}
0113         self.modulename = modulename
0114         self.version = version
0115 
0116     def add_pragma(self, decl):
0117         args = decl.split(' ', 2)[1].strip()
0118 
0119         if args.lower() == "singleton":
0120             self.header_comments.append(QmlClass.SINGLETON_COMMENT)
0121 
0122     def add_import(self, decl):
0123         modules = decl.split()
0124         module = modules[1]
0125         if module[0] == '"':
0126             # Ignore directory or javascript imports for now
0127             return
0128         if "as" in modules:
0129             self.alias[modules[modules.index("as")+1]] = modules[1]
0130         self.imports.append(module)
0131 
0132     def add_header_comment(self, obj):
0133         self.header_comments.append(obj)
0134 
0135     def add_footer_comment(self, obj):
0136         self.footer_comments.append(obj)
0137 
0138     def _export_header(self, lst):
0139         for module in self.imports:
0140             lst.append("using namespace %s;" % module.replace('.', '::'))
0141         if self.namespaces:
0142             lst.append("namespace %s {" % '::'.join(self.namespaces))
0143 
0144         lst.extend([str(x) for x in self.header_comments])
0145 
0146         if self.modulename:
0147             # we want to insert a newline if there has been any comments (usually a description of the class)
0148             newline = ""
0149 
0150             # is there any comments before this one?
0151             any_comments = len(self.header_comments) > 0
0152 
0153             # and if there is any comments, is the last one a SPDX message? they don't render, so don't insert a
0154             # useless newline
0155             is_spdx_last = any_comments and ("SPDX" not in self.header_comments[len(self.header_comments) - 1])
0156 
0157             if any_comments and is_spdx_last:
0158                 newline = "\\n"
0159 
0160             lst.append(QmlClass.IMPORT_STATEMENT_COMMENT.format(newline, self.modulename))
0161         if self.version:
0162             lst.append(QmlClass.VERSION_COMMENT % self.version)
0163 
0164 
0165     def _export_footer(self, lst):
0166         lst.extend([str(x) for x in self.footer_comments])
0167 
0168         if self.namespaces:
0169             lst.append("}")
0170 
0171     def _export_content(self, lst):
0172         self._export_header(lst)
0173 
0174         # Public members.
0175         self._start_class(lst)
0176 
0177         last_element_was_public = False
0178         last_element_was_cxx_comment = False
0179         for element in self.elements:
0180             if str(element) == "" or isinstance(element, str):
0181                 self._export_element_w_access(element, lst,
0182                         last_element_was_public, last_element_was_public,
0183                         last_element_was_cxx_comment)
0184                 last_element_was_cxx_comment = is_cxx_comment(element)
0185             elif element.is_public_element():
0186                 self._export_element_w_access(element, lst, True,
0187                         last_element_was_public, last_element_was_cxx_comment)
0188                 last_element_was_public = True
0189                 last_element_was_cxx_comment = False
0190             else:
0191                 self._export_element_w_access(element, lst, False,
0192                         last_element_was_public, last_element_was_cxx_comment)
0193                 last_element_was_public = False
0194                 last_element_was_cxx_comment = False
0195 
0196         self._end_class(lst)
0197         self._export_footer(lst)
0198 
0199     def is_public_element(self):
0200         return True
0201 
0202 
0203 class QmlComponent(QmlBaseComponent):
0204     """A component inside a QmlClass"""
0205 
0206     def __init__(self, name):
0207         QmlBaseComponent.__init__(self, name)
0208         self.comment = None
0209 
0210     def _export_content(self, lst):
0211         component_id = self.get_component_id()
0212         if component_id:
0213             if self.comment:
0214                 lst.append(self.comment)
0215 
0216             lst.append("%s %s;" % (self.class_name, component_id))
0217 
0218         # Export child components with the top-level component. This avoids
0219         # very deep nesting in the generated documentation.
0220         self._export_elements(self.elements, lst, filter=lambda x:
0221                               isinstance(x, QmlComponent))
0222 
0223     def get_component_id(self):
0224         # Returns the id of the component, if it has one
0225         for attr in self.get_attributes():
0226             if attr.name == "id":
0227                 return attr.value
0228         return None
0229 
0230     def is_public_element(self):
0231         return False
0232 
0233 
0234 class QmlArgument(object):
0235     def __init__(self, name):
0236         self.type = ""
0237         self.name = name
0238         self.default_value = None
0239         self.spread = False
0240 
0241     def __str__(self):
0242         if self.spread:
0243             return '.../*{}*/'.format(self.name)
0244         elif self.type == "":
0245             return self.name + self.default_value_string()
0246         else:
0247             return self.type + " " + self.name + self.default_value_string()
0248 
0249     def default_value_string(self):
0250         if self.default_value is None:
0251             return ''
0252         else:
0253             return ' = {}'.format(self.default_value)
0254 
0255     def is_public_element(self):
0256         return True
0257 
0258 
0259 class QmlAttribute(object):
0260     def __init__(self):
0261         self.name = ""
0262         self.value = ""
0263         self.type = "var"
0264         self.doc = ""
0265 
0266     def __str__(self):
0267         if self.name != "id":
0268             lst = []
0269             if len(self.doc) > 0:
0270                 lst.append(self.doc)
0271             lst.append(self.type + " " + self.name + ";")
0272             return "\n".join(lst)
0273         else:
0274             return ""
0275 
0276     def is_public_element(self):
0277         return False
0278 
0279 
0280 class QmlProperty(object):
0281     type_rx = re.compile(TYPE_RX)
0282 
0283     DEFAULT_PROPERTY_COMMENT = "/** @remark This is the default property */"
0284     READONLY_PROPERTY_COMMENT = "/** @remark This property is read-only */"
0285 
0286     def __init__(self):
0287         self.type = ""
0288         self.is_default = False
0289         self.is_readonly = False
0290         self.name = ""
0291         self.doc = ""
0292         self.doc_is_inline = False
0293 
0294     def __str__(self):
0295         self.post_process_doc()
0296         lst = []
0297         if not self.doc_is_inline:
0298             lst.append(self.doc + "\n")
0299         if self.is_default:
0300             lst.append(self.DEFAULT_PROPERTY_COMMENT + "\n")
0301         elif self.is_readonly:
0302             lst.append(self.READONLY_PROPERTY_COMMENT + "\n")
0303         lst.append("Q_PROPERTY(%s %s READ dummyGetter_%s_ignore)"
0304             % (self.type, self.name, self.name))
0305         if self.doc_is_inline:
0306             lst.append(" " + self.doc)
0307         return "".join(lst)
0308 
0309     def post_process_doc(self):
0310         self.doc, self.type = post_process_type(self.type_rx, self.doc, self.type)
0311 
0312     def is_public_element(self):
0313         # Doxygen always adds Q_PROPERTY items as public members.
0314         return True
0315 
0316 
0317 class QmlFunction(object):
0318     doc_arg_rx = re.compile(r"[@\\]param" + TYPE_RX + r"\s+(?P<name>\w+)")
0319     return_rx = re.compile(r"[@\\]returns?" + TYPE_RX)
0320 
0321     def __init__(self):
0322         self.type = "void"
0323         self.name = ""
0324         self.doc = ""
0325         self.doc_is_inline = False
0326         self.args = []
0327 
0328     def __str__(self):
0329         self.post_process_doc()
0330         arg_string = ", ".join([str(x) for x in self.args])
0331         lst = []
0332         if not self.doc_is_inline:
0333             lst.append(self.doc + "\n")
0334         lst.append("%s %s(%s);" % (self.type, self.name, arg_string))
0335         if self.doc_is_inline:
0336             lst.append(" " + self.doc)
0337         return "".join(lst)
0338 
0339     def post_process_doc(self):
0340         def repl(match):
0341             # For each argument with a specified type, update arg.type and return a typeless @param line
0342             type = match.group("type")
0343             name = match.group("name")
0344             for arg in self.args:
0345                 if arg.name == name:
0346                     arg.type = type
0347                     break
0348             else:
0349                 logging.warning("In function %s(): Unknown argument %s" % (self.name, name))
0350             return "@param %s" % name
0351 
0352         self.doc = self.doc_arg_rx.sub(repl, self.doc)
0353         self.doc, self.type = post_process_type(self.return_rx, self.doc, self.type)
0354 
0355     def is_public_element(self):
0356         return True
0357 
0358 
0359 class QmlEnum(object):
0360     def __init__(self):
0361         self.name = ""
0362         self.doc = ""
0363         self.doc_is_inline = False
0364         self.enumerators = []
0365 
0366     def __str__(self):
0367         lst = []
0368 
0369         if self.doc and not self.doc_is_inline:
0370             lst.append(self.doc + "\n")
0371         lst.append("enum class %s {" % (self.name))
0372         if self.doc and self.doc_is_inline:
0373             lst.append(" " + self.doc)
0374         lst.append("\n")
0375 
0376         for e in self.enumerators:
0377             lst.append(str(e) + "\n")
0378 
0379         lst.append("};")
0380         return "".join(lst)
0381 
0382     def is_public_element(self):
0383         return True
0384 
0385 
0386 class QmlEnumerator(object):
0387     def __init__(self, name):
0388         self.name = name
0389         self.initializer = ""
0390         self.is_last = False
0391         self.doc = ""
0392         self.doc_is_inline = False
0393 
0394     def __str__(self):
0395         lst = []
0396         if self.doc and not self.doc_is_inline:
0397             lst.append(self.doc + "\n")
0398         lst.append("%s" % (self.name))
0399         if self.initializer:
0400             lst.append(" = %s" % (self.initializer))
0401         if not self.is_last:
0402             lst.append(",")
0403         if self.doc and self.doc_is_inline:
0404             lst.append(" " + self.doc)
0405         return "".join(lst)
0406 
0407     def is_public_element(self):
0408         return True
0409 
0410 
0411 class QmlSignal(object):
0412     def __init__(self):
0413         self.name = ""
0414         self.doc = ""
0415         self.doc_is_inline = False
0416         self.args = []
0417 
0418     def __str__(self):
0419         arg_string = ", ".join([str(x) for x in self.args])
0420         lst = []
0421         if not self.doc_is_inline:
0422             lst.append(self.doc + "\n")
0423         lst.append("Q_SIGNALS: void %s(%s); " % (self.name, arg_string))
0424         if self.doc_is_inline:
0425             lst.append(self.doc + "\n")
0426         # Appending "public:" here makes it possible to declare a signal without
0427         # turning all functions defined after into signals.
0428         # It could be replaced with the use of Q_SIGNAL, but my version of
0429         # Doxygen (1.8.4) does not support it
0430         lst.append("public:")
0431         return "".join(lst)
0432 
0433     def is_public_element(self):
0434         # Doxygen always adds Q_SIGNALS items as public members.
0435         return True