Source code for gavo.web.examplesrender

"""
The renderer for VOSI examples, plus the docutils extensions provided for
them.

If you have a renderer that needs custom text roles or directives, read the
docstring of misctricks.RSTExtensions and add whatever roles you need below,
more or less like this::

	misctricks.RSTExtensions.makeTextRole("niceRole")

Only go through RSTExtensions, as these will make sure HTML postprocessing
happens as required.

The reason we keep the roles here and not in the renderer modules where they'd
logically belong (and where they should be documented in the renderer
docstrings) is that we don't want docutils imports all over the place.
"""

#c Copyright 2008-2023, the GAVO project <gavo@ari.uni-heidelberg.de>
#c
#c This program is free software, covered by the GNU GPL.  See the
#c COPYING file in the source distribution.


import re

from docutils import nodes
from docutils import utils as rstutils
from docutils.parsers import rst

from lxml import etree

from twisted.web import template
from gavo import utils
from gavo.utils import misctricks
from gavo import base
from gavo import svcs
from gavo.web import grend


class _Example(base.MetaMixin):
	"""A formatted example.

	These get constructed with example meta items and glue these
	together with the nevow rendering system.

	An important role of this is the translation from the HTML class
	attribute values we use in ReStructuredText to the RDFa properties
	in the output.  The first class that has a matching property wins.

	There's the special exmeta render function that works like metahtml,
	except it's using the example's meta.
	"""
	def __init__(self, exMeta):
		base.MetaMixin.__init__(self)
		self.setMetaParent(exMeta)
		self.original = exMeta
		self.title = base.getMetaText(self.original, "title", propagate=False)
		self.htmlId = re.sub("\W", "", self.title)

	def _getTranslatedHTML(self):
		rawHTML = self.original.getContent("html")
		parsed = etree.fromstring(
			'<div typeof="example" id="%s" resource="#%s">\n'
			'<h2 property="name">%s</h2>\n'
			'%s\n</div>'%(
				self.htmlId,
				self.htmlId,
				utils.escapePCDATA(self.title),
				rawHTML))
		actOnClasses = set(misctricks.RSTExtensions.classToProperty)

		for node in parsed.iterfind(".//*[@class]"):
			nodeClasses = set(node.attrib["class"].split())
			properties = " ".join(misctricks.RSTExtensions.classToProperty[c]
				for c in actOnClasses & nodeClasses)
			if properties:
				node.set("property", properties)

			# For now, I assume element content always is intended to
			# be the relation object (rather than a href that might
			# be present and would take predence by RDFa
			if "href" in node.attrib or "src" in node.attrib:
				node.set("content", node.text)
	
		return etree.tostring(parsed, encoding="utf-8")


