File indexing completed on 2024-04-14 05:34:18

0001 #!/usr/bin/env python3
0002 
0003 import argparse
0004 import codecs
0005 import logging
0006 import os
0007 import re
0008 import sys
0009 
0010 import doxyqml.qmlparser as qmlparser
0011 
0012 from doxyqml import __version__, DESCRIPTION
0013 from doxyqml.lexer import Lexer, LexerError
0014 from doxyqml.qmlclass import QmlClass
0015 
0016 
0017 def coord_for_idx(text, idx):
0018     head, sep, tail = text[:idx].rpartition("\n")
0019     if sep == "\n":
0020         row = head.count("\n") + 2
0021     else:
0022         row = 1
0023     col = len(tail) + 1
0024     return row, col
0025 
0026 
0027 def line_for_idx(text, idx):
0028     bol = text.rfind("\n", 0, idx)
0029     if bol == -1:
0030         bol = 0
0031     eol = text.find("\n", idx)
0032     return text[bol:eol]
0033 
0034 
0035 def info_for_error_at(text, idx):
0036     row, col = coord_for_idx(text, idx)
0037     line = line_for_idx(text, idx)
0038     msg = line + "\n" + "-" * (col - 1) + "^"
0039     return row, msg
0040 
0041 
0042 def parse_args(argv):
0043     parser = argparse.ArgumentParser(
0044         prog="doxyqml",
0045         description=DESCRIPTION,
0046         )
0047     parser.add_argument("-d", "--debug",
0048                         action="store_true",
0049                         help="Log debug info to stderr")
0050     parser.add_argument("--namespace",
0051                         action='append',
0052                         default=[],
0053                         help="Wrap the generated C++ classes in NAMESPACE")
0054     parser.add_argument("--no-since-version",
0055                         action="store_true",
0056                         default=False,
0057                         help="Don't append \"Since: [version]\" info to docstring")
0058     parser.add_argument('--no-nested-components',
0059                         action='store_true',
0060                         default=False,
0061                         help="Don't create private member documentation for nested components")
0062     parser.add_argument('--version',
0063                         action='version',
0064                         version='%%(prog)s %s' % __version__)
0065     parser.add_argument("qml_file",
0066                         help="The QML file to parse")
0067 
0068     return parser.parse_args(argv)
0069 
0070 
0071 def find_qmldir_file(qml_file):
0072     dir = os.path.dirname(qml_file)
0073 
0074     while True:
0075         # Check if `dir` contains a file of the name "qmldir".
0076         name = os.path.join(dir, 'qmldir')
0077 
0078         if os.path.isfile(name):
0079             return name
0080 
0081         # Pick parent of `dir`. Abort once parent stops changing,
0082         # either because we reached the root directory, or because
0083         # relative paths were used and we reached the currrent
0084         # working directory.
0085         parent = os.path.dirname(dir)
0086 
0087         if parent == dir:
0088             return None
0089 
0090         dir = parent
0091 
0092 
0093 def find_classname(qml_file, namespace=None):
0094     classname = os.path.basename(qml_file).split(".")[0]
0095     classversion = None
0096     modulename = ''
0097 
0098     qmldir = find_qmldir_file(qml_file)
0099 
0100     if qmldir:
0101         text = open(qmldir).read()
0102         match = re.match(r'^module\s+((?:\w|\.)+)\s*$', text, re.MULTILINE)
0103 
0104         if match:
0105             modulename = match.group(1)
0106 
0107         basedir = os.path.dirname(qmldir)
0108 
0109         rx_object_type = re.compile(r'^(\w+)\s+(\d+(?:\.\d+)*)\s+(\S+)\s*$', re.MULTILINE)
0110 
0111         # skip internal classes
0112         if "internal" in text:
0113             return None, None, None
0114 
0115         for name, version, path in rx_object_type.findall(text):
0116             filename = os.path.join(basedir, path)
0117 
0118             if os.path.isfile(filename) and os.path.samefile(qml_file, filename):
0119                 classversion = version
0120                 classname = name
0121                 break
0122 
0123     if modulename:
0124         classname = modulename + '.' + classname
0125 
0126     if namespace:
0127         classname = '.'.join(namespace) + '.' + classname
0128 
0129     return classname, classversion, modulename
0130 
0131 
0132 def main(argv=None, out=None):
0133     if argv is None:
0134         argv = sys.argv[1:]
0135     if out is None:
0136         out = sys.stdout
0137 
0138     args = parse_args(argv)
0139 
0140     name = args.qml_file
0141     namespace = args.namespace
0142 
0143     encoding = "utf-8"
0144     first_4_bytes = open(name, 'rb').read(4)
0145     if (first_4_bytes.startswith(codecs.BOM_UTF8)):
0146         encoding = "utf-8-sig"
0147     text = open(name, encoding=encoding).read()
0148 
0149     lexer = Lexer(text)
0150     try:
0151         lexer.tokenize()
0152     except LexerError as exc:
0153         logging.error("Failed to tokenize %s" % name)
0154         row, msg = info_for_error_at(text, exc.idx)
0155         logging.error("Lexer error line %d: %s\n%s", row, exc, msg)
0156         if args.debug:
0157             raise
0158         else:
0159             return -1
0160 
0161     if args.debug:
0162         for token in lexer.tokens:
0163             print("%20s %s" % (token.type, token.value))
0164 
0165     classname, classversion, modulename = find_classname(name, namespace)
0166     if args.no_since_version:
0167         classversion = None
0168 
0169     if classname is None:
0170         return
0171 
0172     qml_class = QmlClass(classname, classversion, modulename, not args.no_nested_components)
0173 
0174     try:
0175         qmlparser.parse(lexer.tokens, qml_class, not args.no_nested_components)
0176     except qmlparser.QmlParserError as exc:
0177         logging.error("Failed to parse %s" % name)
0178         row, msg = info_for_error_at(text, exc.token.idx)
0179         logging.error("Lexer error line %d: %s\n%s", row, exc, msg)
0180         if args.debug:
0181             raise
0182         else:
0183             return -1
0184 
0185     out = codecs.getwriter("utf-8")(out.buffer)
0186     print(qml_class, file=out)
0187 
0188     return 0
0189 
0190 
0191 if __name__ == "__main__":
0192     sys.exit(main())
0193 # vi: ts=4 sw=4 et