diff options
23 files changed, 955 insertions, 22 deletions
diff --git a/Makefile-giscanner.am b/Makefile-giscanner.am index 4f08934c..2879ab20 100644 --- a/Makefile-giscanner.am +++ b/Makefile-giscanner.am @@ -47,6 +47,7 @@ pkgpyexec_PYTHON = \ giscanner/introspectablepass.py \ giscanner/libtoolimporter.py \ giscanner/maintransformer.py \ + giscanner/mdextensions.py \ giscanner/message.py \ giscanner/msvccompiler.py \ giscanner/pkgconfig.py \ @@ -105,7 +106,23 @@ nobase_dist_template_DATA = \ giscanner/doctemplates/mallard/Gjs/property.tmpl \ giscanner/doctemplates/mallard/Gjs/record.tmpl \ giscanner/doctemplates/mallard/Gjs/signal.tmpl \ - giscanner/doctemplates/mallard/Gjs/vfunc.tmpl + giscanner/doctemplates/mallard/Gjs/vfunc.tmpl \ + giscanner/doctemplates/devdocs/Gjs/_doc.tmpl \ + giscanner/doctemplates/devdocs/Gjs/_index.tmpl \ + giscanner/doctemplates/devdocs/Gjs/_method.tmpl \ + giscanner/doctemplates/devdocs/Gjs/_methods.tmpl \ + giscanner/doctemplates/devdocs/Gjs/_properties.tmpl \ + giscanner/doctemplates/devdocs/Gjs/_signals.tmpl \ + giscanner/doctemplates/devdocs/Gjs/_staticmethods.tmpl \ + giscanner/doctemplates/devdocs/Gjs/_vfuncs.tmpl \ + giscanner/doctemplates/devdocs/Gjs/base.tmpl \ + giscanner/doctemplates/devdocs/Gjs/callback.tmpl \ + giscanner/doctemplates/devdocs/Gjs/class.tmpl \ + giscanner/doctemplates/devdocs/Gjs/default.tmpl \ + giscanner/doctemplates/devdocs/Gjs/enum.tmpl \ + giscanner/doctemplates/devdocs/Gjs/function.tmpl \ + giscanner/doctemplates/devdocs/Gjs/interface.tmpl \ + giscanner/doctemplates/devdocs/Gjs/namespace.tmpl _giscanner_la_CFLAGS = \ $(PYTHON_INCLUDES) \ diff --git a/configure.ac b/configure.ac index 29bddde3..e8c23523 100644 --- a/configure.ac +++ b/configure.ac @@ -276,13 +276,20 @@ dnl an external dependency AC_ARG_ENABLE(doctool,[ --disable-doctool disable g-ir-doc-tool ],,enable_doctool=auto) AS_IF([ test x$enable_doctool != xno], [ AM_CHECK_PYMOD(mako,,have_python_mako=yes,have_python_mako=no) + AM_CHECK_PYMOD(markdown,,have_python_markdown=yes,have_python_markdown=no) ]) -AS_IF([ test x$enable_doctool = xauto && test x$have_python_mako = xyes ], +AS_IF([ test x$enable_doctool = xauto && + test x$have_python_mako = xyes && + test x$have_python_markdown = xyes ], [ enable_doctool=yes ], - [ test x$enable_doctool = xauto && test x$have_python_mako = xno ], + [ test x$enable_doctool = xauto && + (test x$have_python_mako = xno || + test x$have_python_markdown = xno) ], [ enable_doctool=no ], [ test x$enable_doctool = xyes && test x$have_python_mako = xno ], - [ AC_MSG_ERROR([Python mako module not found]) ]) + [ AC_MSG_ERROR([Python mako module not found]) ], + [ test x$enable_doctool = xyes && test x$have_python_markdown = xno ], + [ AC_MSG_ERROR([Python markdown module not found]) ]) AM_CONDITIONAL(BUILD_DOCTOOL, test x$enable_doctool != xno) # Glib documentation diff --git a/giscanner/docmain.py b/giscanner/docmain.py index 966b33c2..f184014b 100644 --- a/giscanner/docmain.py +++ b/giscanner/docmain.py @@ -32,7 +32,7 @@ from .docwriter import DocWriter from .sectionparser import generate_sections_file, write_sections_file from .transformer import Transformer -FORMATS = ('mallard', 'sections') +FORMATS = ('devdocs', 'mallard', 'sections') def doc_main(args): @@ -61,6 +61,9 @@ def doc_main(args): args = parser.parse_args(args[1:]) if not args.output: raise SystemExit("missing output parameter") + if args.format not in FORMATS: + raise SystemExit("Unknown output format %s (supported: %s)" % + (args.format, ", ".join(FORMATS))) if 'UNINSTALLED_INTROSPECTION_SRCDIR' in os.environ: top_srcdir = os.environ['UNINSTALLED_INTROSPECTION_SRCDIR'] diff --git a/giscanner/doctemplates/devdocs/Gjs/_doc.tmpl b/giscanner/doctemplates/devdocs/Gjs/_doc.tmpl new file mode 100644 index 00000000..dbdb8259 --- /dev/null +++ b/giscanner/doctemplates/devdocs/Gjs/_doc.tmpl @@ -0,0 +1,37 @@ +<%def name="format_documentation(node)"> + % if node.version: + <p class="since-note"> + New in version ${node.version}. + ${formatter.format_inline(node, node.version_doc)} + </p> + % endif + % if node.deprecated: + <p class="deprecated-note"> + Deprecated since ${node.deprecated}. + ${formatter.format_inline(node, node.deprecated_doc)} + </p> + % endif + % if node.stability: + ## Not sure what this looks like in the wild + <p class="stability-note"> + Stability: ${node.stability}. + ${formatter.format_inline(node, node.stability_doc)} + </p> + % endif + + % if node.doc: + ${formatter.format(node, node.doc)} + % endif +</%def> + +<%def name="deprecated_class(node)"> + % if node.deprecated: + deprecated + % endif +</%def> + +<%def name="introspectable(node)"> + % if getattr(node, "introspectable", True): + ${caller.body()} + % endif +</%def> diff --git a/giscanner/doctemplates/devdocs/Gjs/_index.tmpl b/giscanner/doctemplates/devdocs/Gjs/_index.tmpl new file mode 100644 index 00000000..f3d588ae --- /dev/null +++ b/giscanner/doctemplates/devdocs/Gjs/_index.tmpl @@ -0,0 +1,191 @@ +<%namespace name="doc" file="_doc.tmpl"/> + +<% + ancestors = [] + if isinstance(node, ast.Class): + ancestors = formatter.get_inheritable_types(node) + + fields = getattr(node, 'fields', []) + extra_fields = [getattr(f.anonymous_node, 'fields', []) for f in fields + if f.anonymous_node is not None] + extra_fields = [field for sublist in extra_fields for field in sublist] + non_private_fields = [f for f in fields + extra_fields + if not formatter.is_private_field(node, f)] + + def get_ancestor_counts(*kinds): + counts = {} + for a in ancestors: + count = 0 + for kind in kinds: + if kind == 'fields': + count += len(non_private_fields) + else: + count += len(getattr(a, kind, [])) + if count: + counts[a] = count + return counts + + def should_render(*kinds): + has_nonempty = False + for kind in kinds: + if kind == 'fields': + if non_private_fields: + has_nonempty = True + elif getattr(node, kind, []): + has_nonempty = True + return has_nonempty or get_ancestor_counts(*kinds) +%> + +<%def name="inherited(*kinds)"> + <% counts = get_ancestor_counts(*kinds) %> + % if counts: + <% + links = ', '.join(['{} ({})'.format(formatter.format_xref(a), count) + for a, count in counts.items()]) + %> + ${formatter.format(node, '**Inherited:** ' + links)} + % endif +</%def> + +<%def name="format_function_cell(m)"> + <td class="${doc.deprecated_class(m)}"> + <a href="#${formatter.make_anchor(m)}"> + ${formatter.format_function_name(m)}<!-- no space + --></a><!-- no space + -->(${formatter.format_in_parameters(m)}) + </td> +</%def> + +% if should_render('static_methods', 'constructors', 'methods'): + <h2 id="index-methods">Methods</h2> + ${inherited('static_methods', 'constructors', 'methods')} + <% + static_methods = getattr(node, 'static_methods', []) + getattr(node, 'constructors', []) + methods = static_methods + getattr(node, 'methods', []) + %> + % if methods: + <table class="index"> + <tbody> + % for m in methods: + <%doc:introspectable node="${m}"> + <tr> + % if m in static_methods: + <td class="static-method-indicator">static</td> + % else: + <td></td> + % endif + ${format_function_cell(m)} + </tr> + </%doc:introspectable> + % endfor + </tbody> + </table> + % endif +% endif + +% if should_render('virtual_methods'): + <h2 id="index-vfuncs">Virtual methods</h2> + ${inherited('virtual_methods')} + % if getattr(node, 'virtual_methods', []): + <table> + <tbody> + % for m in node.virtual_methods: + <%doc:introspectable node="${m}"> + <tr> + ${format_function_cell(m)} + </tr> + </%doc:introspectable> + % endfor + </tbody> + </table> + % endif +% endif + +% if should_render('properties'): + <h2 id="index-properties">Properties</h2> + ${inherited('properties')} + % if getattr(node, 'properties', []): + <table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Flags</th> + </tr> + </thead> + <tbody> + % for p in node.properties: + <%doc:introspectable node="${p}"> + <tr> + <td class="${doc.deprecated_class(p)}"> + <a href="#${formatter.make_anchor(p)}">${p.name}</a> + </td> + <td>${formatter.format_type(p.type)}</td> + <td>${formatter.format_property_flags(p, abbrev=True)}</td> + </tr> + </%doc:introspectable> + % endfor + </tbody> + </table> + % endif +% endif + +% if should_render('signals'): + <h2 id="index-signals">Signals</h2> + ${inherited('signals')} + % if getattr(node, 'signals', []): + <table> + <tbody> + % for s in node.signals: + <%doc:introspectable node="${s}"> + <tr> + <td class="${doc.deprecated_class(s)}"> + <a href="#${formatter.make_anchor(s)}">${s.name}</a><!-- no space + -->(${formatter.format_in_parameters(s)}) + </td> + </tr> + </%doc:introspectable> + % endfor + </tbody> + </table> + % endif +% endif + +% if should_render('fields'): + <h2 id="index-fields">Fields</h2> + ${inherited('fields')} + % if non_private_fields: + <table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Access</th> + <th>Description</th> + </tr> + </thead> + <tbody> + % for f in non_private_fields: + <%doc:introspectable node="${f}"> + <tr> + <td class="${doc.deprecated_class(f)}"> + <span class="entry" href="#${formatter.make_anchor(f)}"> + ${f.name} + </span> + </td> + <td>${formatter.format_type(f.type)}</td> + <td>${formatter.format_property_flags(f, abbrev=True)}</td> + ## Fields almost never warrant a detailed entry, we'll just make this + ## the entry to be indexed by DevDocs + <td> + % if f.doc: + ${formatter.format_inline(node, f.doc)} + % endif + </td> + </tr> + </%doc:introspectable> + % endfor + </tbody> + </table> + % endif +% endif diff --git a/giscanner/doctemplates/devdocs/Gjs/_method.tmpl b/giscanner/doctemplates/devdocs/Gjs/_method.tmpl new file mode 100644 index 00000000..57520465 --- /dev/null +++ b/giscanner/doctemplates/devdocs/Gjs/_method.tmpl @@ -0,0 +1,71 @@ +<%namespace name="doc" file="_doc.tmpl"/> + +<%def name="describe_parameters(m, static=False, virtual=False)"> + <dl> + % if static: + <dt>Type:</dt> + <dd>Static</dd> + % elif virtual: + <dt>Type:</dt> + <dd>Virtual</dd> + % endif + <% + in_params = formatter.get_in_parameters(m) + out_params = formatter.get_out_parameters(m) + %> + % if in_params: + <dt>Parameters:</dt> + <dd> + <ul> + % for p in in_params: + <li> + <strong>${p.argname}</strong> + (<code>${formatter.format_type(p.type)}</code>) + % if p.doc: + — ${formatter.format_inline(m, p.doc)} + % endif + </li> + % endfor + </ul> + </dd> + % endif + % if out_params: + <dt>Returns:</dt> + <dd> + <ul> + % for p in out_params: + <li> + % if len(out_params) > 1: + <strong>${p.argname}</strong> + % endif + (<code>${formatter.format_type(p.type)}</code>) + % if p.doc: + — ${formatter.format_inline(m, p.doc)} + % endif + </li> + % endfor + </ul> + </dd> + % endif + % if m.throws: + <dt>Throws exception:</dt> + <dd>Yes</dd> + % endif + </dl> +</%def> + +<%def name="method(m, static=False, virtual=False)"> + <%doc:introspectable node="${m}"> + <% invocation = ", ".join(map(lambda p: p.argname, m.parameters)) %> + + <h3> + <span class="entry ${get_node_kind(m)} ${doc.deprecated_class(m)}" + id="${formatter.make_anchor(m)}"> + ${formatter.format_function_name(m)}<!-- no space + --></span><!-- no space + -->(${formatter.format_in_parameters(m)}) + </h3> + ${describe_parameters(m, static, virtual)} + ${doc.format_documentation(m)} + </%doc:introspectable> +</%def> diff --git a/giscanner/doctemplates/devdocs/Gjs/_methods.tmpl b/giscanner/doctemplates/devdocs/Gjs/_methods.tmpl new file mode 100644 index 00000000..e876cd68 --- /dev/null +++ b/giscanner/doctemplates/devdocs/Gjs/_methods.tmpl @@ -0,0 +1,4 @@ +<%namespace name="method" file="_method.tmpl"/> +% for m in getattr(node, 'methods', []): + ${method.method(m)} +% endfor diff --git a/giscanner/doctemplates/devdocs/Gjs/_properties.tmpl b/giscanner/doctemplates/devdocs/Gjs/_properties.tmpl new file mode 100644 index 00000000..a7054727 --- /dev/null +++ b/giscanner/doctemplates/devdocs/Gjs/_properties.tmpl @@ -0,0 +1,38 @@ +<%namespace name="doc" file="_doc.tmpl"/> +<%! + def dash_to_underscore(string): + return '_'.join(string.split('-')) + + def dash_to_camel(string): + words = string.split('-') + return ''.join([words[0]] + [word.title() for word in words[1:]]) +%> +% if getattr(node, 'properties', []): + <h2>Property Details</h2> + % for p in node.properties: + <%doc:introspectable node="${p}"> + <h3 class="entry property ${doc.deprecated_class(p)}" + id="${formatter.make_anchor(p)}"> + ${p.name | dash_to_underscore} + </h3> + <dl> + % if p.name.lower() != p.name: + <dt>Names</dt> + <dd> + <code>${p.name}</code>, <code>${p.name | dash_to_underscore}</code>, + <code>${p.name | dash_to_camel}</code> + </dd> + % endif + <dt>Type</dt> + <dd><code>${formatter.format_type(p.type)}</code></dd> + ##<dt>Default value</dt> + ##<dd>Not currently stored in GIR</dd> + <dt>Flags</dt> + <dd>${formatter.format_property_flags(p)}</dd> + </dl> + % if p.doc: + ${doc.format_documentation(p)} + % endif + </%doc:introspectable> + % endfor +% endif diff --git a/giscanner/doctemplates/devdocs/Gjs/_signals.tmpl b/giscanner/doctemplates/devdocs/Gjs/_signals.tmpl new file mode 100644 index 00000000..cda46bd5 --- /dev/null +++ b/giscanner/doctemplates/devdocs/Gjs/_signals.tmpl @@ -0,0 +1,22 @@ +<%namespace name="doc" file="_doc.tmpl"/> +<%namespace name="method" file="_method.tmpl"/> +% if getattr(node, 'signals', []): + <h2>Signal Details</h2> + % for s in node.signals: + <%doc:introspectable node="${s}"> + <h3> + <span class="entry signal ${doc.deprecated_class(s)}" + id="${formatter.make_anchor(s)}"> + ${s.name}<!-- no space + --></span><!-- + -->(${formatter.format_in_parameters(s)}) + </h3> + <dl> + <dt>Flags</dt> + <dd>${formatter.format_signal_flags(s)}</dd> + ${method.describe_parameters(s)} + </dl> + ${doc.format_documentation(s)} + </%doc:introspectable> + % endfor +% endif diff --git a/giscanner/doctemplates/devdocs/Gjs/_staticmethods.tmpl b/giscanner/doctemplates/devdocs/Gjs/_staticmethods.tmpl new file mode 100644 index 00000000..dcd542e1 --- /dev/null +++ b/giscanner/doctemplates/devdocs/Gjs/_staticmethods.tmpl @@ -0,0 +1,4 @@ +<%namespace name="method" file="_method.tmpl"/> +% for m in getattr(node, 'static_methods', []) + getattr(node, 'constructors', []): + ${method.method(m, static=True)} +% endfor diff --git a/giscanner/doctemplates/devdocs/Gjs/_vfuncs.tmpl b/giscanner/doctemplates/devdocs/Gjs/_vfuncs.tmpl new file mode 100644 index 00000000..2966ede4 --- /dev/null +++ b/giscanner/doctemplates/devdocs/Gjs/_vfuncs.tmpl @@ -0,0 +1,6 @@ +<%namespace name="method" file="_method.tmpl"/> +% if getattr(node, 'virtual_methods', []): + % for m in node.virtual_methods: + ${method.method(m, virtual=True)} + % endfor +% endif diff --git a/giscanner/doctemplates/devdocs/Gjs/base.tmpl b/giscanner/doctemplates/devdocs/Gjs/base.tmpl new file mode 100644 index 00000000..913fa0cc --- /dev/null +++ b/giscanner/doctemplates/devdocs/Gjs/base.tmpl @@ -0,0 +1,21 @@ +<%namespace name="doc" file="_doc.tmpl"/> +<html> +<body> + <section> + <h1 class="${page_kind} ${doc.deprecated_class(node)} ${self.extra_class()}"> + ${formatter.format_page_name(node)} + </h1> + <%include file="_index.tmpl"/> + <h2>Details</h2> + ${doc.format_documentation(node)} + <%include file="_staticmethods.tmpl"/> + <%include file="_methods.tmpl"/> + <%include file="_vfuncs.tmpl"/> + <%include file="_signals.tmpl"/> + <%include file="_properties.tmpl"/> + ${self.body()} + </section> +</body> +</html> + +<%def name="extra_class()"/> diff --git a/giscanner/doctemplates/devdocs/Gjs/callback.tmpl b/giscanner/doctemplates/devdocs/Gjs/callback.tmpl new file mode 100644 index 00000000..2795ee3c --- /dev/null +++ b/giscanner/doctemplates/devdocs/Gjs/callback.tmpl @@ -0,0 +1,3 @@ +<%namespace name="method" file="_method.tmpl"/> +<%inherit file="base.tmpl"/> +${method.describe_parameters(node)} diff --git a/giscanner/doctemplates/devdocs/Gjs/class.tmpl b/giscanner/doctemplates/devdocs/Gjs/class.tmpl new file mode 100644 index 00000000..9d2b5238 --- /dev/null +++ b/giscanner/doctemplates/devdocs/Gjs/class.tmpl @@ -0,0 +1 @@ +<%inherit file="base.tmpl"/> diff --git a/giscanner/doctemplates/devdocs/Gjs/default.tmpl b/giscanner/doctemplates/devdocs/Gjs/default.tmpl new file mode 100644 index 00000000..4b08adff --- /dev/null +++ b/giscanner/doctemplates/devdocs/Gjs/default.tmpl @@ -0,0 +1,34 @@ +<%inherit file="base.tmpl"/> +% if isinstance(node, ast.Constant): + <dl> + <dt>Value</dt> + <dd> + <code data-mime="application/javascript"><!-- + -->${formatter.format_value(node)}</code> + </dd> + </dl> +% endif +% if isinstance(node, ast.Alias): + <dl> + <dt>Equivalent Type</dt> + <dd> + <code data-mime="application/javascript"> + % if node.target.target_fundamental: + ${formatter.format_fundamental_type(node.target.target_fundamental)} + % else: + ${node.target.target_giname} + % endif + </code> + </dd> + </dl> +% endif + +## This should belong in get_node_kind(), but we don't want to change the way +## all the other templates work. +<%def name="extra_class()"> + % if isinstance(node, ast.Constant): + constant + % elif isinstance(node, ast.Alias): + alias + % endif +</%def> diff --git a/giscanner/doctemplates/devdocs/Gjs/enum.tmpl b/giscanner/doctemplates/devdocs/Gjs/enum.tmpl new file mode 100644 index 00000000..a66cbefa --- /dev/null +++ b/giscanner/doctemplates/devdocs/Gjs/enum.tmpl @@ -0,0 +1,16 @@ +<%inherit file="base.tmpl"/> +<ul> +% for m in node.members: + <li> + <code> + <span class="entry" id="${formatter.make_anchor(m)}"> + ${m.name.upper()} + </span> + = ${m.value} + </code> + % if m.doc: + — ${formatter.format_inline(node, m.doc)} + % endif + </li> +% endfor +</ul> diff --git a/giscanner/doctemplates/devdocs/Gjs/function.tmpl b/giscanner/doctemplates/devdocs/Gjs/function.tmpl new file mode 100644 index 00000000..2795ee3c --- /dev/null +++ b/giscanner/doctemplates/devdocs/Gjs/function.tmpl @@ -0,0 +1,3 @@ +<%namespace name="method" file="_method.tmpl"/> +<%inherit file="base.tmpl"/> +${method.describe_parameters(node)} diff --git a/giscanner/doctemplates/devdocs/Gjs/interface.tmpl b/giscanner/doctemplates/devdocs/Gjs/interface.tmpl new file mode 100644 index 00000000..9d2b5238 --- /dev/null +++ b/giscanner/doctemplates/devdocs/Gjs/interface.tmpl @@ -0,0 +1 @@ +<%inherit file="base.tmpl"/> diff --git a/giscanner/doctemplates/devdocs/Gjs/method.tmpl b/giscanner/doctemplates/devdocs/Gjs/method.tmpl new file mode 100644 index 00000000..2c997c09 --- /dev/null +++ b/giscanner/doctemplates/devdocs/Gjs/method.tmpl @@ -0,0 +1 @@ +<%inherit file="function.tmpl"/>
\ No newline at end of file diff --git a/giscanner/doctemplates/devdocs/Gjs/namespace.tmpl b/giscanner/doctemplates/devdocs/Gjs/namespace.tmpl new file mode 100644 index 00000000..8f5a4e7e --- /dev/null +++ b/giscanner/doctemplates/devdocs/Gjs/namespace.tmpl @@ -0,0 +1,57 @@ +<%def name="get_types(n, types)"> + <% nodes = [] %> + % for a in n.values(): + % if isinstance(a, types): + % if formatter.should_render_node(a): + <% nodes.append(a) %> + % endif + % endif + % endfor + <% return nodes %> +</%def> + +<%def name="render_list(nodes)"> + <ul> + % for a in nodes: + <li>${formatter.format_inline(a, formatter.format_xref(a))}</li> + % endfor + </ul> +</%def> + +<html> +<body> + <section> + <h1 class="namespace">${node.name}</h1> + </section> + + <% nodes = get_types(node, (ast.Class, ast.Interface)) %> + % if len(nodes) > 0: + <h1>Classes</h1> + ${render_list(nodes)} + % endif + + <% nodes = get_types(node, (ast.Enum)) %> + % if len(nodes) > 0: + <h1>Enums</h1> + ${render_list(nodes)} + % endif + + <% nodes = get_types(node, (ast.Function)) %> + % if len(nodes) > 0: + <h1>Functions</h1> + ${render_list(nodes)} + % endif + + <% nodes = get_types(node, (ast.Constant)) %> + % if len(nodes) > 0: + <h1>Constants</h1> + ${render_list(nodes)} + % endif + + <% nodes = get_types(node, (ast.Property)) %> + % if len(nodes) > 0: + <h1>Properties</h1> + ${render_list(nodes)} + % endif +</body> +</html> diff --git a/giscanner/docwriter.py b/giscanner/docwriter.py index d9c2ed57..d49446a0 100644 --- a/giscanner/docwriter.py +++ b/giscanner/docwriter.py @@ -28,13 +28,42 @@ from __future__ import unicode_literals import os import re +import sys import tempfile from xml.sax import saxutils from mako.lookup import TemplateLookup +import markdown +from markdown.extensions.headerid import HeaderIdExtension from . import ast, xmlwriter from .utils import to_underscores +from .mdextensions import InlineMarkdown + +# Freely inspired from +# https://github.com/GNOME/yelp-xsl/blob/master/js/syntax.html +language_mimes = { + "bash-script": "application/x-shellscript", + "shell": "application/x-shellscript", + "csharp": "text/x-csharp", + "css": "text/css", + "diff": "text/xpatch", + "html": "text/html", + "java": "text/x-java", + "javascript": "application/javascript", + "lisp": "text/x-scheme", + "lua": "text-x-lua", + "c": "text/x-csrc", + "c++": "text/x-c++src", + "pascal": "text/x-pascal", + "perl": "application/x-perl", + "php": "application/x-php", + "plain": "text/plain", + "python": "text/x-python", + "ruby": "application/x-ruby", + "sql": "text/x-sql", + "yaml": "application/x-yaml", +} def make_page_id(node, recursive=False): @@ -166,6 +195,15 @@ class DocstringScanner(TemplatedScanner): specs = [ ('!alpha', r'[a-zA-Z0-9_]+'), ('!alpha_dash', r'[a-zA-Z0-9_-]+'), + ('code_start_with_language', + r'\|\[\<!\-\-\s*language\s*\=\s*\"<<language_name:alpha>>\"\s*\-\-\>'), + ('code_start', r'\|\['), + ('code_end', r'\]\|'), + ('html_code_start', r'<code(.*?)>'), + ('html_code_end', r'</code>'), + ('markdown_code_toggle', r'\`'), + ('markdown_attr_start', r'\{'), + ('markdown_attr_end', r'\}'), ('property', r'#<<type_name:alpha>>:(<<property_name:alpha_dash>>)'), ('signal', r'#<<type_name:alpha>>::(<<signal_name:alpha_dash>>)'), ('type_name', r'#(<<type_name:alpha>>)'), @@ -181,10 +219,18 @@ class DocFormatter(object): def __init__(self, transformer): self._transformer = transformer self._scanner = DocstringScanner() + # If we are processing a code block as defined by + # https://wiki.gnome.org/Projects/GTK%2B/DocumentationSyntax/Markdown + # we won't insert paragraphs and will respect new lines. + self._processing_code = False + self._processing_attr = False def escape(self, text): return saxutils.escape(text) + def unescape(self, text): + return saxutils.unescape(text) + def should_render_node(self, node): if getattr(node, "private", False): return False @@ -203,11 +249,9 @@ class DocFormatter(object): if doc is None: return '' - result = '' - for para in doc.split('\n\n'): - result += ' <p>' - result += self.format_inline(node, para) - result += '</p>' + result = '<p>' + result += self.format_inline(node, doc) + result += '</p>' return result def _resolve_type(self, ident): @@ -239,6 +283,8 @@ class DocFormatter(object): raise KeyError("Could not find %s" % (name, )) def _process_other(self, node, match, props): + if self._processing_code: + return match return self.escape(match) def _process_property(self, node, match, props): @@ -266,11 +312,20 @@ class DocFormatter(object): return self.format_xref(signal) def _process_type_name(self, node, match, props): - type_ = self._resolve_type(props['type_name']) - if type_ is None: + if self._processing_attr: return match - return self.format_xref(type_) + ident = props['type_name'] + type_ = self._resolve_type(ident) + plural = False + if type_ is None: + singularized = ident.rstrip("s") # Try to remove plural + type_ = self._resolve_type(singularized) + plural = True + if type_ is None: + return match + + return self.format_xref(type_, pluralize=plural) def _process_enum_value(self, node, match, props): member_name = props['member_name'] @@ -301,6 +356,79 @@ class DocFormatter(object): return self.format_xref(func) + # FIXME: the four spaces after newlines in the following functions are to + # keep Markdown happy. We pass the documentation string first through this + # templated scanner, which converts |[ ]| to <pre></pre>. Then in the case + # of DevDocs output, we pass the resulting string through Markdown; but + # Markdown will not respect the <pre> element and will treat the code as + # markup, converting asterisks into <em> etc. Putting four spaces at the + # start of each line makes Markdown recognize the code as code without + # affecting the normal HTML output too much. + # + # A better solution would be to replace DocstringScanner by Markdown + # entirely, implementing the custom markup with Markdown extensions. + # + # UPDATE: As a temporary fix for code blocks we will convert directly to ``` syntax. + # + # NOTES: + # _process_markdown_code_toggle: + # Whenever we encounter ` we need to toggle whether we are escaping text as text inside + # inline code blocks is unescaped + # _process_markdown_attr_(start|end): + # Whenever we encounter { or } we must stop parsing type names as curly braces are used for + # attributes in GIR files in addition to type declarations. + # _process_html_code_(start|end): + # Whenever we encounter an HTML <code> block we must stop escaping text. + # + # TODO: Convert to markdown extensions. + + def _process_markdown_code_toggle(self, node, match, props): + self._processing_code = not self._processing_code + return match + + def _process_markdown_attr_start(self, node, match, props): + if not self._processing_code: + self._processing_attr = True + return match + + def _process_markdown_attr_end(self, node, match, props): + if not self._processing_code: + self._processing_attr = False + return match + + def _process_html_code_start(self, node, match, props): + self._processing_code = True + return match + + def _process_html_code_end(self, node, match, props): + self._processing_code = False + return match + + def _process_code_start(self, node, match, props): + self._processing_code = True + return '</p>\n```\n' + + def _process_code_start_with_language(self, node, match, props): + self._processing_code = True + try: + return '</p>\n```' + props["language_name"].lower() + '\n' + except KeyError: + return '</p>\n```\n' + + def _process_code_end(self, node, match, props): + self._processing_code = False + return '\n```\n<p>' + + def _process_new_line(self, node, match, props): + if self._processing_code: + return '\n' + return '\n' + + def _process_new_paragraph(self, node, match, props): + if self._processing_code: + return '\n\n' + return "</p><p>" + def _process_token(self, node, tok): kind, match, props = tok @@ -312,6 +440,16 @@ class DocFormatter(object): 'enum_value': self._process_enum_value, 'parameter': self._process_parameter, 'function_call': self._process_function_call, + 'code_start': self._process_code_start, + 'code_start_with_language': self._process_code_start_with_language, + 'code_end': self._process_code_end, + 'html_code_start': self._process_html_code_start, + 'html_code_end': self._process_html_code_end, + 'markdown_code_toggle': self._process_markdown_code_toggle, + 'markdown_attr_start': self._process_markdown_attr_start, + 'markdown_attr_end': self._process_markdown_attr_end, + 'new_line': self._process_new_line, + 'new_paragraph': self._process_new_paragraph, } return dispatch[kind](node, match, props) @@ -336,6 +474,9 @@ class DocFormatter(object): def format_type(self, type_, link=False): raise NotImplementedError + def format_value(self, node): + raise NotImplementedError + def format_page_name(self, node): if isinstance(node, ast.Namespace): return node.name @@ -352,33 +493,41 @@ class DocFormatter(object): else: return make_page_id(node) - def format_xref(self, node, **attrdict): + def format_xref(self, node, pluralize=False, **attrdict): if node is None or not hasattr(node, 'namespace'): attrs = [('xref', 'index')] + list(sorted(attrdict.items())) return xmlwriter.build_xml_tag('link', attrs) elif isinstance(node, ast.Member): # Enum/BitField members are linked to the main enum page. - return self.format_xref(node.parent, **attrdict) + '.' + node.name + return self.format_xref(node.parent, pluralize=pluralize, **attrdict) + '.' + node.name elif node.namespace is self._transformer.namespace: - return self.format_internal_xref(node, attrdict) + return self.format_internal_xref(node, attrdict, pluralize=pluralize) else: - return self.format_external_xref(node, attrdict) + return self.format_external_xref(node, attrdict, pluralize=pluralize) - def format_internal_xref(self, node, attrdict): + def format_internal_xref(self, node, attrdict, pluralize=False): attrs = [('xref', make_page_id(node))] + list(sorted(attrdict.items())) - return xmlwriter.build_xml_tag('link', attrs) + if not pluralize: + return xmlwriter.build_xml_tag('link', attrs) + else: + return xmlwriter.build_xml_tag('link', attrs, make_page_id(node) + + "s") - def format_external_xref(self, node, attrdict): + def format_external_xref(self, node, attrdict, pluralize=False): ns = node.namespace attrs = [('href', '../%s-%s/%s.html' % (ns.name, str(ns.version), make_page_id(node)))] attrs += list(sorted(attrdict.items())) - return xmlwriter.build_xml_tag('link', attrs, self.format_page_name(node)) + if not pluralize: + return xmlwriter.build_xml_tag('link', attrs, self.format_page_name(node)) + else: + return xmlwriter.build_xml_tag('link', attrs, + self.format_page_name(node) + "s") def field_is_writable(self, field): return True - def format_property_flags(self, property_, construct_only=False): + def format_property_flags(self, property_, construct_only=False, abbrev=False): flags = [] if property_.readable and not construct_only: @@ -392,6 +541,23 @@ class DocFormatter(object): if property_.construct_only: flags.append("Construct Only") + if abbrev: + return "/".join([''.join([word[0] for word in flag.lower().split()]) + for flag in flags]) + return " / ".join(flags) + + def format_signal_flags(self, signal): + flags = [] + if signal.action: + flags.append("Action") + if signal.detailed: + flags.append("Detailed") + if signal.no_hooks: + flags.append("No Hooks") + if signal.no_recurse: + flags.append("No Recurse") + if signal.when: + flags.append("Run " + signal.when.capitalize()) return " / ".join(flags) def to_underscores(self, node): @@ -399,6 +565,8 @@ class DocFormatter(object): return node.name.replace('-', '_') elif node.name: return to_underscores(node.name) + elif isinstance(node, ast.Function) and node.moved_to: + return to_underscores(node.moved_to) elif isinstance(node, ast.Callback): return 'callback' elif isinstance(node, ast.Union): @@ -422,6 +590,51 @@ class DocFormatter(object): parent_chain.reverse() return parent_chain + def get_inheritable_types(self, node): + """Return an ast.Node object for each type (ast.Class and ast.Interface + types) from which an ast.Class @node might inherit methods, properties, + and signals.""" + + assert isinstance(node, ast.Class) + + parent_chain = self.get_class_hierarchy(node) + types = [] + for p in parent_chain: + types += [self._transformer.lookup_typenode(t) for t in p.interfaces] + types += [t for t in parent_chain if t is not node] + return types + + def is_private_field(self, node, f): + """Returns whether @f is a private field of @node (including a heuristic + that tries to determine whether the field is the parent instance field + or a private pointer but not marked as such.)""" + + if f.private: + return True + if f.anonymous_node: + return True + if f.name == 'g_type_instance': + return True # this field on GObject is not exposed + + field_typenode = self._transformer.lookup_typenode(f.type) + if not field_typenode: + return False + + if getattr(field_typenode, 'disguised', False): + return True # guess that it's a pointer to a private struct + # this also catches fields of type GdkAtom, since that is disguised + # as well. Not sure whether that's correct or not. + + if not isinstance(node, ast.Class): + return False # parent instance heuristics only apply to classes + + if node.parent_type: + parent_typenode = self._transformer.lookup_typenode(node.parent_type) + if field_typenode == parent_typenode: + return True # guess that it's a parent instance field + + return False + def format_prerequisites(self, node): assert isinstance(node, ast.Interface) @@ -721,6 +934,8 @@ class DocFormatterGjs(DocFormatterIntrospectableBase): return "void" elif type_.target_giname is not None: giname = type_.target_giname + if giname == 'Gdk.Atom': + return 'String' if giname in ('GLib.ByteArray', 'GLib.Bytes'): return 'ByteArray' if giname == 'GObject.Value': @@ -889,7 +1104,172 @@ class DocFormatterGjs(DocFormatterIntrospectableBase): return ', '.join(('%s: %s' % (p.argname, self.format_type(p.type))) for p in construct_params) + +class DevDocsFormatterGjs(DocFormatterGjs): + output_format = "devdocs" + output_extension = ".html" + + def _is_static_method(self, node): + if not hasattr(node.parent, "static_methods"): + return False + return node in node.parent.static_methods + + def should_render_node(self, node): + # For DevDocs, we only want to render the top-level nodes. + if isinstance(node, (ast.Compound, ast.Boxed)): + self.resolve_gboxed_constructor(node) + + if not super(DevDocsFormatterGjs, self).should_render_node(node): + return False + + if isinstance(node, ast.Function) and not node.is_method and \ + not node.is_constructor and not self._is_static_method(node): + return True # module-level function + toplevel_types = [ast.Alias, ast.Bitfield, ast.Boxed, ast.Callback, + ast.Class, ast.Constant, ast.Enum, ast.Interface, ast.Namespace, + ast.Record, ast.Union] + for ast_type in toplevel_types: + if isinstance(node, ast_type): + return True + + return False + + def format_fundamental_type(self, name): + # Don't specify the C type after Number as the Mallard docs do; it's + # confusing to GJS newbies. + if name in ["gint8", "guint8", "gint16", "guint16", "gint32", "guint32", + "gchar", "guchar", "gshort", "gint", "guint", "gfloat", + "gdouble", "gsize", "gssize", "gintptr", "guintptr", + "glong", "gulong", "gint64", "guint64", "long double", + "long long", "unsigned long long"]: + return "Number" # gsize and up cannot fully be represented in GJS + if name in ["none", "gpointer"]: + return "void" + if name in ["utf8", "gunichar", "filename"]: + return "String" + if name == "gboolean": + return "Boolean" + if name == "GType": + return "GObject.Type" + if name == "GVariant": + return "GLib.Variant" + return name + + def format_value(self, node): + # Constants only have fundamental types? + type_ = node.value_type.target_fundamental + if type_ in ["utf8", "gunichar", "filename"]: + return repr(node.value) + # escapes quotes in the string; ought to be the same in Javascript + return node.value + + def format(self, node, doc): + if doc is None: + return '' + + cleaned_up_gtkdoc = super(DevDocsFormatterGjs, self).format_inline(node, doc) + return markdown.markdown(cleaned_up_gtkdoc, extensions=[ + 'markdown.extensions.fenced_code', + 'markdown.extensions.nl2br', + 'markdown.extensions.attr_list', + HeaderIdExtension(forceid=False) + ]) + + def format_function_name(self, func): + name = func.name + if func.shadows: + name = func.shadows + + if isinstance(func, ast.VFunction): + return 'vfunc_' + name + return name + + def format_page_name(self, node): + if isinstance(node, ast.Function) and node.parent is not None: + return node.parent.name + "." + self.format_function_name(node) + return super(DevDocsFormatterGjs, self).format_page_name(node) + + def _write_xref_markdown(self, target, anchor=None, display_name=None, pluralize=False): + if display_name is None: + display_name = target + link = target + ".html" + if anchor is not None: + link += "#" + anchor + return "[{}]({}){}".format(display_name, link, 's' if pluralize else '') + + def to_underscores(self, node): + try: + return super(DevDocsFormatterGjs, self).to_underscores(node) + except Exception as e: + if e.message == 'invalid node': + print('warning: invalid node in', node.parent.name, + file=sys.stderr) + return node.parent.name + '_invalid_node' + + def make_anchor(self, node): + style_class = get_node_kind(node) + return "{}-{}".format(style_class, self.to_underscores(node)) + + def _process_parameter(self, node, match, props): + # Display the instance parameter as "this" instead of whatever name it + # has in C. + if hasattr(node, 'instance_parameter') and \ + node.instance_parameter is not None and \ + props['param_name'] == node.instance_parameter.argname: + return '<code>this</code>' + return super(DevDocsFormatterGjs, self)._process_parameter(node, match, props) + + def format_xref(self, node, pluralize=False, **attrdict): + if node is None or not hasattr(node, 'namespace'): + return self._write_xref_markdown('index') + if node.namespace is self._transformer.namespace: + return self.format_internal_xref(node, attrdict, pluralize=pluralize) + return self.format_external_xref(node, attrdict, pluralize=pluralize) + + def format_internal_xref(self, node, attrdict, pluralize=False): + if not self.should_render_node(node): + # Non-toplevel nodes are linked to the main page. + page = make_page_id(node.parent) + name = node.name + if isinstance(node, ast.Member): + name = name.upper() + return self._write_xref_markdown(page, self.make_anchor(node), + page + "." + name, + pluralize=pluralize) + return self._write_xref_markdown(make_page_id(node), pluralize=pluralize) + + def format_external_xref(self, node, attrdict, pluralize=False): + ns = node.namespace + slug = ns.name.lower() + str(ns.version).replace('.', '') + if not self.should_render_node(node): + target = 'gir:///%s/%s' % (slug, make_page_id(node.parent)) + return self._write_xref_markdown(target, self.make_anchor(node), + self.format_page_name(node.parent), + pluralize=pluralize) + target = 'gir:///%s/%s' % (slug, make_page_id(node)) + return self._write_xref_markdown(target, None, + self.format_page_name(node), + pluralize=pluralize) + + def format_inline(self, node, para): + if para is None: + return '' + cleaned_up_gtkdoc = super(DevDocsFormatterGjs, self).format_inline(node, para) + return markdown.markdown(cleaned_up_gtkdoc, extensions=[ + InlineMarkdown(), + 'markdown.extensions.fenced_code', + 'markdown.extensions.nl2br', + 'markdown.extensions.attr_list', + HeaderIdExtension(forceid=False) + ]) + + def format_in_parameters(self, node): + return ', '.join(p.argname for p in self.get_in_parameters(node)) + LANGUAGES = { + "devdocs": { + "gjs": DevDocsFormatterGjs, + }, "mallard": { "c": DocFormatterC, "python": DocFormatterPython, @@ -910,6 +1290,7 @@ class DocWriter(object): self._formatter = formatter_class(self._transformer) self._language = self._formatter.language + self._output_format = output_format self._lookup = self._get_template_lookup() @@ -968,6 +1349,7 @@ class DocWriter(object): node=node, page_id=page_id, page_kind=page_kind, + get_node_kind=get_node_kind, formatter=self._formatter, ast=ast) diff --git a/giscanner/mdextensions.py b/giscanner/mdextensions.py new file mode 100644 index 00000000..16af4fc3 --- /dev/null +++ b/giscanner/mdextensions.py @@ -0,0 +1,13 @@ +from markdown.extensions import Extension +from markdown.treeprocessors import Treeprocessor + + +class RemoveOuterP(Treeprocessor): + def run(self, root): + if len(root) == 1 and root[0].tag == "p": + root[0].tag = "span" + + +class InlineMarkdown(Extension): + def extendMarkdown(self, md, md_globals): + md.treeprocessors.add("remove_outer_p", RemoveOuterP(md), "_end") diff --git a/giscanner/transformer.py b/giscanner/transformer.py index 1a27aa3c..c3187e82 100644 --- a/giscanner/transformer.py +++ b/giscanner/transformer.py @@ -262,6 +262,7 @@ currently-scanned namespace is first.""" stderr=subprocess.PIPE) _name = name proc_name, err = proc.communicate(name.encode()) + proc_name = proc_name.strip() if proc.returncode: raise ValueError('filter: %r exited: %d with error: %s' % (self._symbol_filter_cmd, proc.returncode, err)) |