aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Robillard <d@drobilla.net>2020-12-20 20:20:07 +0100
committerDavid Robillard <d@drobilla.net>2020-12-21 09:48:06 +0100
commitf95f22013d51133ec1a7b1554878ff354b9f0f21 (patch)
tree2f65c78412fa9a083bc82e49480ba206fa14a689
parentfabf7113483ffd70024df989de3aa9361146d60c (diff)
downloadserd-f95f22013d51133ec1a7b1554878ff354b9f0f21.tar.gz
serd-f95f22013d51133ec1a7b1554878ff354b9f0f21.tar.bz2
serd-f95f22013d51133ec1a7b1554878ff354b9f0f21.zip
Generate documentation with Sphinx
-rw-r--r--.gitlab-ci.yml15
-rw-r--r--README.md2
-rw-r--r--doc/_static/custom.css95
-rw-r--r--doc/_templates/about.html57
-rw-r--r--doc/c/Doxyfile33
-rw-r--r--doc/c/index.rst5
-rw-r--r--doc/c/overview.rst22
-rw-r--r--doc/c/reference.rst11
-rw-r--r--doc/c/wscript42
-rw-r--r--doc/conf.py.in86
-rw-r--r--doc/footer.html20
-rw-r--r--doc/header.html46
-rw-r--r--doc/index.html.in35
-rw-r--r--doc/layout.xml194
-rw-r--r--doc/mainpage.md15
-rw-r--r--doc/serd.rst11
-rw-r--r--doc/style.css808
-rw-r--r--include/serd/serd.h16
-rw-r--r--resources/serd.svg20
-rwxr-xr-xscripts/dox_to_sphinx.py674
-rw-r--r--wscript11
21 files changed, 1076 insertions, 1142 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 16992228..3f50493d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -88,7 +88,9 @@ test:x64_dbg:
- groff -Thtml -P -l -P -r -man -wall doc/serdi.1 > build/doc/serdi.html
needs: ["x64_dbg"]
artifacts:
- paths: ["build/index.html", "build/coverage", "build/doc"]
+ paths:
+ - build/doc
+ - build/coverage
x64_rel:
@@ -216,15 +218,12 @@ pages:
script:
- mkdir -p .public/doc
- mkdir -p .public/man
- - mkdir -p .public/images
- - mv build/doc/*.svg .public/images
- - mv build/doc/html/ .public/doc/html
- - mv build/coverage/ .public/coverage
- - mv build/index.html .public/index.html
+ - mkdir -p .public/c
+ - mv build/doc/c/singlehtml .public/c/singlehtml
- mv build/doc/serdi.html .public/man/serdi.html
- mv .public public
artifacts:
paths:
- public
- only:
- - master
+ # only:
+ # - master
diff --git a/README.md b/README.md
index d607541c..73ab5ead 100644
--- a/README.md
+++ b/README.md
@@ -48,7 +48,7 @@ constant amount of memory (a single page) for all input sizes.
Documentation
-------------
- * [API reference](https://drobilla.gitlab.io/serd/doc/html/index.html)
+ * [API reference](https://drobilla.gitlab.io/serd/c/singlehtml)
* [`serdi` man page](https://drobilla.gitlab.io/serd/man/serdi.html)
-- David Robillard <d@drobilla.net>
diff --git a/doc/_static/custom.css b/doc/_static/custom.css
new file mode 100644
index 00000000..60aa759e
--- /dev/null
+++ b/doc/_static/custom.css
@@ -0,0 +1,95 @@
+div.document {
+ margin: 0;
+}
+
+div.body {
+ margin-top: 2em;
+}
+
+div.sphinxsidebarwrapper {
+ background: #EEE;
+}
+
+div.sphinxsidebarwrapper p.blurb {
+ text-align: center;
+}
+
+div.sphinxsidebarwrapper span.logo {
+ display: block;
+ text-align: center;
+ font-family: Georgia, serif;
+ padding: 0;
+ font-size: 180%;
+}
+
+div.sphinxsidebar a {
+ border-width: 0;
+}
+
+div.sphinxsidebar li {
+ color: #444;
+}
+
+div.section {
+ margin-top: 2.5em;
+}
+
+a.reference {
+ border-bottom: none;
+}
+
+code.xref {
+ font-weight: normal;
+ background-color: #F8F8F8;
+ padding: 0.1em 0 0.1em 0;
+}
+
+div.section > dl.c > dt:first-child,
+div.section > dl.cpp > dt:first-child {
+ background-color: #F8F8F8;
+ font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
+ font-size: 0.9em;
+ font-weight: normal;
+ margin-bottom: 0.5em;
+ padding: 0.1em 0 0.1em 0;
+}
+
+tt.descname, tt.descclassname, code.descname, code.descclassname {
+ font-size: 0.9em;
+}
+
+dl.member {
+ margin-top: 0.5em;
+}
+
+dl.enumerator {
+ margin-top: 0.5em;
+}
+
+dl.field-list > dt {
+ padding-left: 0;
+}
+
+pre, tt, code {
+ background-color: #F8F8F8;
+}
+
+.toctree-l1 {
+ margin-top: 1.0em;
+}
+
+img.logo {
+ width: 6em;
+}
+
+.class {
+ padding-top: 1.5em;
+}
+
+.exception {
+ padding-top: 1.5em;
+}
+
+.class > dd > dl.function {
+ padding-top: 1.0em;
+}
diff --git a/doc/_templates/about.html b/doc/_templates/about.html
new file mode 100644
index 00000000..5bbadbe9
--- /dev/null
+++ b/doc/_templates/about.html
@@ -0,0 +1,57 @@
+{% if theme_logo %}
+<p class="logo">
+ <a href="{{ pathto(master_doc) }}">
+ <img class="logo" src="{{ pathto('_static/' ~ theme_logo, 1) }}" alt="Logo"/>
+ {% if theme_logo_name|lower == 'true' %}
+ <span class="logo logo-name">{{ project }}</span>
+ {% endif %}
+ </a>
+</p>
+{% else %}
+<h1 class="logo"><a href="{{ pathto(master_doc) }}">{{ project }}</a></h1>
+{% endif %}
+
+{% if theme_description %}
+<p class="blurb">{{ theme_description }}</p>
+{% endif %}
+
+{% if theme_github_user and theme_github_repo %}
+{% if theme_github_button|lower == 'true' %}
+<p>
+<iframe src="https://ghbtns.com/github-btn.html?user={{ theme_github_user }}&repo={{ theme_github_repo }}&type={{ theme_github_type }}&count={{ theme_github_count }}&size=large&v=2"
+ allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px"></iframe>
+</p>
+{% endif %}
+{% endif %}
+
+{% if theme_travis_button|lower != 'false' %}
+{% if theme_travis_button|lower == 'true' %}
+ {% set path = theme_github_user + '/' + theme_github_repo %}
+{% else %}
+ {% set path = theme_travis_button %}
+{% endif %}
+<p>
+<a class="badge" href="https://travis-ci.org/{{ path }}">
+ <img
+ alt="https://secure.travis-ci.org/{{ path }}.svg?branch={{ theme_badge_branch }}"
+ src="https://secure.travis-ci.org/{{ path }}.svg?branch={{ theme_badge_branch }}"
+ />
+</a>
+</p>
+{% endif %}
+
+{% if theme_codecov_button|lower != 'false' %}
+{% if theme_codecov_button|lower == 'true' %}
+ {% set path = theme_github_user + '/' + theme_github_repo %}
+{% else %}
+ {% set path = theme_codecov_button %}
+{% endif %}
+<p>
+<a class="badge" href="https://codecov.io/github/{{ path }}">
+ <img
+ alt="https://codecov.io/github/{{ path }}/coverage.svg?branch={{ theme_badge_branch }}"
+ src="https://codecov.io/github/{{ path }}/coverage.svg?branch={{ theme_badge_branch }}"
+ />
+</a>
+</p>
+{% endif %}
diff --git a/doc/c/Doxyfile b/doc/c/Doxyfile
new file mode 100644
index 00000000..a88f8ce8
--- /dev/null
+++ b/doc/c/Doxyfile
@@ -0,0 +1,33 @@
+PROJECT_NAME = Serd
+PROJECT_BRIEF = "A lightweight library for RDF storage and serialisation"
+
+QUIET = YES
+WARN_AS_ERROR = NO
+WARN_IF_UNDOCUMENTED = NO
+WARN_NO_PARAMDOC = NO
+
+JAVADOC_AUTOBRIEF = YES
+
+CASE_SENSE_NAMES = YES
+HIDE_IN_BODY_DOCS = YES
+REFERENCES_LINK_SOURCE = NO
+
+GENERATE_HTML = NO
+GENERATE_LATEX = NO
+GENERATE_XML = YES
+XML_PROGRAMLISTING = NO
+SHOW_FILES = NO
+
+MACRO_EXPANSION = YES
+PREDEFINED = SERD_ALLOCATED \
+ SERD_API \
+ SERD_CONST_FUNC= \
+ SERD_DEPRECATED_BY(x)= \
+ SERD_DISABLE_DEPRECATED \
+ SERD_NONNULL= \
+ SERD_NULLABLE= \
+ SERD_PURE_FUNC=
+
+INPUT = ../../include/serd/serd.h
+
+OUTPUT_DIRECTORY = .
diff --git a/doc/c/index.rst b/doc/c/index.rst
new file mode 100644
index 00000000..3fe14ca6
--- /dev/null
+++ b/doc/c/index.rst
@@ -0,0 +1,5 @@
+.. toctree::
+
+ serd
+ overview
+ reference
diff --git a/doc/c/overview.rst b/doc/c/overview.rst
new file mode 100644
index 00000000..95862368
--- /dev/null
+++ b/doc/c/overview.rst
@@ -0,0 +1,22 @@
+########
+Overview
+########
+
+.. default-domain:: c
+.. highlight:: c
+
+The API revolves around two main types: the :doc:`api/reader`,
+which reads text and fires callbacks,
+and the :doc:`api/writer`,
+which writes text when driven by corresponding functions.
+Both work in a streaming fashion but still support pretty-printing,
+so the pair can be used to pretty-print, translate,
+or otherwise process arbitrarily large documents very quickly.
+The context of a stream is tracked by the :doc:`api/env`,
+which stores the current base URI and set of namespace prefixes.
+
+The complete API is declared in ``serd.h``:
+
+.. code-block:: c
+
+ #include <serd/serd.h>
diff --git a/doc/c/reference.rst b/doc/c/reference.rst
new file mode 100644
index 00000000..70de0603
--- /dev/null
+++ b/doc/c/reference.rst
@@ -0,0 +1,11 @@
+#############
+API Reference
+#############
+
+This section contains the generated documentation for all symbols in the public
+API.
+
+.. toctree::
+
+ api/serd
+
diff --git a/doc/c/wscript b/doc/c/wscript
new file mode 100644
index 00000000..1132f197
--- /dev/null
+++ b/doc/c/wscript
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+
+def build(bld):
+ dox_to_sphinx = bld.path.find_node("../../scripts/dox_to_sphinx.py")
+ index_xml = bld.path.get_bld().make_node("xml/index.xml")
+
+ files = [
+ ("../../resources/serd.svg", "sphinx/_static/serd.svg"),
+ ("../_static/custom.css", "sphinx/_static/custom.css"),
+ ("../_templates/about.html", "sphinx/_templates/about.html"),
+ ("../serd.rst", "sphinx/serd.rst"),
+ ("index.rst", "sphinx/index.rst"),
+ ("overview.rst", "sphinx/overview.rst"),
+ ("reference.rst", "sphinx/reference.rst"),
+ ]
+
+ # Run Doxygen to generate XML documentation
+ bld(features="doxygen", doxyfile="Doxyfile")
+
+ # Substitute variables to make Sphinx configuration file
+ bld(features="subst",
+ source="../conf.py.in",
+ target="sphinx/conf.py",
+ SERD_VERSION=bld.env.SERD_VERSION)
+
+ # Copy static documentation files to Sphinx build directory
+ for f in files:
+ bld(features="subst", is_copy=True, source=f[0], target=f[1])
+
+ # Generate Sphinx markup from Doxygen XML
+ bld.add_group()
+ bld(rule="${PYTHON} " + dox_to_sphinx.abspath() + " -f ${SRC} ${TGT}",
+ source=index_xml,
+ target="sphinx/api/")
+
+ # Run Sphinx to generate HTML documentation
+ doc_dir = bld.env.DOCDIR + "/serd-%s/" % bld.env.SERD_MAJOR_VERSION
+ bld(features="sphinx",
+ sphinx_source=bld.path.get_bld().make_node("sphinx"),
+ sphinx_output_format="singlehtml",
+ sphinx_options=["-E", "-q"],
+ install_path=doc_dir + "c/singlehtml/")
diff --git a/doc/conf.py.in b/doc/conf.py.in
new file mode 100644
index 00000000..224b6e77
--- /dev/null
+++ b/doc/conf.py.in
@@ -0,0 +1,86 @@
+# Project information
+
+project = "Serd"
+copyright = "2020, David Robillard"
+author = "David Robillard"
+release = "@SERD_VERSION@"
+
+# General configuration
+
+language = "en"
+
+extensions = [
+ # 'sphinx_rtd_theme',
+]
+
+# Enable nitpicky mode to get warnings about broken links
+# Unfortunately this means we need to explicitly ignore everything external
+nitpicky = True
+_opaque = [
+ "FILE",
+ "SerdByteSinkImpl",
+ "SerdByteSourceImpl",
+ "SerdCursorImpl",
+ "SerdEnvImpl",
+ "SerdIterImpl",
+ "SerdModelImpl",
+ "SerdNodeImpl",
+ "SerdNodesImpl",
+ "SerdRangeImpl",
+ "SerdReaderImpl",
+ "SerdSinkImpl",
+ "SerdStatementImpl",
+ "SerdWorldImpl",
+ "SerdWriterImpl",
+ "int64_t",
+ "size_t",
+ "uint32_t",
+ "uint8_t",
+ "uintptr_t",
+ "va_list",
+]
+
+nitpick_ignore = list(map(lambda x: ("c:identifier", x), _opaque))
+
+templates_path = ["_templates"]
+
+pygments_style = "friendly"
+
+# HTML output
+
+exclude_patterns = ["xml"]
+html_static_path = ["_static"]
+
+html_theme = "alabaster"
+# html_theme = "sphinx_rtd_theme"
+
+if html_theme == "alabaster":
+
+ html_theme_options = {
+ "description": "A lightweight library for RDF storage and serialisation",
+ "donate_url": "http://drobilla.net/pages/donate.html",
+ # "github_repo": "serd",
+ # "github_user": "lv2",
+ "logo": "serd.svg",
+ "logo_name": True,
+ "logo_text_align": "center",
+ "page_width": "80em - 20em",
+ "sidebar_width": "24em",
+ }
+
+ html_sidebars = {
+ "**": [
+ "about.html",
+ "localtoc.html",
+ "donate.html",
+ ]
+ }
+
+elif html_theme == "sphinx_rtd_theme":
+
+ html_theme_options = {
+ "sticky_navigation": False,
+ "collapse_navigation": False,
+ "navigation_depth": 4,
+ "display_version": True,
+ }
diff --git a/doc/footer.html b/doc/footer.html
deleted file mode 100644
index 0dc69197..00000000
--- a/doc/footer.html
+++ /dev/null
@@ -1,20 +0,0 @@
-<!-- HTML footer for doxygen 1.8.15-->
-<!-- start footer part -->
-<!--BEGIN GENERATE_TREEVIEW-->
-<div id="nav-path" class="navpath"><!-- id is needed for treeview function! -->
- <ul>
- $navpath
- <li class="footer">$generatedby
- <a href="http://www.doxygen.org/index.html">Doxygen $doxygenversion</li>
- </ul>
-</div>
-<!--END GENERATE_TREEVIEW-->
-<!--BEGIN !GENERATE_TREEVIEW-->
-<div id="footer">
- <address class="footer">$generatedby
- <a href="http://www.doxygen.org/">Doxygen</a> $doxygenversion
- </address>
-</div>
-<!--END !GENERATE_TREEVIEW-->
-</body>
-</html>
diff --git a/doc/header.html b/doc/header.html
deleted file mode 100644
index b907bc8b..00000000
--- a/doc/header.html
+++ /dev/null
@@ -1,46 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml">
- <head>
- <meta http-equiv="Content-Type" content="text/xhtml;charset=UTF-8"/>
- <!--BEGIN PROJECT_NAME--><title>$projectname: $title</title><!--END PROJECT_NAME-->
- <!--BEGIN !PROJECT_NAME--><title>$title</title><!--END !PROJECT_NAME-->
- <link href="$relpath^$stylesheet" rel="stylesheet" type="text/css" />
- $extrastylesheet
- </head>
- <body>
- <div id="top"><!-- do not remove this div, it is closed by doxygen! -->
-
- <!--BEGIN TITLEAREA-->
- <div id="titlearea">
- <div id="header">
- <div id="titlebox">
- <!--BEGIN PROJECT_LOGO-->
- <div id="projectlogo"><img alt="Logo" src="$relpath^$projectlogo"/></div>
- <!--END PROJECT_LOGO-->
- <!--BEGIN PROJECT_NAME-->
- <h1 id="title">$projectname</h1>
- <!--END PROJECT_NAME-->
- <!--BEGIN PROJECT_BRIEF-->
- <div id="shortdesc">$projectbrief</div>
- <!--END PROJECT_BRIEF-->
- </div>
- <div id="metabox">
- <table id="meta">
- <!--BEGIN PROJECT_NUMBER-->
- <tr><th>Version</th><td>$projectnumber</td></tr>
- <!--END PROJECT_NUMBER-->
- </table>
- </div>
- </div>
- </div>
- <!--END TITLEAREA-->
- <!-- end header part -->
-
- <!-- Fake static menu from Doxygen 1.8.15 -->
- <div id="staticnavrow" class="tabs">
- <ul class="tablist">
- <li><a href="index.html"><span>Main&#160;Page</span></a></li>
- <li><a href="modules.html"><span>Modules</span></a></li>
- <li><a href="annotated.html"><span>Data&#160;Structures</span></a></li>
- </ul>
- </div>
diff --git a/doc/index.html.in b/doc/index.html.in
deleted file mode 100644
index 55d2d1e7..00000000
--- a/doc/index.html.in
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml">
- <head>
- <meta http-equiv="Content-Type" content="text/xhtml;charset=UTF-8"/>
- <title>Serd</title>
- <link href="doc/html/style.css" rel="stylesheet" type="text/css" />
- </head>
- <body>
- <div id="top">
- <div id="titlearea">
- <div id="header">
- <div id="titlebox">
- <h1 id="title">Serd</h1>
- <div id="shortdesc">A lightweight library for RDF storage and serialisation</div>
- </div>
- <div id="metabox">
- <table id="meta">
- <tr><th>Version</th><td>@SERD_VERSION@</td></tr>
- </table>
- </div>
- </div>
- </div>
-
- <div id="navrow1" class="tabs" />
- <ul class="tablist" />
- </div>
-
- <ul>
- <li><a href="doc/html/index.html">API Documentation</a></li>
- <li><a href="coverage/index.html">Test Coverage</a></li>
- </ul>
-
- <div id="footer" />
- </body>
-</html>
diff --git a/doc/layout.xml b/doc/layout.xml
deleted file mode 100644
index 4744245e..00000000
--- a/doc/layout.xml
+++ /dev/null
@@ -1,194 +0,0 @@
-<doxygenlayout version="1.0">
- <!-- Generated by doxygen 1.8.13 -->
- <!-- Navigation index tabs for HTML output -->
- <navindex>
- <tab type="mainpage" visible="yes" title=""/>
- <tab type="pages" visible="yes" title="" intro=""/>
- <tab type="modules" visible="yes" title="" intro=""/>
- <tab type="namespaces" visible="yes" title="">
- <tab type="namespacelist" visible="yes" title="" intro=""/>
- <tab type="namespacemembers" visible="yes" title="" intro=""/>
- </tab>
- <tab type="classes" visible="yes" title="">
- <tab type="classlist" visible="yes" title="" intro=""/>
- <tab type="classindex" visible="$ALPHABETICAL_INDEX" title=""/>
- <tab type="hierarchy" visible="yes" title="" intro=""/>
- <tab type="classmembers" visible="yes" title="" intro=""/>
- </tab>
- <tab type="files" visible="yes" title="">
- <tab type="filelist" visible="yes" title="" intro=""/>
- <tab type="globals" visible="yes" title="" intro=""/>
- </tab>
- <tab type="examples" visible="yes" title="" intro=""/>
- </navindex>
-
- <!-- Layout definition for a class page -->
- <class>
- <briefdescription visible="yes"/>
- <detaileddescription title=""/>
- <includes visible="$SHOW_INCLUDE_FILES"/>
- <inheritancegraph visible="$CLASS_GRAPH"/>
- <collaborationgraph visible="$COLLABORATION_GRAPH"/>
- <memberdecl>
- <nestedclasses visible="yes" title=""/>
- <publictypes title=""/>
- <services title=""/>
- <interfaces title=""/>
- <publicslots title=""/>
- <signals title=""/>
- <publicmethods title=""/>
- <publicstaticmethods title=""/>
- <publicattributes title=""/>
- <publicstaticattributes title=""/>
- <protectedtypes title=""/>
- <protectedslots title=""/>
- <protectedmethods title=""/>
- <protectedstaticmethods title=""/>
- <protectedattributes title=""/>
- <protectedstaticattributes title=""/>
- <packagetypes title=""/>
- <packagemethods title=""/>
- <packagestaticmethods title=""/>
- <packageattributes title=""/>
- <packagestaticattributes title=""/>
- <properties title=""/>
- <events title=""/>
- <privatetypes title=""/>
- <privateslots title=""/>
- <privatemethods title=""/>
- <privatestaticmethods title=""/>
- <privateattributes title=""/>
- <privatestaticattributes title=""/>
- <friends title=""/>
- <related title="" subtitle=""/>
- <membergroups visible="yes"/>
- </memberdecl>
- <memberdef>
- <inlineclasses title=""/>
- <typedefs title=""/>
- <enums title=""/>
- <services title=""/>
- <interfaces title=""/>
- <constructors title=""/>
- <functions title=""/>
- <related title=""/>
- <variables title=""/>
- <properties title=""/>
- <events title=""/>
- </memberdef>
- <allmemberslink visible="yes"/>
- <usedfiles visible="$SHOW_USED_FILES"/>
- <authorsection visible="yes"/>
- </class>
-
- <!-- Layout definition for a namespace page -->
- <namespace>
- <briefdescription visible="yes"/>
- <detaileddescription title=""/>
- <memberdecl>
- <nestednamespaces visible="yes" title=""/>
- <constantgroups visible="yes" title=""/>
- <classes visible="yes" title=""/>
- <typedefs title=""/>
- <enums title=""/>
- <functions title=""/>
- <variables title=""/>
- <membergroups visible="yes"/>
- </memberdecl>
- <memberdef>
- <inlineclasses title=""/>
- <typedefs title=""/>
- <enums title=""/>
- <functions title=""/>
- <variables title=""/>
- </memberdef>
- <authorsection visible="yes"/>
- </namespace>
-
- <!-- Layout definition for a file page -->
- <file>
- <briefdescription visible="yes"/>
- <includes visible="$SHOW_INCLUDE_FILES"/>
- <includegraph visible="$INCLUDE_GRAPH"/>
- <includedbygraph visible="$INCLUDED_BY_GRAPH"/>
- <sourcelink visible="yes"/>
- <memberdecl>
- <classes visible="yes" title=""/>
- <namespaces visible="yes" title=""/>
- <constantgroups visible="yes" title=""/>
- <defines title=""/>
- <typedefs title=""/>
- <enums title=""/>
- <functions title=""/>
- <variables title=""/>
- <membergroups visible="yes"/>
- </memberdecl>
- <detaileddescription title=""/>
- <memberdef>
- <inlineclasses title=""/>
- <defines title=""/>
- <typedefs title=""/>
- <enums title=""/>
- <functions title=""/>
- <variables title=""/>
- </memberdef>
- <authorsection/>
- </file>
-
- <!-- Layout definition for a group page -->
- <group>
- <briefdescription visible="yes"/>
- <detaileddescription title=""/>
- <groupgraph visible="$GROUP_GRAPHS"/>
- <memberdecl>
- <nestedgroups visible="yes" title=""/>
- <dirs visible="yes" title=""/>
- <files visible="yes" title=""/>
- <namespaces visible="yes" title=""/>
- <classes visible="yes" title=""/>
- <defines title=""/>
- <typedefs title=""/>
- <enums title=""/>
- <enumvalues title=""/>
- <functions title=""/>
- <variables title=""/>
- <signals title=""/>
- <publicslots title=""/>
- <protectedslots title=""/>
- <privateslots title=""/>
- <events title=""/>
- <properties title=""/>
- <friends title=""/>
- <membergroups visible="yes"/>
- </memberdecl>
- <memberdef>
- <pagedocs/>
- <inlineclasses title=""/>
- <defines title=""/>
- <typedefs title=""/>
- <enums title=""/>
- <enumvalues title=""/>
- <functions title=""/>
- <variables title=""/>
- <signals title=""/>
- <publicslots title=""/>
- <protectedslots title=""/>
- <privateslots title=""/>
- <events title=""/>
- <properties title=""/>
- <friends title=""/>
- </memberdef>
- <authorsection visible="yes"/>
- </group>
-
- <!-- Layout definition for a directory page -->
- <directory>
- <briefdescription visible="yes"/>
- <directorygraph visible="yes"/>
- <memberdecl>
- <dirs visible="yes"/>
- <files visible="yes"/>
- </memberdecl>
- <detaileddescription title=""/>
- </directory>
-</doxygenlayout>
diff --git a/doc/mainpage.md b/doc/mainpage.md
deleted file mode 100644
index 33ba7ec0..00000000
--- a/doc/mainpage.md
+++ /dev/null
@@ -1,15 +0,0 @@
-Serd is a lightweight and dependency-free C library for RDF serialisation which
-supports [Turtle], [NTriples], [NQuads], and [TriG].
-
-The complete API is documented in the [serd](@ref serd) group. It revolves
-around two main types: @ref SerdReader, which reads text and fires callbacks,
-and @ref SerdWriter, which writes text when driven by corresponding functions.
-Both work in a streaming fashion but still support pretty-printing, so the pair
-can be used to pretty-print, translate, or otherwise process arbitrarily large
-documents very quickly. The stream context is maintained by @ref SerdEnv,
-which stores the current base URI and set of namespace prefixes.
-
-[Turtle]: http://www.w3.org/TR/turtle/
-[NTriples]: http://www.w3.org/TR/n-triples/
-[NQuads]: http://www.w3.org/TR/n-quads/
-[TriG]: http://www.w3.org/TR/trig/
diff --git a/doc/serd.rst b/doc/serd.rst
new file mode 100644
index 00000000..e90a8368
--- /dev/null
+++ b/doc/serd.rst
@@ -0,0 +1,11 @@
+####
+Serd
+####
+
+Serd is a lightweight and dependency-free C library for RDF serialisation which
+supports Turtle_, NTriples_, NQuads_, and TriG_.
+
+.. _Turtle: http://www.w3.org/TR/turtle/
+.. _NTriples: http://www.w3.org/TR/n-triples/
+.. _NQuads: http://www.w3.org/TR/n-quads/
+.. _TriG: http://www.w3.org/TR/trig/
diff --git a/doc/style.css b/doc/style.css
deleted file mode 100644
index 2092dab0..00000000
--- a/doc/style.css
+++ /dev/null
@@ -1,808 +0,0 @@
-body {
- background: #FFF;
- color: #222;
- font-style: normal;
- line-height: 1.6em;
- margin-left: auto;
- margin-right: auto;
- padding: 1em;
- max-width: 60em;
- font-family: "SF Pro Text", Verdana, "DejaVu Sans", sans-serif;
- text-rendering: optimizeLegibility;
-}
-
-h1 {
- font-size: 1.68em;
- font-weight: 500;
- font-family: Helvetica, Arial, "DejaVu Sans Condensed", Verdana, sans-serif;
- line-height: 2em;
- margin: 0 0 0.25em 0;
-}
-
-h2 {
- line-height: 1.68em;
- font-size: 1.41em;
- font-weight: 600;
- font-family: Helvetica, Arial, "DejaVu Sans Condensed", Verdana, sans-serif;
- margin: 1.25em 0 0.5em 0;
-}
-
-h3 {
- line-height: 1.41em;
- font-size: 1.18em;
- font-weight: 600;
- font-family: Helvetica, Arial, "DejaVu Sans Condensed", Verdana, sans-serif;
- margin: 1.25em 0 0.5em 0;
-}
-
-h4 {
- line-height: 1.18em;
- font-size: 1em;
- font-weight: 600;
- font-family: Helvetica, Arial, "DejaVu Sans Condensed", Verdana, sans-serif;
- margin: 1.25em 0 0.5em 0;
-}
-
-h5, h6 {
- font-size: 0.7em;
- font-weight: 600;
- font-family: Helvetica, Arial, "DejaVu Sans Condensed", Verdana, sans-serif;
- margin: 1.25em 0 0.5em 0;
-}
-
-a {
- color: #546E00;
- text-decoration: none;
-}
-
-h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
- color: #444;
-}
-
-a:hover {
- text-decoration: underline;
-}
-
-h1 a:link, h2 a:link, h3 a:link, h4 a:link, h5 a:link, h6 a:link {
- color: #444;
-}
-
-h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {
- color: #444;
-}
-
-p {
- margin: 0.5em 0 0.5em 0;
-}
-
-dt {
- font-weight: 600;
-}
-
-dd {
- margin-left: 2em;
-}
-
-caption {
- font-weight: 700;
-}
-
-.title, #projectname {
- line-height: 1.0125em;
- margin: 0.75em 0 0 0;
-}
-
-.titlearea .header .titlebox, #projectname {
- font-size: 1.68em;
- font-weight: 400;
- margin-bottom: 0.25em;
- margin-top: 0;
-}
-
-#header {
- padding: 0 0 0.5em 0;
- border-bottom: 1px solid #EEE;
-}
-
-.header .headertitle .title {
- line-height: 1.68em;
- font-size: 1.68em;
- font-weight: 600;
- font-family: Helvetica, Arial, "DejaVu Sans Condensed", Verdana, sans-serif;
-}
-
-.ingroups {
- display: none;
-}
-
-.title .ingroups a {
- font-size: small;
- margin-left: 1em;
-}
-
-#titlebox, #metabox {
- display: inline-block;
-}
-
-#titlebox {
- display: inline-block;
- width: 75%;
- left: 0;
- top: 0;
-}
-
-#title {
- margin-bottom: 0.25em;
- line-height: 1.25em;
- font-size: 2.5em;
- color: #333;
- font-weight: 600;
-}
-
-.PageDoc {
- margin-top: 1.5em;
-}
-
-.PageDoc .header .headertitle .title {
- display: none;
-}
-
-#shortdesc {
- margin: 0;
- color: #666;
- display: inline-block;
- font-style: italic;
- font-family: Helvetica, Arial, "DejaVu Sans Condensed", Verdana, sans-serif;
- padding: 0;
-}
-
-#titlearea {
- margin: 0.25em auto 0 auto;
- padding: 0;
- position: relative;
- clear: both;
- line-height: 1em;
-}
-
-.legend {
- font-size: small;
- text-align: center;
-}
-
-.version {
- font-size: small;
- text-align: center;
-}
-
-div.qindex,div.navtab {
- background-color: #EBEFF6;
- border: 1px solid #A3B4D7;
- text-align: center;
- margin: 2px;
- padding: 2px;
-}
-
-div.navtab {
- margin-right: 15px;
-}
-
-.contents a:visited {
- color: #344E00;
-}
-
-a.qindexHL {
- background-color: #9CAFD4;
- color: #FFF;
- border: 1px double #869DCA;
-}
-
-code {
- color: #444;
- font-family: "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace, fixed;
-}
-
-dl.el {
- margin-left: -1cm;
-}
-
-.fragment {
- font-family: "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace, fixed;
-}
-
-pre.fragment {
- border: 1px solid #C4C4C4;
- background-color: #F9F9F9;
- padding: 0.5em;
- overflow: auto;
-}
-
-div.ah {
- background-color: #000;
- font-weight: 700;
- color: #FFF;
- margin-bottom: 3px;
- margin-top: 3px;
- padding: 0.2em;
- border: thin solid #333;
-}
-
-div.groupHeader {
- margin-left: 16px;
- margin-top: 12px;
- margin-bottom: 6px;
- font-weight: 700;
-}
-
-a + h2.groupheader {
- display: none;
-}
-
-div.groupText {
- margin-left: 16px;
- font-style: italic;
-}
-
-div.contents, #content {
- max-width: 60em;
- margin-left: auto;
- margin-right: auto;
-}
-
-.groupheader + p {
- font-style: italic;
- color: #666;
- margin: 0 0 1em 0;
-}
-
-td.indexkey {
- background-color: #EBEFF6;
- font-weight: 700;
- border: 1px solid #C4CFE5;
- margin: 2px 0;
- padding: 2px 10px;
-}
-
-td.indexvalue {
- background-color: #EBEFF6;
- border: 1px solid #C4CFE5;
- padding: 2px 10px;
- margin: 2px 0;
-}
-
-table.memname {
- font-family: "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace, fixed;
- border-spacing: 0;
-}
-
-table.memname tbody tr:last-child {
- display: none;
-}
-
-table.memname tbody tr:only-child {
- display: table-cell;
-}
-
-table.memname tbody tr:nth-last-child(2)::after {
- content: ")";
-}
-
-tr.memlist {
- background-color: #EEF1F7;
-}
-
-p.formulaDsp {
- text-align: center;
-}
-
-img.formulaInl {
- vertical-align: middle;
-}
-
-div.center {
- text-align: center;
- margin-top: 0;
- margin-bottom: 0;
- padding: 0;
-}
-
-div.center img {
- border: 0;
-}
-
-address.footer {
- text-align: right;
-}
-
-img.footer {
- border: 0;
- vertical-align: middle;
-}
-
-span.keyword {
- color: #586E75;
-}
-
-span.keywordtype {
- color: #546E00;
-}
-
-span.keywordflow {
- color: #586E75;
-}
-
-span.comment {
- color: #6C71C4;
-}
-
-span.preprocessor {
- color: #D33682;
-}
-
-span.stringliteral {
- color: #CB4B16;
-}
-
-span.charliteral {
- color: #CB4B16;
-}
-
-td.tiny {
- font-size: x-small;
-}
-
-.dirtab {
- padding: 4px;
- border-collapse: collapse;
- border: 1px solid #A3B4D7;
-}
-
-th.dirtab {
- background: #EBEFF6;
- font-weight: 700;
-}
-
-hr {
- height: 0;
- border: none;
- border-top: 1px solid #DDD;
- margin: 2em 0;
-}
-
-#footer {
- bottom: 0;
- clear: both;
- font-size: x-small;
- margin: 2em 0 0;
- padding: 0 1em 1em 1em;
- vertical-align: top;
- color: #888;
-}
-
-td.ititle {
- padding-bottom: 0.75em;
-}
-
-table.memberdecls {
- border-spacing: 0.125em;
- line-height: 1.3em;
-}
-
-.mdescLeft,.mdescRight,.memItemLeft,.memItemRight,.memTemplItemLeft,.memTemplItemRight,.memTemplParams {
- margin: 0;
- padding: 0;
-}
-
-.mdescLeft,.mdescRight {
- color: #555;
-}
-
-.memItemLeft,.memItemRight,.memTemplParams {
- border: 0;
- font-family: "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace, fixed;
-}
-
-.memItemLeft,.memTemplItemLeft {
- white-space: nowrap;
- padding-left: 2em;
-}
-
-.memItemLeft a.el {
- font-weight: bold;
-}
-
-.memTemplParams {
- color: #464646;
- white-space: nowrap;
-}
-
-td.memSeparator {
- display: none;
-}
-
-td.mlabels-right {
- color: #B4C342;
- font-weight: normal;
- margin-left: 1em;
-}
-
-.memtitle {
- display: none;
-}
-
-.memtemplate {
- color: #888;
- font-style: italic;
- font-family: "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace, fixed;
- font-size: small;
-}
-
-.memnav {
- background-color: #EEE;
- border: 1px solid #B4C342;
- text-align: center;
- margin: 2px;
- margin-right: 15px;
- padding: 2px;
-}
-
-.memitem {
- padding: 0.5em 0.5em 0.25em 0.5em;
- margin: 1em 0 2em 0;
-}
-
-.memproto {
- border-bottom: 1px solid #EEE;
- font-family: "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace, fixed;
- font-size: 1.09em;
- font-weight: 600;
- line-height: 1.41em;
- margin-bottom: 0.25em;
- padding-bottom: 0.125em;
-}
-
-.memproto .paramname {
- font-style: normal;
- padding-right: 0.25em;
-}
-
-.memdoc {
- padding: 0;
-}
-
-.memdoc > p:first-child, .memdoc .textblock > p:first-child {
- font-style: italic;
- color: #444;
- margin-bottom: 0.75em;
-}
-
-.paramkey {
- text-align: right;
-}
-
-.paramtype {
- color: #666;
- padding: 0 0.25em 0 0.25em;
- white-space: nowrap;
-}
-
-.params .paramname {
- color: #111;
- white-space: nowrap;
- font-family: "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace, fixed;
- font-style: italic;
- padding-right: 0.5em;
- vertical-align: top;
-}
-
-.fieldname {
- color: #000;
-}
-
-.fieldtable {
- margin-top: 1.0em;
- border-collapse: collapse;
-}
-
-.fieldtable tbody tr:first-child {
- display: none;
-}
-
-td.fieldname {
- vertical-align: top;
- font-family: "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace, fixed;
-}
-
-td.fielddoc {
- padding: 0.125em 0.5em 0 0.25em;
- vertical-align: top;
-}
-
-.fieldtable tbody tr td {
- border-top: 1px dashed #DDD;
- border-bottom: 1px dashed #DDD;
-}
-
-td.fieldtype {
- color: #666;
- padding: 0 0.5em 0 0;
- vertical-align: top;
- font-family: "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace, fixed;
-}
-
-td.fielddoc p {
- margin: 0;
- padding: 0 0.5em 0 0;
-}
-
-p.reference {
- font-size: x-small;
- font-style: italic;
-}
-
-.ftvtree {
- font-family: "DejaVu Sans", Verdana, Helvetica, Arial, sans-serif;
- margin: 0;
-}
-
-.directory {
- margin: 0.5em;
-}
-
-.directory h3 {
- margin: 0;
- margin-top: 1em;
- font-size: 11pt;
-}
-
-.directory > h3 {
- margin-top: 0;
-}
-
-.directory p {
- margin: 0;
- white-space: nowrap;
-}
-
-.directory div {
- display: none;
- margin: 0;
-}
-
-.directory img {
- vertical-align: -30%;
-}
-
-td.entry {
- font-family: "DejaVu Sans", Verdana, Helvetica, Arial, sans-serif;
- font-weight: 400;
- padding-right: 1em;
-}
-
-.arrow {
- color: #CCC;
- user-select: none;
- font-size: 80%;
- display: inline-block;
- width: 16px;
- height: 22px;
- vertical-align: top;
-}
-
-td.entry b {
- font-family: "DejaVu Sans", Verdana, Helvetica, Arial, sans-serif;
- font-weight: 400;
- font-size: 130%;
-}
-
-.directory-alt {
- font-size: 100%;
- font-weight: bold;
-}
-
-.directory-alt h3 {
- margin: 0;
- margin-top: 1em;
- font-size: 11pt;
-}
-
-.directory-alt > h3 {
- margin-top: 0;
-}
-
-.directory-alt p {
- margin: 0;
- white-space: nowrap;
-}
-
-.directory-alt div {
- display: none;
- margin: 0;
-}
-
-.directory-alt img {
- vertical-align: -30%;
-}
-
-div.dynheader {
- margin-top: 8px;
-}
-
-address {
- font-style: normal;
- color: #444;
-}
-
-table.doxtable {
- border-collapse: collapse;
- margin: 0.5em;
-}
-
-table.doxtable td,table.doxtable th {
- border: 1px solid #DDD;
- padding: 3px 7px 2px;
-}
-
-table.doxtable th {
- background-color: #F3F3F3;
- color: #000;
- padding-bottom: 4px;
- padding-top: 5px;
- text-align: left;
- font-weight: bold;
-}
-
-.tabsearch {
- top: 0;
- left: 10px;
- height: 36px;
- z-index: 101;
- overflow: hidden;
- font-size: 13px;
-}
-
-div.navpath {
- color: #DDD;
-}
-
-.navpath ul {
- overflow: hidden;
- margin: 0;
- padding: 0;
-}
-
-.navpath li {
- float: left;
- padding-left: 0;
- margin-left: 0.5em;
- padding-right: 1em;
-}
-
-.navpath a {
- display: block;
- text-decoration: none;
- outline: none;
-}
-
-div.summary {
- font-size: small;
- font-family: "DejaVu Sans", Verdana, Helvetica, Arial, sans-serif;
- margin: 0;
- padding: 0.25em 0;
- display: none;
-}
-
-div.summary a {
- white-space: nowrap;
-}
-
-#metabox {
- display: inline-block;
- font-size: x-small;
- font-family: Helvetica, Arial, "DejaVu Sans Condensed", Verdana, sans-serif;
- position: absolute;
- right: 0;
- bottom: 0.25em;
- color: #666;
- font-style: italic;
-}
-
-#meta {
- border-style: hidden;
- margin-right: 0.25em;
-}
-
-#meta tr, #meta th, #meta td {
- background-color: transparent;
- border: 0;
- margin: 0;
- font-weight: normal;
-}
-
-#meta th {
- text-align: right;
-}
-
-#meta th::after {
- content: ":";
-}
-
-div.line {
- font-family: "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace, fixed;
- line-height: 1.4em;
- white-space: pre-wrap;
-}
-
-.glow {
- background-color: #2AA198;
- box-shadow: 0 0 10px #2AA198;
-}
-
-span.lineno {
- padding-right: 4px;
- text-align: right;
- border-right: 2px solid #546E00;
- background-color: #E8E8E8;
- white-space: pre;
-}
-
-span.lineno a {
- background-color: #D8D8D8;
-}
-
-span.lineno a:hover {
- background-color: #C8C8C8;
-}
-
-.tabs, .tabs2, .navpath {
- padding: 0.25em 0;
- font-size: small;
- font-family: Helvetica, Arial, "DejaVu Sans Condensed", Verdana, sans-serif;
- margin: 0;
-}
-
-th {
- text-align: left;
- font-size: 110%;
- font-weight: 500;
-}
-
-.mlabel {
- padding: 0.125em;
-}
-
-#navrow1, #navrow2 {
- /* Disable menu from Doxygen 1.8.15, it is faked in the template */
- display: none;
-}
-
-.tablist {
- margin: 0;
- padding: 0;
- display: table;
-}
-
-.tablist li {
- display: table-cell;
- line-height: 2em;
- list-style: none;
- border-bottom: 0;
-}
-
-.tablist a {
- display: block;
- padding: 0 1em 0 0;
- text-decoration: none;
- outline: none;
-}
-
-.tabs3 .tablist a {
- padding: 0 10px;
-}
-
-.tablist li.current a {
- color: #222;
-}
-
-span.icon {
- display: none;
-}
diff --git a/include/serd/serd.h b/include/serd/serd.h
index 12b9f44e..6dc33441 100644
--- a/include/serd/serd.h
+++ b/include/serd/serd.h
@@ -258,7 +258,7 @@ void
serd_free(void* SERD_NULLABLE ptr);
/**
- @name String Utilities
+ @defgroup string String Utilities
@{
*/
@@ -312,7 +312,7 @@ serd_base64_decode(const uint8_t* SERD_NONNULL str,
/**
@}
- @name Byte Streams
+ @defgroup streams Byte Streams
@{
*/
@@ -349,7 +349,7 @@ typedef size_t (*SerdSink)(const void* SERD_NONNULL buf,
/**
@}
- @name URI
+ @defgroup uri URI
@{
*/
@@ -427,7 +427,7 @@ serd_uri_serialise_relative(const SerdURI* SERD_NONNULL uri,
/**
@}
- @name Node
+ @defgroup node Node
@{
*/
@@ -585,7 +585,7 @@ serd_node_free(SerdNode* SERD_NULLABLE node);
/**
@}
- @name Event Handlers
+ @defgroup event Event Handlers
@{
*/
@@ -642,7 +642,7 @@ typedef SerdStatus (*SerdEndSink)(void* SERD_NULLABLE handle,
/**
@}
- @name Environment
+ @defgroup env Environment
@{
*/
@@ -728,7 +728,7 @@ serd_env_foreach(const SerdEnv* SERD_NONNULL env,
/**
@}
- @name Reader
+ @defgroup reader Reader
@{
*/
@@ -880,7 +880,7 @@ serd_reader_free(SerdReader* SERD_NULLABLE reader);
/**
@}
- @name Writer
+ @defgroup writer Writer
@{
*/
diff --git a/resources/serd.svg b/resources/serd.svg
new file mode 100644
index 00000000..570a4f98
--- /dev/null
+++ b/resources/serd.svg
@@ -0,0 +1,20 @@
+<svg height="128" viewBox="0 0 33.867 33.867" width="128" xmlns="http://www.w3.org/2000/svg">
+ <g stroke="#444" stroke-linejoin="round" stroke-width="1.058">
+ <path d="M26.726 7.14h6.529V.613h-6.529z" fill="#fff" fill-rule="evenodd"/>
+ <path d="M26.726 7.14h6.529V.613h-6.529z" fill="none"/>
+ <path d="M13.67 7.14h6.528V.613h-6.529z" fill="#fff" fill-rule="evenodd"/>
+ <path d="M13.67 7.14h6.528V.613h-6.529z" fill="none"/>
+ <path d="M.612 7.14h6.529V.613H.612z" fill="#fff" fill-rule="evenodd"/>
+ <path d="M.612 7.14h6.529V.613H.612z" fill="none"/>
+ <path d="M26.726 33.255h6.529v-6.529h-6.529z" fill="#fff" fill-rule="evenodd"/>
+ <path d="M26.726 33.255h6.529v-6.529h-6.529z" fill="none"/>
+ <path d="M13.67 33.255h6.528v-6.529h-6.529z" fill="#fff" fill-rule="evenodd"/>
+ <path d="M13.67 33.255h6.528v-6.529h-6.529z" fill="none"/>
+ <path d="M.612 33.255h6.529v-6.529H.612z" fill="#fff" fill-rule="evenodd"/>
+ <path d="M.612 33.255h6.529v-6.529H.612z" fill="none"/>
+ <path d="M13.67 20.198h6.528v-6.529h-6.529z" fill="#fff" fill-rule="evenodd"/>
+ <g fill="none">
+ <path d="M13.67 20.198h6.528v-6.529h-6.529zM20.198 3.876h6.528M7.14 3.876h6.53M7.14 7.14l6.53 6.53M20.198 20.198l6.528 6.528M7.14 29.99h6.53M20.198 29.99h6.528"/>
+ </g>
+ </g>
+</svg>
diff --git a/scripts/dox_to_sphinx.py b/scripts/dox_to_sphinx.py
new file mode 100755
index 00000000..c9d401cc
--- /dev/null
+++ b/scripts/dox_to_sphinx.py
@@ -0,0 +1,674 @@
+#!/usr/bin/env python3
+
+# Copyright 2020 David Robillard <d@drobilla.net>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""
+Write Sphinx markup from Doxygen XML.
+
+Takes a path to a directory of XML generated by Doxygen, and emits a directory
+with a reStructuredText file for every documented symbol.
+"""
+
+import argparse
+import os
+import sys
+import textwrap
+import xml.etree.ElementTree
+
+__author__ = "David Robillard"
+__date__ = "2020-11-18"
+__email__ = "d@drobilla.net"
+__license__ = "ISC"
+__version__ = __date__.replace("-", ".")
+
+
+def load_index(index_path):
+ """
+ Load the index from XML.
+
+ :returns: A dictionary from ID to skeleton records with basic information
+ for every documented entity. Some records have an ``xml_filename`` key
+ with the filename of a definition file. These files will be loaded later
+ to flesh out the records in the index.
+ """
+
+ root = xml.etree.ElementTree.parse(index_path).getroot()
+ index = {}
+
+ for compound in root:
+ compound_id = compound.get("refid")
+ compound_kind = compound.get("kind")
+ compound_name = compound.find("name").text
+ if compound_kind in ["dir", "file", "page"]:
+ continue
+
+ # Add record for compound (compounds appear only once in the index)
+ assert compound_id not in index
+ index[compound_id] = {
+ "kind": compound_kind,
+ "name": compound_name,
+ "xml_filename": compound_id + ".xml",
+ "children": [],
+ }
+
+ name_prefix = (
+ ("%s::" % compound_name) if compound_kind == "namespace" else ""
+ )
+
+ for child in compound.findall("member"):
+ if child.get("refid") in index:
+ assert compound_kind == "group"
+ continue
+
+ # Everything has a kind and a name
+ child_record = {
+ "kind": child.get("kind"),
+ "name": name_prefix + child.find("name").text,
+ }
+
+ if child.get("kind") == "enum":
+ # Enums are not compounds, but we want to resolve the parent of
+ # their values so they are not written as top level documents
+ child_record["children"] = []
+
+ if child.get("kind") == "enumvalue":
+ # Remove namespace prefix
+ child_record["name"] = child.find("name").text
+
+ index[child.get("refid")] = child_record
+
+ return index
+
+
+def resolve_index(index, root):
+ """
+ Walk a definition document and extend the index for linking.
+
+ This does two things: sets the "parent" and "children" fields of all
+ applicable records, and sets the "strong" field of enums so that the
+ correct Sphinx role can be used when referring to them.
+ """
+
+ def add_child(index, parent_id, child_id):
+ parent = index[parent_id]
+ child = index[child_id]
+
+ if child["kind"] == "enumvalue":
+ assert parent["kind"] == "enum"
+ assert "parent" not in child or child["parent"] == parent_id
+ child["parent"] = parent_id
+
+ else:
+ if parent["kind"] in ["class", "struct", "union"]:
+ assert "parent" not in child or child["parent"] == parent_id
+ child["parent"] = parent_id
+
+ if child_id not in parent["children"]:
+ parent["children"] += [child_id]
+
+ compound = root.find("compounddef")
+ compound_kind = compound.get("kind")
+
+ if compound_kind == "group":
+ for subgroup in compound.findall("innergroup"):
+ add_child(index, compound.get("id"), subgroup.get("refid"))
+
+ for klass in compound.findall("innerclass"):
+ add_child(index, compound.get("id"), klass.get("refid"))
+
+ for section in compound.findall("sectiondef"):
+ if section.get("kind").startswith("private"):
+ for member in section.findall("memberdef"):
+ if member.get("id") in index:
+ del index[member.get("id")]
+ else:
+ for member in section.findall("memberdef"):
+ member_id = member.get("id")
+ add_child(index, compound.get("id"), member_id)
+
+ if member.get("kind") == "enum":
+ index[member_id]["strong"] = member.get("strong") == "yes"
+ for value in member.findall("enumvalue"):
+ add_child(index, member_id, value.get("id"))
+
+
+def sphinx_role(record, lang):
+ """
+ Return the Sphinx role used for a record.
+
+ This is used for the description directive like ".. c:function::", and
+ links like ":c:func:`foo`.
+ """
+
+ kind = record["kind"]
+
+ if kind in ["class", "function", "namespace", "struct", "union"]:
+ return lang + ":" + kind
+
+ if kind == "define":
+ return "c:macro"
+
+ if kind == "enum":
+ return lang + (":enum-class" if record["strong"] else ":enum")
+
+ if kind == "typedef":
+ return lang + ":type"
+
+ if kind == "enumvalue":
+ return lang + ":enumerator"
+
+ if kind == "variable":
+ return lang + (":member" if "parent" in record else ":var")
+
+ raise RuntimeError("No known role for kind '%s'" % kind)
+
+
+def child_identifier(lang, parent_name, child_name):
+ """
+ Return the identifier for an enum value or struct member.
+
+ Sphinx, for some reason, uses a different syntax for this in C and C++.
+ """
+
+ separator = "::" if lang == "cpp" else "."
+
+ return "%s%s%s" % (parent_name, separator, child_name)
+
+
+def link_markup(index, lang, refid):
+ """Return a Sphinx link for a Doxygen reference."""
+
+ record = index[refid]
+ kind, name = record["kind"], record["name"]
+ role = sphinx_role(record, lang)
+
+ if kind in ["class", "enum", "struct", "typedef", "union"]:
+ return ":%s:`%s`" % (role, name)
+
+ if kind == "function":
+ return ":%s:func:`%s`" % (lang, name)
+
+ if kind == "enumvalue":
+ parent_name = index[record["parent"]]["name"]
+ return ":%s:`%s`" % (role, child_identifier(lang, parent_name, name))
+
+ if kind == "variable":
+ if "parent" not in record:
+ return ":%s:var:`%s`" % (lang, name)
+
+ parent_name = index[record["parent"]]["name"]
+ return ":%s:`%s`" % (role, child_identifier(lang, parent_name, name))
+
+ raise RuntimeError("Unknown link target kind: %s" % kind)
+
+
+def indent(markup, depth):
+ """
+ Indent markup to a depth level.
+
+ Like textwrap.indent() but takes an integer and works in reST indentation
+ levels for clarity."
+ """
+
+ return textwrap.indent(markup, " " * depth)
+
+
+def heading(text, level):
+ """
+ Return a ReST heading at a given level.
+
+ Follows the style in the Python documentation guide, see
+ <https://devguide.python.org/documenting/#sections>.
+ """
+
+ assert 1 <= level <= 6
+
+ chars = ("#", "*", "=", "-", "^", '"')
+ line = chars[level] * len(text)
+
+ return "%s\n%s\n%s\n\n" % (line if level < 3 else "", text, line)
+
+
+def dox_to_rst(index, lang, node):
+ """
+ Convert documentation commands (docCmdGroup) to Sphinx markup.
+
+ This is used to convert the content of descriptions in the documentation.
+ It recursively parses all children tags and raises a RuntimeError if any
+ unknown tag is encountered.
+ """
+
+ def field_value(markup):
+ """Return a value for a field as a single line or indented block."""
+ if "\n" in markup.strip():
+ return "\n" + indent(markup, 1)
+
+ return " " + markup.strip()
+
+ if node.tag == "computeroutput":
+ # assert len(node) == 0 FIXME
+ return "``%s``" % node.text
+
+ if node.tag == "itemizedlist":
+ markup = ""
+ for item in node.findall("listitem"):
+ assert len(item) == 1
+ markup += "\n- %s" % dox_to_rst(index, lang, item[0])
+
+ return markup
+
+ if node.tag == "para":
+ markup = node.text if node.text is not None else ""
+ for child in node:
+ markup += dox_to_rst(index, lang, child)
+ markup += child.tail if child.tail is not None else ""
+
+ return markup.strip() + "\n\n"
+
+ if node.tag == "parameterlist":
+ markup = ""
+ for item in node.findall("parameteritem"):
+ name = item.find("parameternamelist/parametername")
+ description = item.find("parameterdescription")
+ assert len(description) == 1
+ markup += "\n\n:param %s:%s" % (
+ name.text,
+ field_value(dox_to_rst(index, lang, description[0])),
+ )
+
+ return markup + "\n"
+
+ if node.tag == "programlisting":
+ return "\n.. code-block:: %s\n\n%s" % (
+ lang,
+ indent(plain_text(node), 1),
+ )
+
+ if node.tag == "ref":
+ refid = node.get("refid")
+ if refid not in index:
+ sys.stderr.write("warning: Unresolved link: %s\n" % refid)
+ return node.text
+
+ assert len(node) == 0
+ assert len(link_markup(index, lang, refid)) > 0
+ return link_markup(index, lang, refid)
+
+ if node.tag == "simplesect":
+ assert len(node) == 1
+
+ if node.get("kind") == "return":
+ return "\n:returns:" + field_value(
+ dox_to_rst(index, lang, node[0])
+ )
+
+ if node.get("kind") == "see":
+ return dox_to_rst(index, lang, node[0])
+
+ raise RuntimeError("Unknown simplesect kind: %s" % node.get("kind"))
+
+ if node.tag == "ulink":
+ return "`%s <%s>`_" % (node.text, node.get("url"))
+
+ raise RuntimeError("Unknown documentation command: %s" % node.tag)
+
+
+def description_markup(index, lang, node):
+ """Return the markup for a brief or detailed description."""
+
+ assert node.tag == "briefdescription" or node.tag == "detaileddescription"
+ assert not (node.tag == "briefdescription" and len(node) > 1)
+ assert len(node.text.strip()) == 0
+
+ return "".join([dox_to_rst(index, lang, child) for child in node])
+
+
+def set_descriptions(index, lang, definition, record):
+ """Set a record's brief/detailed descriptions from the XML definition."""
+
+ for tag in ["briefdescription", "detaileddescription"]:
+ node = definition.find(tag)
+ if node is not None:
+ record[tag] = description_markup(index, lang, node)
+
+
+def set_template_params(node, record):
+ """Set a record's template_params from the XML definition."""
+
+ template_param_list = node.find("templateparamlist")
+ if template_param_list is not None:
+ params = []
+ for param in template_param_list.findall("param"):
+ if param.find("declname") is not None:
+ # Value parameter
+ type_text = plain_text(param.find("type"))
+ name_text = plain_text(param.find("declname"))
+
+ params += ["%s %s" % (type_text, name_text)]
+ else:
+ # Type parameter
+ params += ["%s" % (plain_text(param.find("type")))]
+
+ record["template_params"] = "%s" % ", ".join(params)
+
+
+def plain_text(node):
+ """
+ Return the plain text of a node with all tags ignored.
+
+ This is needed where Doxygen may include refs but Sphinx needs plain text
+ because it parses things itself to generate links.
+ """
+
+ if node.tag == "sp":
+ markup = " "
+ elif node.text is not None:
+ markup = node.text
+ else:
+ markup = ""
+
+ for child in node:
+ markup += plain_text(child)
+ markup += child.tail if child.tail is not None else ""
+
+ return markup
+
+
+def local_name(name):
+ """Return a name with all namespace prefixes stripped."""
+
+ return name[name.rindex("::") + 2 :] if "::" in name else name
+
+
+def read_definition_doc(index, lang, root):
+ """Walk a definition document and update described records in the index."""
+
+ # Set descriptions for the compound itself
+ compound = root.find("compounddef")
+ compound_record = index[compound.get("id")]
+ set_descriptions(index, lang, compound, compound_record)
+ set_template_params(compound, compound_record)
+
+ if compound.find("title") is not None:
+ compound_record["title"] = compound.find("title").text.strip()
+
+ # Set documentation for all children
+ for section in compound.findall("sectiondef"):
+ if section.get("kind").startswith("private"):
+ continue
+
+ for member in section.findall("memberdef"):
+ kind = member.get("kind")
+ record = index[member.get("id")]
+ set_descriptions(index, lang, member, record)
+ set_template_params(member, record)
+
+ if compound.get("kind") in ["class", "struct", "union"]:
+ assert kind in ["function", "typedef", "variable"]
+ record["type"] = plain_text(member.find("type"))
+
+ if kind == "enum":
+ for value in member.findall("enumvalue"):
+ set_descriptions(
+ index, lang, value, index[value.get("id")]
+ )
+
+ elif kind == "function":
+ record["prototype"] = "%s %s%s" % (
+ plain_text(member.find("type")),
+ member.find("name").text,
+ member.find("argsstring").text,
+ )
+
+ elif kind == "typedef":
+ name = local_name(record["name"])
+ args_text = member.find("argsstring").text
+ target_text = plain_text(member.find("type"))
+ if args_text is not None: # Function pointer
+ assert target_text[-2:] == "(*" and args_text[0] == ")"
+ record["type"] = target_text + args_text
+ record["definition"] = target_text + name + args_text
+ else: # Normal named typedef
+ assert target_text is not None
+ record["type"] = target_text
+ if member.find("definition").text.startswith("using"):
+ record["definition"] = "%s = %s" % (
+ name,
+ target_text,
+ )
+ else:
+ record["definition"] = "%s %s" % (
+ target_text,
+ name,
+ )
+
+ elif kind == "variable":
+ record["definition"] = member.find("definition").text
+
+
+def declaration_string(record):
+ """
+ Return the string that describes a declaration.
+
+ This is what follows the directive, and is in C/C++ syntax, except without
+ keywords like "typedef" and "using" as expected by Sphinx. For example,
+ "struct ThingImpl Thing" or "void run(int value)".
+ """
+
+ kind = record["kind"]
+ result = ""
+
+ if "template_params" in record:
+ result = "template <%s> " % record["template_params"]
+
+ if kind == "function":
+ result += record["prototype"]
+ elif kind == "typedef":
+ result += record["definition"]
+ elif kind == "variable":
+ if "parent" in record:
+ result += "%s %s" % (record["type"], local_name(record["name"]))
+ else:
+ result += record["definition"]
+ elif "type" in record:
+ result += "%s %s" % (record["type"], local_name(record["name"]))
+ else:
+ result += local_name(record["name"])
+
+ return result
+
+
+def document_markup(index, lang, record):
+ """Return the complete document that describes some documented entity."""
+
+ kind = record["kind"]
+ role = sphinx_role(record, lang)
+ name = record["name"]
+ markup = ""
+
+ if name != local_name(name):
+ markup += ".. cpp:namespace:: %s\n\n" % name[0 : name.rindex("::")]
+
+ # Write top-level directive
+ markup += ".. %s:: %s\n" % (role, declaration_string(record))
+
+ # Write main description blurb
+ markup += "\n"
+ markup += indent(record["briefdescription"], 1)
+ markup += indent(record["detaileddescription"], 1)
+
+ assert (
+ kind in ["class", "enum", "namespace", "struct", "union"]
+ or "children" not in record
+ )
+
+ # Sphinx C++ namespaces work by setting a scope, they have no content
+ child_indent = 0 if kind == "namespace" else 1
+
+ # Write inline children if applicable
+ markup += "\n"
+ for child_id in record.get("children", []):
+ child_record = index[child_id]
+ child_role = sphinx_role(child_record, lang)
+
+ child_header = ".. %s:: %s\n\n" % (
+ child_role,
+ declaration_string(child_record),
+ )
+
+ markup += "\n"
+ markup += indent(child_header, child_indent)
+ markup += indent(child_record["briefdescription"], child_indent + 1)
+ markup += indent(child_record["detaileddescription"], child_indent + 1)
+ markup += "\n"
+
+ return markup
+
+
+def symbol_filename(name):
+ """Adapt the name of a symbol to be suitable for use as a filename."""
+
+ return name.replace("::", "__")
+
+
+def emit_symbols(index, lang, symbol_dir, force):
+ """Write a description file for every symbol documented in the index."""
+
+ for record in index.values():
+ if (
+ record["kind"] in ["group", "namespace"]
+ or "parent" in record
+ and index[record["parent"]]["kind"] != "group"
+ ):
+ continue
+
+ name = record["name"]
+ filename = os.path.join(symbol_dir, symbol_filename("%s.rst" % name))
+ if not force and os.path.exists(filename):
+ raise FileExistsError("File already exists: '%s'" % filename)
+
+ with open(filename, "w") as rst:
+ rst.write(heading(local_name(name), 3))
+ rst.write(document_markup(index, lang, record))
+
+
+def emit_groups(index, output_dir, symbol_dir_name, force):
+ """Write a description file for every group documented in the index."""
+
+ for record in index.values():
+ if record["kind"] != "group":
+ continue
+
+ name = record["name"]
+ filename = os.path.join(output_dir, "%s.rst" % name)
+ if not force and os.path.exists(filename):
+ raise FileExistsError("File already exists: '%s'" % filename)
+
+ with open(filename, "w") as rst:
+ rst.write(heading(record["title"], 2))
+
+ # Get all child group and symbol names
+ group_names = []
+ symbol_names = []
+ for child_id in record["children"]:
+ child = index[child_id]
+ if child["kind"] == "group":
+ group_names += [child["name"]]
+ else:
+ symbol_names += [child["name"]]
+
+ # Emit description (document body)
+ rst.write(record["briefdescription"] + "\n\n")
+ rst.write(record["detaileddescription"] + "\n\n")
+
+ # Emit TOC
+ rst.write(".. toctree::\n")
+
+ # Emit groups at the top of the TOC
+ for group_name in group_names:
+ rst.write("\n" + indent(group_name, 1))
+
+ # Emit symbols in sorted order
+ for symbol_name in sorted(symbol_names):
+ path = "/".join(
+ [symbol_dir_name, symbol_filename(symbol_name)]
+ )
+ rst.write("\n" + indent(path, 1))
+
+ rst.write("\n")
+
+
+def run(index_xml_path, output_dir, symbol_dir_name, language, force):
+ """Write a directory of Sphinx files from a Doxygen XML directory."""
+
+ # Build skeleton index from index.xml
+ xml_dir = os.path.dirname(index_xml_path)
+ index = load_index(index_xml_path)
+
+ # Load all definition documents
+ definition_docs = []
+ for record in index.values():
+ if "xml_filename" in record:
+ xml_path = os.path.join(xml_dir, record["xml_filename"])
+ definition_docs += [xml.etree.ElementTree.parse(xml_path)]
+
+ # Do an initial pass of the definition documents to resolve the index
+ for root in definition_docs:
+ resolve_index(index, root)
+
+ # Finally read the documentation from definition documents
+ for root in definition_docs:
+ read_definition_doc(index, language, root)
+
+ # Emit output files
+ symbol_dir = os.path.join(output_dir, symbol_dir_name)
+ os.makedirs(symbol_dir, exist_ok=True)
+ emit_symbols(index, language, symbol_dir, force)
+ emit_groups(index, output_dir, symbol_dir_name, force)
+
+
+if __name__ == "__main__":
+ ap = argparse.ArgumentParser(
+ usage="%(prog)s [OPTION]... XML_DIR OUTPUT_DIR",
+ description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+
+ ap.add_argument(
+ "-f",
+ "--force",
+ action="store_true",
+ help="overwrite files",
+ )
+
+ ap.add_argument(
+ "-l",
+ "--language",
+ default="c",
+ choices=["c", "cpp"],
+ help="language domain for output",
+ )
+
+ ap.add_argument(
+ "-s",
+ "--symbol-dir-name",
+ default="symbols",
+ help="name for subdirectory of symbol documentation files",
+ )
+
+ ap.add_argument("index_xml_path", help="path index.xml from Doxygen")
+ ap.add_argument("output_dir", help="output directory")
+
+ run(**vars(ap.parse_args(sys.argv[1:])))
diff --git a/wscript b/wscript
index 6812c532..fa2d2479 100644
--- a/wscript
+++ b/wscript
@@ -43,6 +43,9 @@ def configure(conf):
conf.load('compiler_c', cache=True)
conf.load('autowaf', cache=True)
+ if conf.env.DOCS:
+ conf.load('sphinx')
+
if not autowaf.set_c_lang(conf, 'c11', mandatory=False):
autowaf.set_c_lang(conf, 'c99')
@@ -264,13 +267,7 @@ def build(bld):
# Documentation
if bld.env.DOCS:
- autowaf.build_dox(bld, 'SERD', SERD_VERSION, top, out)
- bld(features='subst',
- source='doc/index.html.in',
- target='index.html',
- install_path='',
- name='index',
- SERD_VERSION=SERD_VERSION)
+ bld.recurse('doc/c')
# Man page
bld.install_files('${MANDIR}/man1', 'doc/serdi.1')