[docs]class Examples(grend.CustomTemplateMixin, grend.ServiceBasedPage): r"""A renderer for examples for service usage. This renderer formats _example meta items in its service. Its output is XHTML compliant to VOSI examples; clients can parse it to, for instance, fill forms for service operation or display examples to users. The examples make use of RDFa to convey semantic markup. To see what kind of semantics is contained, try http://www.w3.org/2012/pyRdfa/Overview.html and feed it the example URL of your service. The default content of _example is ReStructuredText, and, really, not much else makes sense. An example for such a meta item can be viewed by executing ``gavo admin dumpDF //userconfig``, in the tapexamples STREAM. To support annotation of things within the example text, DaCHS defines several RST extensions, both interpreted text roles (used like ``:role-name:`content with blanks```) and custom directives (used to mark up blocks introduced by a single line like ``.. directive-name ::`` (the blanks before and after the directive name are significant). Here's the custom interpreted text roles: * *dl-id*: An publisher DID a service returns data for (used in datalink examples) * *taptable*: A (fully qualified) table name a TAP example query is (particularly) relevant for; in HTML, this is also a link to the table description. * *genparam*: A "generic parameter" as defined by DALI. The values of these have the form param(value), e.g., :genparam:\`POS(32,4)\`. Right now, not parentheses are allowed in the value. Complain if this bites you. These are the custom directives: * *tapquery*: The query discussed in a TAP example. Examples for how to write TAP examples are in the userconfig.rd distributed with DaCHS. Examples for Datalink examples can be found in the GAVO RDs feros/q and califa/q3.A In addition, you can define moreExamples meta items. These point to further DALI-compliant examples document(s) and will typically be presented in a hierarchical fashion by clients. The content is either a URI to the examples document (when it starts with https?://) or a DaCHS-internal reference to a service with the examples. They should have a (short) title meta child to give clients a hint as to what the continuation is about, somewhat like this:: <meta> # more examples provided by the ex service in the RD rr/q moreExamples: rr/q#ex moreExamples.title: RegTAP # more examples provided by an external document moreExamples: http://ivoa.net/doc/obscore/tap-examples.xhtml moreExamples.title: ObsCore </meta> """ name = "examples" checkedRenderer = False customTemplate = svcs.loadSystemTemplate("examples.html") gavo_useDoctype = (b'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.1//EN"' b' "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-2.dtd">')
[docs] def render(self, request): # overridden to return a 404 when there are no examples, # which is what TOPCAT wants. self.serviceExamples = list( self.service.iterMeta("_example")) if not self.serviceExamples: request.setResponseCode(404) return grend.ServiceBasedPage.render(self, request)
[docs] @classmethod def isCacheable(self, segments, request): return True
[docs] @classmethod def isBrowseable(cls, service): return True
[docs] @template.renderer def title(self, request, tag): return tag["Examples for %s"%base.getMetaText( self.service, "title")]
[docs] @template.renderer def noexamples(self, request, tag): if not self.serviceExamples: return tag return ""
[docs] def data_continuations(self, request, tag): """yields dicts with title and uri keys from this service's continuation metadata. """ for item in self.service.iterMeta("moreExamples"): src = item.getContent(targetFormat="text") if not re.match("https?://", src): svc = base.resolveId(self.rd, src, forceType=svcs.Service) src = svc.getURL("examples") yield { "uri": src, "title": str(item.getMeta("title", default="More"))}
[docs] def data_examples(self, request, tag): """returns _Example instances from the service metadata. """ for ex in self.serviceExamples: yield _Example(ex)
[docs] def data_rendered(self, request, tag): """returns the current data (an example) as XHTML. """ data = tag.slotData if not hasattr(data, "renderedDescription"): data.renderedDescription = data._getTranslatedHTML() return data.renderedDescription
[docs] def data_id(self, request, tag): """returns the htmlId attribute of the current data (an example). """ return tag.slotData.htmlId
################## RST extensions # When you add anything here, be sure to update the Examples docstring # above. ### ...for TAP def _taptableRoleFunc(name, rawText, text, lineno, inliner, options={}, content=[]): tablename = nodes.emphasis(rawText, text) tablename["classes"] = ["dachs-ex-taptable"] descr = nodes.reference("\u2197", "\u2197", refuri="/tableinfo/%s"%text) descr["classes"] = ["taptable-link"] return [descr, tablename], [] misctricks.RSTExtensions.makeTextRole("taptable", _taptableRoleFunc, propertyName="table") del _taptableRoleFunc class _TAPQuery(rst.Directive): has_content = True def run(self): body = "\n".join(self.content) res = nodes.literal_block(body, body) res["classes"] = ["dachs-ex-tapquery"] return [res] misctricks.RSTExtensions.addDirective("tapquery", _TAPQuery, propertyName="query") del _TAPQuery ### ...for datalink misctricks.RSTExtensions.makeTextRole("dl-id") ### ...for DALI-style generic parameters def _genparamRoleFunc(name, rawText, text, lineno, inliner, options={}, content=[]): mat = re.match(r"([^(]+)\(([^)]*)\)$", text) if not mat: msg = inliner.reporter.error( "genparam content must have the form key(value); %s does not."%text) return [inliner.problematic(rawText, rawText, msg)], [msg] key, value = mat.groups() formatted = """<span property="generic-parameter" typeof="keyval" class="generic-parameter"> <span property="key" class="genparam-key">%s</span> = <span property="value" class="genparam-value">%s</span> </span>"""%( utils.escapePCDATA(rstutils.unescape(key, 1)), utils.escapePCDATA(rstutils.unescape(value, 1))) res = nodes.raw( rawsource=rawText, text=formatted, format='html') return [res], [] misctricks.RSTExtensions.makeTextRole("genparam", _genparamRoleFunc) del _genparamRoleFunc