diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4c0e9c47..10d34811 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -19,7 +19,7 @@ include:
.fedora:
variables:
# Update this tag when you want to trigger a rebuild
- FDO_DISTRIBUTION_TAG: '2021-04-01.1'
+ FDO_DISTRIBUTION_TAG: '2021-04-16.1'
FDO_DISTRIBUTION_VERSION: '32'
# findutils: used by the .build script below
# cmake: required for hotdoc
@@ -45,6 +45,7 @@ include:
# in the fedora docker image
FDO_DISTRIBUTION_EXEC: >-
dnf -y install python3-pip doxygen python3-sphinx python3-sphinx_rtd_theme python3-breathe python-docutils ;
+ pip3 install lxml ;
dnf -y install glib2-doc --setopt='tsflags='
.build:
diff --git a/docs/gen-api-gtkdoc.py.in b/docs/gen-api-gtkdoc.py.in
new file mode 100644
index 00000000..10a49de9
--- /dev/null
+++ b/docs/gen-api-gtkdoc.py.in
@@ -0,0 +1,416 @@
+#!/usr/bin/env python
+
+#
+# Copyright 2015 The Geany contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1301, USA.
+
+import os
+import sys
+import re
+from lxml import etree
+from optparse import OptionParser
+
+
+def normalize_text(s):
+ r"""
+ Normalizes whitespace in text.
+ >>> normalize_text("asd xxx")
+ 'asd xxx'
+ >>> normalize_text(" asd\nxxx ")
+ 'asd xxx'
+ """
+ return s.replace("\n", " ").strip()
+
+
+CXX_NAMESPACE_RE = re.compile(r'[_a-zA-Z][_0-9a-zA-Z]*::')
+def fix_definition(s):
+ """
+ Removes C++ name qualifications from some definitions.
+ For example:
+ >>> fix_definition("bool flag")
+ 'bool flag'
+ >>> fix_definition("bool FooBar::flag")
+ 'bool flag'
+ >>> fix_definition("void(* _GeanyObjectClass::project_open) (GKeyFile *keyfile)")
+ 'void(* project_open) (GKeyFile *keyfile)'
+ """
+ return CXX_NAMESPACE_RE.sub(r"", s)
+
+
+class AtDoc(object):
+ def __init__(self):
+ self.retval = None
+ self.since = ""
+ self.annot = []
+
+ def cb(self, type, str):
+ if (type == "param"):
+ words = str.split(" ", 2)
+ self.annot = []
+ elif (type == "return"):
+ self.annot = []
+ elif (type == "since"):
+ self.since = str.rstrip()
+ elif (type == "see"):
+ return "See " + str
+ elif type in ("a", "c") and str in ("NULL", "TRUE", "FALSE"):
+ return "%" + str
+ elif (type == "a"):
+ return "@" + str
+ else:
+ return str
+
+ return ""
+
+
+class DoxygenProcess(object):
+ def __init__(self):
+ self.at = None
+
+ # http://stackoverflow.com/questions/4624062/get-all-text-inside-a-tag-in-lxml
+ @staticmethod
+ def stringify_children(node):
+ from lxml.etree import tostring
+ from itertools import chain
+ parts = ([node.text] +
+ list(chain(*([c.text, tostring(c).decode("utf-8"), c.tail] for c in node.getchildren()))) +
+ [node.tail])
+ # filter removes possible Nones in texts and tails
+ return "".join(filter(None, parts))
+
+ def get_program_listing(self, xml):
+ from lxml.etree import tostring
+ arr = ["", "|["]
+ for l in xml.getchildren():
+ if (l.tag == "codeline"):
+ # a codeline is of the form
+ # GeanyDocument*doc=...;
+ # tags must be replaced with spaces, then just use the text
+ h = l.find("highlight")
+ if h is not None:
+ html = tostring(h).decode("utf-8")
+ html = html.replace("", " ")
+ arr.append(" " + tostring(etree.HTML(html), method="text").decode("utf-8"))
+ arr.append("]|")
+ return "\n".join(arr)
+
+ def join_annot(self):
+ s = " ".join(map(lambda x: "(%s)" % x, self.at.annot))
+ return s + ": " if s else ""
+
+ def process_element(self, xml):
+ self.at = AtDoc()
+ s = self.__process_element(xml)
+ return s
+
+ def get_extra(self):
+ return self.join_annot()
+
+ def get_return(self):
+ return self.at.retval
+
+ def get_since(self):
+ return self.at.since
+
+ def __process_element(self, xml):
+ s = ""
+
+ if xml.text:
+ s += xml.text
+ for n in xml.getchildren():
+ if n.tag == "emphasis":
+ s += self.at.cb("a", self.__process_element(n))
+ if n.tag == "computeroutput":
+ s += self.at.cb("c", self.__process_element(n))
+ if n.tag == "itemizedlist":
+ s += "\n" + self.__process_element(n)
+ if n.tag == "listitem":
+ s += " - " + self.__process_element(n)
+ if n.tag == "para":
+ s += self.__process_element(n) + "\n"
+ if n.tag == "ref":
+ s += n.text if n.text else ""
+ if n.tag == "simplesect":
+ ss = self.at.cb(n.get("kind"), self.__process_element(n))
+ s += ss + "\n" if ss else ""
+ if n.tag == "programlisting":
+ s += self.get_program_listing(n)
+ if n.tag == "xrefsect":
+ s += self.__process_element(n)
+ if n.tag == "xreftitle":
+ s += self.__process_element(n) + ": "
+ if n.tag == "xrefdescription":
+ s += self.__process_element(n)
+ if n.tag == "ulink":
+ s += self.__process_element(n)
+ if n.tag == "linebreak":
+ s += "\n"
+ if n.tag == "ndash":
+ s += "--"
+ # workaround for doxygen bug #646002
+ if n.tag == "htmlonly":
+ s += ""
+ if n.tail:
+ s += n.tail
+ if n.tag.startswith("param"):
+ pass # parameters are handled separately in DoxyFunction::from_memberdef()
+ return s
+
+
+class DoxyMember(object):
+ def __init__(self, name, brief, extra=""):
+ self.name = name
+ self.brief = brief
+ self.extra = extra
+
+
+class DoxyElement(object):
+
+ def __init__(self, name, definition, **kwargs):
+ self.name = name
+ self.definition = definition
+ self.brief = kwargs.get('brief', "")
+ self.detail = kwargs.get('detail', "")
+ self.members = kwargs.get('members', [])
+ self.since = kwargs.get('since', "")
+ self.extra = kwargs.get('extra', "")
+ self.retval = kwargs.get('retval', None)
+
+ def is_documented(self):
+ if (normalize_text(self.brief)) != "":
+ return True
+ return False
+
+ def add_brief(self, xml):
+ proc = DoxygenProcess()
+ self.brief = proc.process_element(xml)
+ self.extra += proc.get_extra()
+
+ def add_detail(self, xml):
+ proc = DoxygenProcess()
+ self.detail = proc.process_element(xml)
+ self.extra += proc.get_extra()
+ self.since = proc.get_since()
+
+ def add_member(self, xml):
+ name = xml.find("name").text
+ proc = DoxygenProcess()
+ brief = proc.process_element(xml.find("briefdescription"))
+ # optional doxygen command output appears within
+ proc.process_element(xml.find("detaileddescription"))
+ self.members.append(DoxyMember(name, normalize_text(brief), proc.get_extra()))
+
+ def add_param(self, xml):
+ name = xml.find("parameternamelist").find("parametername").text
+ proc = DoxygenProcess()
+ brief = proc.process_element(xml.find("parameterdescription"))
+ self.members.append(DoxyMember(name, normalize_text(brief), proc.get_extra()))
+
+ def add_return(self, xml):
+ proc = DoxygenProcess()
+ brief = proc.process_element(xml)
+ self.retval = DoxyMember("ret", normalize_text(brief), proc.get_extra())
+
+ def to_gtkdoc(self):
+ s = []
+ s.append("/**")
+ s.append(" * %s: %s" % (self.name, self.extra))
+ for p in self.members:
+ s.append(" * @%s: %s %s" % (p.name, p.extra, p.brief))
+ s.append(" *")
+ s.append(" * %s" % self.brief.replace("\n", "\n * "))
+ s.append(" *")
+ s.append(" * %s" % self.detail.replace("\n", "\n * "))
+ s.append(" *")
+ if self.retval:
+ s.append(" * Returns: %s %s" % (self.retval.extra, self.retval.brief))
+ if self.since:
+ s.append(" *")
+ s.append(" * Since: %s" % self.since)
+ s.append(" */")
+ s.append("")
+ return "\n".join(s)
+
+
+class DoxyTypedef(DoxyElement):
+ @staticmethod
+ def from_memberdef(xml):
+ name = xml.find("name").text
+ d = normalize_text(xml.find("definition").text)
+ d += ";"
+ return DoxyTypedef(name, d)
+
+
+class DoxyEnum(DoxyElement):
+ @staticmethod
+ def from_memberdef(xml):
+ name = xml.find("name").text
+ d = "typedef enum {\n"
+ for member in xml.findall("enumvalue"):
+ v = member.find("initializer")
+ d += "\t%s%s,\n" % (member.find("name").text, " "+v.text if v is not None else "")
+ d += "} %s;\n" % name
+
+ e = DoxyEnum(name, d)
+ e.add_brief(xml.find("briefdescription"))
+ for p in xml.findall("enumvalue"):
+ e.add_member(p)
+ return e
+
+
+class DoxyStruct(DoxyElement):
+ @staticmethod
+ def from_compounddef(xml, typedefs=[]):
+ name = xml.find("compoundname").text
+ d = "struct %s {\n" % name
+ memberdefs = xml.xpath(".//sectiondef[@kind='public-attrib']/memberdef")
+ for p in memberdefs:
+ # workaround for struct members. g-ir-scanner can't properly map struct members
+ # (beginning with struct GeanyFoo) to the typedef and assigns a generic type for them
+ # thus we fix that up here and enforce usage of the typedef. These are written
+ # out first, before any struct definition, for this reason
+ # Exception: there are no typedefs for GeanyFooPrivate so skip those. Their exact
+ # type isn't needed anyway
+ s = fix_definition(p.find("definition").text).lstrip()
+ proc = DoxygenProcess()
+ brief = proc.process_element(p.find("briefdescription"))
+ private = (normalize_text(brief) == "")
+ words = s.split()
+ if (words[0] == "struct"):
+ if not (words[1].endswith("Private") or words[1].endswith("Private*")):
+ s = " ".join(words[1:])
+ d += "\t/*< %s >*/\n\t%s;\n" % ("private" if private else "public", s)
+
+ d += "};\n"
+ e = DoxyStruct(name, d)
+ e.add_brief(xml.find("briefdescription"))
+ for p in memberdefs:
+ e.add_member(p)
+ return e
+
+
+class DoxyFunction(DoxyElement):
+ @staticmethod
+ def from_memberdef(xml):
+ name = xml.find("name").text
+ d = normalize_text(xml.find("definition").text)
+ if ((xml.find("argsstring").text) is not None):
+ d += " " + xml.find("argsstring").text + ";"
+ d = normalize_text(d)
+
+ e = DoxyFunction(name, d)
+ e.add_brief(xml.find("briefdescription"))
+ e.add_detail(xml.find("detaileddescription"))
+ for p in xml.xpath(".//detaileddescription/*/parameterlist[@kind='param']/parameteritem"):
+ e.add_param(p)
+ x = xml.xpath(".//detaileddescription/*/simplesect[@kind='return']")
+ if (len(x) > 0):
+ e.add_return(x[0])
+ return e
+
+
+def main(args):
+ xml_dir = None
+ outfile = None
+
+ parser = OptionParser(usage="usage: %prog [options] XML_DIR")
+ parser.add_option("--xmldir", metavar="DIRECTORY", help="Path to Doxygen-generated XML files",
+ action="store", dest="xml_dir")
+ parser.add_option("-d", "--outdir", metavar="DIRECTORY", help="Path to Doxygen-generated XML files",
+ action="store", dest="outdir", default=".")
+ parser.add_option("-o", "--output", metavar="FILE", help="Write output to FILE",
+ action="store", dest="outfile")
+ opts, args = parser.parse_args(args[1:])
+
+ xml_dir = args[0]
+
+ if not (os.path.exists(xml_dir)):
+ sys.stderr.write("invalid xml directory\n")
+ return 1
+
+ transform = etree.XSLT(etree.parse(os.path.join(xml_dir, "combine.xslt")))
+ doc = etree.parse(os.path.join(xml_dir, "index.xml"))
+ root = transform(doc)
+
+ other = []
+ enums = []
+ typedefs = []
+
+ c_files = root.xpath(".//compounddef[@kind='file']/compoundname[substring(.,string-length(.)-1)='.c']/..")
+ h_files = root.xpath(".//compounddef[@kind='file']/compoundname[substring(.,string-length(.)-1)='.h']/..")
+
+ for f in h_files:
+ if not (f.find("compoundname").text.endswith("private.h")):
+ for n0 in f.xpath(".//*/memberdef[@kind='typedef' and @prot='public']"):
+ if not (DoxygenProcess.stringify_children(n0.find("type")).startswith("enum")):
+ e = DoxyTypedef.from_memberdef(n0)
+ typedefs.append(e)
+
+ for n0 in f.xpath(".//*/memberdef[@kind='enum' and @prot='public']"):
+ e = DoxyEnum.from_memberdef(n0)
+ enums.append(e)
+
+ for n0 in root.xpath(".//compounddef[@kind='struct' and @prot='public']"):
+ e = DoxyStruct.from_compounddef(n0)
+ other.append(e)
+
+ for n0 in root.xpath(".//compounddef[@kind='struct' and @prot='public']"):
+ for n1 in n0.xpath(".//*/memberdef[@kind='function' and @prot='public']"):
+ e = DoxyFunction.from_memberdef(n1)
+ other.append(e)
+
+ if (opts.outfile):
+ try:
+ outfile = open(opts.outfile, "w+")
+ except OSError as err:
+ sys.stderr.write("failed to open \"%s\" for writing (%s)\n" % (opts.outfile, err.strerror))
+ return 1
+ else:
+ outfile = sys.stdout
+
+ try:
+ outfile.write("/*\n * Automatically generated file - do not edit\n */\n\n")
+
+ # write enums first, so typedefs to them are valid (as forward enum declaration
+ # is invalid). It's fine as an enum can't contain reference to other types.
+ for e in filter(lambda x: x.is_documented(), enums):
+ outfile.write("\n\n")
+ outfile.write(e.to_gtkdoc())
+ outfile.write(e.definition)
+ outfile.write("\n\n")
+
+ # write typedefs second, they are possibly undocumented but still required (even
+ # if they are documented, they must be written out without gtkdoc)
+ for e in typedefs:
+ outfile.write(e.definition)
+ outfile.write("\n\n")
+
+ # write the rest (structures, functions, ...)
+ for e in filter(lambda x: x.is_documented(), other):
+ outfile.write("\n\n")
+ outfile.write(e.to_gtkdoc())
+ outfile.write(e.definition)
+ outfile.write("\n\n")
+
+ except BrokenPipeError:
+ # probably piped to head or tail
+ return 0
+
+ return 0
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv))
\ No newline at end of file
diff --git a/docs/meson.build b/docs/meson.build
index 7819e50d..472dd91b 100644
--- a/docs/meson.build
+++ b/docs/meson.build
@@ -4,7 +4,6 @@ if not sphinx_p.found()
subdir_done()
endif
-doxygen_p = find_program('doxygen', required: get_option('doc'))
if not doxygen_p.found()
message('Doxygen not found, not building the documentation')
subdir_done()
@@ -16,6 +15,18 @@ if not breathe_p.found()
subdir_done()
endif
+pymod = import('python')
+lxml_p = pymod.find_installation(
+ modules: [
+ 'lxml',
+ ],
+ required: get_option('doc')
+)
+if not lxml_p.found()
+ message('lxml not found, not building gtk documentation from Doxygen')
+ subdir_done()
+endif
+
sphinx_c = run_command(sphinx_p, '--version')
breathe_c = run_command(breathe_p, '--version')
doxygen_c = run_command(doxygen_p, '--version')
@@ -40,9 +51,6 @@ if not build_gir
if get_option('doc').enabled()
error('Documentation enabled but introspection not built.')
endif
-
-# message('Introspection not built, can\'t build the documentation')
-# subdir_done()
endif
doxygen_database = meson.current_build_dir() + '/doxygen_doc'
@@ -58,6 +66,8 @@ sphinx_conf = configure_file(
configuration: sphinx_conf_data
)
+# build gir
+
doxy_conf_data = configuration_data()
doxy_conf_data.set('SRC_ROOT', meson.source_root())
doxy_conf_data.set('OUTPUT_DIR', doxygen_database)
@@ -73,13 +83,12 @@ dir_data = join_paths(dir_prefix, get_option('datadir'))
script_data = configuration_data()
script_data.set('SRCDIR', meson.current_build_dir())
script_data.set('OUTDIR', meson.current_build_dir() + '/docs')
-script_data.set('DOXYGEN_CONF', meson.current_build_dir() + '/doxyfile')
# Set a different directory for doctrees to avoid installing them
script_data.set('DOCTREES_DIR', meson.current_build_dir() + '/doctrees')
-script_data.set('DOXYGEN_CMD', doxygen_p.path())
script_data.set('SPHINX_CMD', sphinx_p.path())
+
script_doxy_sphinx = configure_file(
input: 'run_doxygen_sphinx.sh.in',
output: 'run_doxygen_sphinx.sh',
diff --git a/docs/run_doxygen_sphinx.sh.in b/docs/run_doxygen_sphinx.sh.in
index 3b0eba95..e7a7f9f1 100755
--- a/docs/run_doxygen_sphinx.sh.in
+++ b/docs/run_doxygen_sphinx.sh.in
@@ -1,2 +1,2 @@
#!/bin/sh
-@DOXYGEN_CMD@ @DOXYGEN_CONF@ && @SPHINX_CMD@ -E -Q -j auto -d @DOCTREES_DIR@ @SRCDIR@ @OUTDIR@
+@SPHINX_CMD@ -E -Q -j auto -d @DOCTREES_DIR@ @SRCDIR@ @OUTDIR@
diff --git a/docs/run_gen_api.sh.in b/docs/run_gen_api.sh.in
new file mode 100755
index 00000000..37051b13
--- /dev/null
+++ b/docs/run_gen_api.sh.in
@@ -0,0 +1,2 @@
+#!/bin/sh
+@DOXYGEN_CMD@ @DOXYGEN_CONF@ && @PYTHON@ @GIR_PY_SCRIPT@ @XML_DIR@ -d @XML_DIR@ -o @OUTDIR@/wp-gtkdoc.h
\ No newline at end of file
diff --git a/lib/wp/meson.build b/lib/wp/meson.build
index 35fc9b8a..2e5e491f 100644
--- a/lib/wp/meson.build
+++ b/lib/wp/meson.build
@@ -106,10 +106,37 @@ wp_lib = library('wireplumber-' + wireplumber_api_version,
)
if build_gir
+ wp_gtkdoc_conf = configure_file(
+ input: '../../docs/gen-api-gtkdoc.py.in',
+ output: 'gen-api-gtkdoc.py',
+ copy: true
+ )
+
+ script_data = configuration_data()
+ script_data.set('PYTHON', 'python3')
+ script_data.set('OUTDIR', meson.current_build_dir())
+ script_data.set('GIR_PY_SCRIPT', meson.current_build_dir() + '/gen-api-gtkdoc.py')
+ script_data.set('XML_DIR', meson.build_root() + '/docs/doxygen_doc/xml')
+ script_data.set('DOXYGEN_CONF', meson.build_root() + '/docs/doxyfile')
+ script_data.set('DOXYGEN_CMD', doxygen_p.path())
+
+ script_gen_api = configure_file(
+ input: '../../docs/run_gen_api.sh.in',
+ output: 'run_gen_api.sh',
+ configuration: script_data
+ )
+
+ wp_gtkdoc_h = custom_target(
+ 'gir',
+ command: script_gen_api,
+ output: 'wp-gtkdoc.h',
+ build_by_default: true,
+ )
+
wp_gir = gnome.generate_gir(wp_lib,
namespace: 'Wp',
nsversion: wireplumber_api_version,
- sources: [wp_lib_sources, wp_lib_headers, wpenums_c, wpenums_h],
+ sources: [wpenums_c, wpenums_h, wp_gtkdoc_h],
includes: ['GLib-2.0', 'GObject-2.0', 'Gio-2.0'],
install: true,
)
diff --git a/meson.build b/meson.build
index 660088cc..80cce931 100644
--- a/meson.build
+++ b/meson.build
@@ -93,6 +93,8 @@ common_flags = [
]
add_project_arguments(cc.get_supported_arguments(common_flags), language: 'c')
+doxygen_p = find_program('doxygen', required: get_option('doc'))
+
subdir('lib')
subdir('docs')
subdir('modules')