Skip to content

File node.py

File List > mkdoxy > node.py

Go to the documentation of this file

import logging
import os
from xml.etree import ElementTree
from xml.etree.ElementTree import Element as Element

from mkdoxy.constants import OVERLOAD_OPERATORS, Kind, Visibility
from mkdoxy.markdown import escape
from mkdoxy.project import ProjectContext
from mkdoxy.property import Property
from mkdoxy.utils import split_safe
from mkdoxy.xml_parser import XmlParser

log: logging.Logger = logging.getLogger("mkdocs")


class Node:
    def __init__(
        self,
        xml_file: str,
        xml: Element,
        project: ProjectContext,
        parser: XmlParser,
        parent: "Node",
        refid: str = None,
        debug: bool = False,
    ):
        self._children: ["Node"] = []
        self._cache_cache = project.cache
        self._parser: XmlParser = parser
        self._parent_parent = parent
        self.debugdebug = debug
        self.projectproject = project

        if xml_file == "root":
            self._refid_refid = "root"
            self._kind_kind = Kind.from_str("root")
            self._name_name = "root"
            self._xml_xml = None

        elif xml is None:
            if self.debugdebug:
                log.info(f"Loading XML from: {xml_file}")
            self._dirname_dirname = os.path.dirname(xml_file)
            self._xml_xml = ElementTree.parse(xml_file).getroot().find("compounddef")
            if self._xml_xml is None:
                raise Exception(f"File {xml_file} has no <compounddef>")
            self._kind_kind = Kind.from_str(self._xml_xml.get("kind"))
            self._refid_refid = self._xml_xml.get("id")
            self._language_language = self._xml_xml.get("language")
            if self._xml_xml.find("compoundname").text is not None:
                self._name_name = self._xml_xml.find("compoundname").text
            elif self.is_namespaceis_namespace:
                location = self._xml_xml.find("location")
                self._name_name = f"anonymous namespace{{{location.get('file')}}}" if location is not None else self._refid_refid
            else:
                self._name_name = self._refid_refid
            self._cache_cache.add(self._refid_refid, self)
            self._static_static = False

            if self.debugdebug:
                log.info(f"Parsing: {self._refid}")
            self._check_for_children_check_for_children()

            title = self._xml_xml.find("title")
            self._title_title = title.text if title is not None else self._name_name
        else:
            self._xml_xml = xml
            self._kind_kind = Kind.from_str(self._xml_xml.get("kind"))
            self._language_language = parent.code_language
            self._refid_refid = refid if refid is not None else self._xml_xml.get("id")
            self._cache_cache.add(self._refid_refid, self)

            if self.debugdebug:
                log.info(f"Parsing: {self._refid}")
            self._check_attrs_check_attrs()
            self._title_title = self._name_name

        self._details_details = Property.Details(self._xml_xml, parser, self._kind_kind)
        self._brief_brief = Property.Brief(self._xml_xml, parser, self._kind_kind)
        self._includes_includes = Property.Includes(self._xml_xml, parser, self._kind_kind)
        self._type_type = Property.Type(self._xml_xml, parser, self._kind_kind)
        self._location_location = Property.Location(self._xml_xml, parser, self._kind_kind)
        self._params_params = Property.Params(self._xml_xml, parser, self._kind_kind)
        self._templateparams_templateparams = Property.TemplateParams(self._xml_xml, parser, self._kind_kind)
        self._specifiers_specifiers = Property.Specifiers(self._xml_xml, parser, self._kind_kind)
        self._values_values = Property.Values(self._xml_xml, parser, self._kind_kind)
        self._initializer_initializer = Property.Initializer(self._xml_xml, parser, self._kind_kind)
        self._definition_definition = Property.Definition(self._xml_xml, parser, self._kind_kind)
        self._programlisting_programlisting = Property.Programlisting(self._xml_xml, parser, self._kind_kind)

    def __repr__(self):
        return f"Node: {self.name} refid: {self._refid}"

    def add_child(self, child: "Node"):
        self._children.append(child)

    def sort_children(self):
        self._children.sort(key=lambda x: x._name, reverse=False)

    def _check_for_children(self):
        for innergroup in self._xml_xml.findall("innergroup"):
            refid = innergroup.get("refid")
            if self._kind_kind in [Kind.GROUP, Kind.DIR, Kind.FILE]:
                try:
                    child = self._cache_cache.get(refid)
                    self.add_childadd_child(child)
                    continue
                except Exception:
                    pass
            child = Node(
                os.path.join(self._dirname_dirname, f"{refid}.xml"),
                None,
                self.projectproject,
                self._parser,
                self,
            )
            child._visibility = Visibility.PUBLIC
            self.add_childadd_child(child)

        for innerclass in self._xml_xml.findall("innerclass"):
            refid = innerclass.get("refid")
            prot = Visibility(innerclass.get("prot"))
            if prot == Visibility.PRIVATE:
                continue

            if self._kind_kind in [Kind.GROUP, Kind.DIR, Kind.FILE]:
                try:
                    child = self._cache_cache.get(refid)
                    self.add_childadd_child(child)
                    continue
                except Exception:
                    pass

            try:
                child = Node(
                    os.path.join(self._dirname_dirname, f"{refid}.xml"),
                    None,
                    self.projectproject,
                    self._parser,
                    self,
                )
            except FileNotFoundError:
                child = Node(
                    os.path.join(self._dirname_dirname, f"{refid}.xml"),
                    Element("compounddef"),
                    self.projectproject,
                    self._parser,
                    self,
                    refid=refid,
                )
                child._name = innerclass.text
            child._visibility = prot
            self.add_childadd_child(child)

        for innerfile in self._xml_xml.findall("innerfile"):
            refid = innerfile.get("refid")
            if self._kind_kind == Kind.DIR:
                try:
                    child = self._cache_cache.get(refid)
                    self.add_childadd_child(child)
                    continue
                except Exception:
                    pass

            child = Node(
                os.path.join(self._dirname_dirname, f"{refid}.xml"),
                None,
                self.projectproject,
                self._parser,
                self,
            )
            child._visibility = Visibility.PUBLIC
            self.add_childadd_child(child)

        for innerdir in self._xml_xml.findall("innerdir"):
            refid = innerdir.get("refid")
            if self._kind_kind == Kind.DIR:
                try:
                    child = self._cache_cache.get(refid)
                    self.add_childadd_child(child)
                    continue
                except Exception:
                    pass

            child = Node(
                os.path.join(self._dirname_dirname, f"{refid}.xml"),
                None,
                self.projectproject,
                self._parser,
                self,
            )
            child._visibility = Visibility.PUBLIC
            self.add_childadd_child(child)

        for innernamespace in self._xml_xml.findall("innernamespace"):
            refid = innernamespace.get("refid")

            if self._kind_kind in [Kind.GROUP, Kind.DIR, Kind.FILE]:
                try:
                    child = self._cache_cache.get(refid)
                    self.add_childadd_child(child)
                    continue
                except Exception:
                    pass

            child = Node(
                os.path.join(self._dirname_dirname, f"{refid}.xml"),
                None,
                self.projectproject,
                self._parser,
                self,
            )
            child._visibility = Visibility.PUBLIC
            self.add_childadd_child(child)

        for sectiondef in self._xml_xml.findall("sectiondef"):
            for memberdef in sectiondef.findall("memberdef"):
                kind = Kind.from_str(memberdef.get("kind"))
                if kind.is_language():
                    if self._kind_kind in [Kind.GROUP, Kind.DIR, Kind.FILE]:
                        refid = memberdef.get("id")
                        try:
                            child = self._cache_cache.get(refid)
                            self.add_childadd_child(child)
                            continue
                        except Exception:
                            pass
                    child = Node(None, memberdef, self.projectproject, self._parser, self)
                    self.add_childadd_child(child)

        # for detaileddescription in self._xml.findall('detaileddescription'):
        #   for para in detaileddescription.findall('para'):
        #       for programlisting in para.findall('programlisting'):
        #           pass

        # # kind = Kind.from_str(memberdef.get('kind'))
        # #     if kind.is_language():
        # if self._kind in [Kind.EXAMPLE]:
        #   log.info(f'programlisting: {para.text}')
        # if para.find('programlisting') is not None:
        #   self._programlisting = Property.Programlisting(para, self._parser, self._kind)

    def _check_attrs(self):
        prot = self._xml_xml.get("prot")
        self._visibility_visibility = Visibility(prot) if prot is not None else Visibility.PUBLIC

        static = self._xml_xml.get("static")
        self._static_static = static == "yes"

        explicit = self._xml_xml.get("explicit")
        self._explicit_explicit = explicit == "yes"

        mutable = self._xml_xml.get("mutable")
        self._mutable_mutable = mutable == "yes"

        inline = self._xml_xml.get("inline")
        self._inline_inline = inline == "yes"

        const = self._xml_xml.get("inline")
        self._const_const = const == "yes"

        name = self._xml_xml.find("name")
        if name is not None and name.text:
            self._name_name = name.text
        else:
            # Doxygen doesn't give anonymous unions any name
            qualifiedname = self._xml_xml.find("qualifiedname")
            if qualifiedname is not None and qualifiedname.text:
                self._name_name = qualifiedname.text
            else:
                self._name_name = self._refid_refid

        virt = self._xml_xml.get("virt")
        if virt:
            self._virtual_virtual = virt in ["virtual", "pure-virtual"]
            self._pure_pure = virt == "pure-virtual"
        else:
            self._virtual_virtual = False
            self._pure_pure = False

    def has(self, visibility: str, kinds: [str], static: bool) -> bool:
        return len(self.queryquery(visibility, kinds, static)) > 0

    def query(self, visibility: str, kinds: [str], static: bool) -> ["Node"]:
        visibility = Visibility(visibility)
        kinds = list(map(lambda kind: Kind.from_str(kind), kinds))
        return [
            child
            for child in self._children
            if child._visibility == visibility and child._kind in kinds and child._static == static
        ]

    @property
    def is_static(self) -> bool:
        return self._static_static

    @property
    def is_explicit(self) -> bool:
        return self._explicit_explicit

    @property
    def is_const(self) -> bool:
        return self._const_const

    @property
    def is_inline(self) -> bool:
        return self._inline_inline

    @property
    def is_mutable(self) -> bool:
        return self._mutable_mutable

    @property
    def is_virtual(self) -> bool:
        return self._virtual_virtual

    @property
    def is_pure(self) -> bool:
        return self._pure_pure

    @property
    def has_children(self) -> bool:
        return len(self._children) > 0

    @property
    def children(self) -> ["Node"]:
        return self._children

    @property
    def parent(self) -> "Node":
        return self._parent_parent

    @property
    def is_function(self) -> bool:
        return self._kind_kind.is_function()

    @property
    def is_variable(self) -> bool:
        return self._kind_kind.is_variable()

    @property
    def is_namespace(self) -> bool:
        return self._kind_kind.is_namespace()

    @property
    def is_class(self) -> bool:
        return self._kind_kind.is_class()

    @property
    def is_struct(self) -> bool:
        return self._kind_kind.is_struct()

    @property
    def is_enum(self) -> bool:
        return self._kind_kind.is_enum()

    @property
    def is_class_or_struct(self) -> bool:
        return self._kind_kind.is_class_or_struct()

    @property
    def is_interface(self) -> bool:
        return self._kind_kind.is_interface()

    @property
    def is_typedef(self) -> bool:
        return self._kind_kind.is_typedef()

    @property
    def is_define(self) -> bool:
        return self._kind_kind.is_define()

    @property
    def is_union(self) -> bool:
        return self._kind_kind.is_union()

    @property
    def is_group(self) -> bool:
        return self._kind_kind.is_group()

    @property
    def is_language(self) -> bool:
        return self._kind_kind.is_language()

    @property
    def is_root(self) -> bool:
        return self._kind_kind.is_root()

    @property
    def is_parent(self) -> bool:
        return self._kind_kind.is_parent()

    @property
    def is_friend(self) -> bool:
        return self._kind_kind.is_friend()

    @property
    def is_file(self) -> bool:
        return self._kind_kind.is_file()

    @property
    def is_dir(self) -> bool:
        return self._kind_kind.is_dir()

    @property
    def is_page(self) -> bool:
        return self._kind_kind.is_page()

    @property
    def is_example(self) -> bool:
        return self._kind_kind.is_example()

    @property
    def name(self) -> str:
        return self._name_name

    @property
    def name_params(self) -> str:
        name = self._name_name
        type = self._type_type.plain()
        params = self._specifiers_specifiers.plain()
        return f"{type} {name}{params}" if params else self.name_longname_long

    @property
    def title(self) -> str:
        return self._title_title

    @property
    def refid(self) -> str:
        return self._refid_refid

    @property
    def kind(self) -> str:
        return self._kind_kind

    @property
    def is_operator(self) -> bool:
        return self._name_name in OVERLOAD_OPERATORS

    @property
    def operators_total(self) -> int:
        return sum(child.name in OVERLOAD_OPERATORS for child in self.childrenchildren)

    @property
    def operator_num(self) -> int:
        total = 0
        for child in self.parentparent.children:
            if child.is_function and child.name.replace(" ", "") in OVERLOAD_OPERATORS:
                total += 1
            if child.refid == self._refid_refid:
                break
        return total

    @property
    def name_url_safe(self) -> str:
        name = self.name_tokensname_tokens[-1]
        return name.replace(" ", "-").replace("=", "").replace("~", "").lower()

    @property
    def anchor(self) -> str:
        name = ""
        if self._name_name.replace(" ", "") in OVERLOAD_OPERATORS:
            num = self.operator_numoperator_num
            name = f"operator_{str(self.operator_num - 1)}" if num > 1 else "operator"
        elif self.is_overloadedis_overloaded:
            name = f"{self.name_url_safe}-{str(self.overload_num)}{str(self.overload_total)}"
        else:
            name = self.name_url_safename_url_safe

        if name.startswith("-"):
            name = name[1:]
        return f"{self._kind.value}-{name}"

    @property
    def url(self) -> str:
        if self.is_parentis_parent or self.is_groupis_group or self.is_fileis_file or self.is_diris_dir or self.is_pageis_page:
            return self.projectproject.linkPrefix + self._refid_refid + ".md"
        else:
            return f"{self._parent.url}#{self.anchor}"

    @property
    def base_url(self) -> str:
        def prefix(page: str):
            return self.projectproject.linkPrefix + page

        if self.is_groupis_group:
            return prefix("modules.md")
        elif self.is_fileis_file or self.is_diris_dir:
            return prefix("files.md")
        elif self.is_namespaceis_namespace:
            return prefix("namespaces.md")
        else:
            return prefix("annotated.md")

    @property
    def base_name(self) -> str:
        if self.is_groupis_group:
            return "Modules"
        elif self.is_fileis_file or self.is_diris_dir:
            return "FileList"
        elif self.is_namespaceis_namespace:
            return "Namespace List"
        else:
            return "ClassList"

    @property
    def url_source(self) -> str:
        if self.is_parentis_parent or self.is_groupis_group or self.is_fileis_file or self.is_diris_dir:
            return self.projectproject.linkPrefix + self._refid_refid + "_source.md"
        else:
            return self.projectproject.linkPrefix + self._refid_refid + ".md"

    @property
    def filename(self) -> str:
        return self.projectproject.linkPrefix + self._refid_refid + ".md"

    @property
    def root(self) -> "Node":
        return self if self._kind_kind == Kind.ROOT else self._parent_parent.root

    @property
    def name_tokens(self) -> [str]:
        if self.is_diris_dir or self.is_fileis_file:
            return self._name_name.split("/")
        return split_safe(self._name_name, "::")

    @property
    def name_short(self) -> str:
        return escape(self.name_tokensname_tokens[-1])

    @property
    def name_long(self) -> str:
        try:
            if self._parent_parent.is_parent:
                return f"{self._parent.name_long}::{escape(self.name_tokens[-1])}"
            else:
                return escape(self._name_name)
        except Exception as e:
            print(e)
            raise e

    @property
    def name_full_unescaped(self) -> str:
        if self._parent_parent is not None and not self._parent_parent.is_root and self._parent_parent.is_parent:
            return f"{self._parent.name_full_unescaped}::{self.name_tokens[-1]}"
        else:
            return self.name_tokensname_tokens[-1]

    @property
    def overload_total(self) -> int:
        if self._parent_parent is not None and self._parent_parent.is_class_or_struct:
            return sum(neighbour.name == self.namename for neighbour in self._parent_parent.children)
        return 0

    @property
    def overload_num(self) -> int:
        if self._parent_parent is not None and self._parent_parent.is_class_or_struct:
            count = 0
            for neighbour in self._parent_parent.children:
                if neighbour.name == self.namename:
                    count += 1
                if neighbour.refid == self.refidrefid:
                    break
            return count
        return 0

    @property
    def is_overloaded(self) -> bool:
        return self.overload_totaloverload_total > 1

    @property
    def overload_suffix(self) -> str:
        if self.is_operatoris_operator:
            return ""

        total = self.overload_totaloverload_total
        return f"[{str(self.overload_num)}/{str(total)}]" if total > 1 else ""

    @property
    def parents(self) -> ["Node"]:
        ret = []
        if self._parent_parent is not None and (self._parent_parent.is_language or self._parent_parent.is_dir):
            ret.extend(self.parentparent.parents)
        ret.append(self)
        return ret

    @property
    def suffix(self) -> str:
        if self.is_parentis_parent:
            if self._templateparams_templateparams.has():
                return "&lt;" + ", ".join(self._templateparams_templateparams.array(notype=True)) + "&gt;"
            else:
                return ""
        elif self.is_functionis_function:
            return self._specifiers_specifiers.md()
        elif self.is_variableis_variable:
            return f" = {self._initializer.md()}" if self._initializer_initializer.has() else ""
        elif self.is_defineis_define:
            test = self._initializer_initializer.md()
            return "" if "\n" in test else test
        else:
            return ""

    @property
    def prefix(self) -> str:
        if self.is_functionis_function:
            ret = []
            if self.is_virtualis_virtual:
                ret.append("virtual")
            return " ".join(ret)
        elif self.kindkind is Kind.VARIABLE:
            return ""
        else:
            return self.kindkind.value

    @property
    def code_language(self) -> str:
        return self._language_language

    @property
    def codeblock(self) -> str:
        code = []
        if self.is_functionis_function or self.is_friendis_friend:
            if self._templateparams_templateparams.has():
                code.append(f"template<{self._templateparams.plain()}>")

            typ = self._type_type.plain()
            if typ:
                typ += " "
            if self.is_virtualis_virtual:
                typ = f"virtual {typ}"
            if self.is_explicitis_explicit:
                typ = f"explicit {typ}"
            if self.is_inlineis_inline:
                typ = f"inline {typ}"
            if self.is_staticis_static:
                typ = f"static {typ}"

            if self._params_params.has():
                code.append(typ + self.name_full_unescapedname_full_unescaped + " (")
                params = self._params_params.array(plain=True)
                for i, param in enumerate(params):
                    if i + 1 >= len(params):
                        code.append(f"    {param}")
                    else:
                        code.append(f"    {param},")
                code.append(f") {self._specifiers.parsed()}")
            else:
                code.append(typ + self.name_full_unescapedname_full_unescaped + " () " + self._specifiers_specifiers.parsed())

        elif self.is_enumis_enum:
            if self._values_values.has():
                code.append(f"enum {self.name_full_unescaped}" + " {")

                values = []
                for enumvalue in self._xml_xml.findall("enumvalue"):
                    p = enumvalue.find("name").text
                    initializer = enumvalue.find("initializer")
                    if initializer is not None:
                        p += f" {self._parser.paras_as_str(initializer, plain=True)}"
                    values.append(p)

                for i, value in enumerate(values):
                    if i + 1 >= len(values):
                        code.append(f"    {value}")
                    else:
                        code.append(f"    {value},")
                code.append("};")
            else:
                code.append(f"enum {self.name_full_unescaped};")

        elif self.is_defineis_define:
            if self._params_params.has():
                code.append(f"#define {self.name_full_unescaped} (")
                params = self._params_params.array(plain=True)
                for i, param in enumerate(params):
                    if i + 1 >= len(params):
                        code.append(f"    {param}")
                    else:
                        code.append(f"    {param},")
                code.append(f") {self._initializer.plain()}")
            else:
                code.append(f"#define {self.name_full_unescaped} {self._initializer.plain()}")

        else:
            code.append(self._definition_definition.plain())
        return "\n".join(["```", *code, "```"])

    @property
    def has_base_classes(self) -> bool:
        return len(self._xml_xml.findall("basecompoundref")) > 0

    @property
    def has_derived_classes(self) -> bool:
        return len(self._xml_xml.findall("derivedcompoundref")) > 0

    @property
    def base_classes(self) -> ["Node"]:
        ret = []
        for basecompoundref in self._xml_xml.findall("basecompoundref"):
            refid = basecompoundref.get("refid")
            if refid is None:
                ret.append(basecompoundref.text)
            else:
                ret.append(self._cache_cache.get(refid))
        return ret

    @property
    def derived_classes(self) -> ["Node"]:
        ret = []
        for derivedcompoundref in self._xml_xml.findall("derivedcompoundref"):
            refid = derivedcompoundref.get("refid")
            if refid is None:
                ret.append(derivedcompoundref.text)
            else:
                ret.append(self._cache_cache.get(refid))
        return ret

    @property
    def has_details(self) -> bool:
        return self._details_details.has()

    @property
    def details(self) -> str:
        return self._details_details.md()

    @property
    def has_brief(self) -> bool:
        return self._brief_brief.has()

    @property
    def brief(self) -> str:
        return self._brief_brief.md()

    @property
    def has_includes(self) -> bool:
        return self._includes_includes.has()

    @property
    def includes(self) -> str:
        return self._includes_includes.plain()

    @property
    def has_type(self) -> bool:
        return self._type_type.has()

    @property
    def type(self) -> str:
        return self._type_type.md()

    @property
    def has_location(self) -> bool:
        return self._location_location.has()

    @property
    def location(self) -> str:
        return self._location_location.plain()

    @property
    def location_bodystart(self) -> int:
        return self._location_location.bodystart()

    @property
    def location_bodyend(self) -> int:
        return self._location_location.bodyend()

    @property
    def has_params(self) -> bool:
        return self._params_params.has()

    @property
    def params(self) -> str:
        if self._params_params.has():
            return f"({self._params.md()})"
        elif self.is_functionis_function:
            return "()"
        else:
            return ""

    @property
    def has_templateparams(self) -> bool:
        return self._templateparams_templateparams.has()

    @property
    def templateparams(self) -> str:
        return self._templateparams_templateparams.md()

    @property
    def has_specifiers(self) -> bool:
        return self._specifiers_specifiers.has()

    @property
    def specifiders(self) -> str:
        return self._specifiers_specifiers.parsed()

    @property
    def has_values(self) -> bool:
        return self._values_values.has()

    @property
    def values(self) -> str:
        return self._values_values.md()

    @property
    def has_initializer(self) -> bool:
        return self._initializer_initializer.has()

    @property
    def initializer(self) -> str:
        return self._initializer_initializer.md()

    @property
    def has_definition(self) -> bool:
        return self._definition_definition.has()

    @property
    def definition(self) -> str:
        return self._definition_definition.plain()

    @property
    def has_programlisting(self) -> bool:
        return self._programlisting_programlisting.has()

    @property
    def programlisting(self) -> str:
        return self._programlisting_programlisting.md()

    @property
    def is_resolved(self) -> bool:
        return True

    @property
    def reimplements(self) -> "Node":
        reimp = self._xml_xml.find("reimplements")
        return self._cache_cache.get(reimp.get("refid")) if reimp is not None else None

    @property
    def print_node_recursive(self) -> str:
        # code_block = f'```md\n{self._print_node_recursive_md(self._xml, 0)}```'
        # return code_block
        return self._print_node_recursive_md_print_node_recursive_md(self._xml_xml, 0)

    def _print_node_recursive_md(self, node: Element, depth: int) -> str:
        # print as Markdown code block
        indent = "  " * depth
        ret = f"{indent} * {node.tag} {node.attrib} -> Text: {node.text}\n"
        for child in node.findall("*"):
            ret += self._print_node_recursive_md_print_node_recursive_md(child, depth + 1)

        return ret


class DummyNode:
    def __init__(self, name_long: str, derived_classes: [Node], kind: Kind):
        self.name_longname_long = name_long
        self.derived_classesderived_classes = derived_classes
        self.kindkind = kind

    @property
    def is_resolved(self) -> bool:
        return False