diff --git a/_extensions/cps.py b/_extensions/cps/__init__.py similarity index 84% rename from _extensions/cps.py rename to _extensions/cps/__init__.py index cdad821..04e6d28 100644 --- a/_extensions/cps.py +++ b/_extensions/cps/__init__.py @@ -1,16 +1,19 @@ import os import re from dataclasses import dataclass -from typing import cast +from types import NoneType +from typing import Any, cast from docutils import nodes from docutils.parsers.rst import Directive, directives, roles from docutils.transforms import Transform +from docutils.utils.code_analyzer import Lexer, LexerError -from jsb import JsonSchema +import jsb from sphinx import addnodes, domains from sphinx.util import logging +from sphinx.util.docutils import SphinxRole from sphinx.util.nodes import clean_astext logger = logging.getLogger(__name__) @@ -49,8 +52,11 @@ def apply(self, **kwargs): if self.document.nameids.get(ref['refname']): continue - # Convert remaining non-external links to intra-document references refuri = ref['refuri'] if 'refuri' in ref else None + if refuri and refuri.startswith('./'): + continue + + # Convert remaining non-external links to intra-document references if self.is_internal_link(refuri): # Get the raw text (strip ``s and _) rawtext = re.sub('^`(.*)`_?$', '\\1', ref.rawsource) @@ -126,6 +132,8 @@ class AttributeDirective(Directive): 'overload': directives.flag, 'required': directives.flag, 'conditionally-required': directives.flag, + 'format': directives.unchanged, + 'default': directives.unchanged, } # ------------------------------------------------------------------------- @@ -165,9 +173,8 @@ def parse_type(self, typedesc): return content - m = re.match(r'^(list|map)[(](.*)[)]$', typedesc) - if m: - outer, inner = m.groups() + outer, inner = jsb.decompose_typedesc(typedesc) + if outer: content = [ make_code(outer, 'type'), nodes.Text(' of '), @@ -180,12 +187,33 @@ def parse_type(self, typedesc): return content + self.parse_type(inner) - elif typedesc in {'string'}: + elif typedesc in jsb.BUILTIN_TYPES: return [make_code(typedesc, 'type')] else: return [make_code(typedesc, 'object')] + # ------------------------------------------------------------------------- + def parse_data(self, data): + language = 'json' + classes = ['code', 'highlight', f'highlight-{language}'] + + try: + tokens = Lexer(nodes.unescape(data, True), language, 'short') + except LexerError as error: + msg = self.state.inliner.reporter.warning(error) + prb = self.state.inliner.problematic(data, data, msg) + return [prb] + + node = nodes.literal(data, '', classes=classes) + for classes, value in tokens: + if classes: + node += nodes.inline(value, value, classes=classes) + else: + node += nodes.Text(value) + + return [node] + # ------------------------------------------------------------------------- def run(self): name = self.arguments[0] @@ -194,6 +222,8 @@ def run(self): overload = 'overload' in self.options required = 'required' in self.options conditionally_required = 'conditionally-required' in self.options + typeformat = self.options.get('format') + default = self.options.get('default') if overload: target = f'{name} ({context[0]})' @@ -233,6 +263,9 @@ def run(self): fields += self.make_field( 'Required', required_text, [nodes.Text(required_text)]) section += fields + if default: + fields += self.make_field( + 'Default', default, self.parse_data(default)) # Parse attribute description content = nodes.Element() @@ -245,18 +278,29 @@ def run(self): # Record object on domain env = self.state.document.settings.env domain = cast(CpsDomain, env.get_domain('cps')) - domain.note_attribute(name, context, typedesc, required=required, - description=simplify_text(content), node=section) + domain.note_attribute(name, context, typedesc, typeformat, + required=required, default=default, + description=simplify_text(content), + node=section) # Return generated nodes return [section] +# ============================================================================= +class SchemaRole(SphinxRole): + def run(self): + uri = f'./{self.config.schema_filename}' + node = nodes.reference(self.rawtext, self.text, refuri=uri) + return [node], [] + # ============================================================================= @dataclass class Attribute: typedesc: str + typeformat: str description: str required: bool + default: Any # ============================================================================= class AttributeSet: @@ -293,7 +337,10 @@ def __init__(self, *args, **kwargs): self.directives['object'] = ObjectDirective self.directives['attribute'] = AttributeDirective - # Site-specific custom roles (these just apply styling) + # Site-specific roles + self.roles['schema'] = SchemaRole() + + # Additional site-specific roles (these just apply styling) self.add_role('hidden') self.add_role('applies-to') self.add_role('separator') @@ -326,9 +373,9 @@ def note_object(self, name, description): self.objects[name] = description # ------------------------------------------------------------------------- - def note_attribute(self, name, context, typedesc, - required, description, node): - a = Attribute(typedesc, description, required) + def note_attribute(self, name, context, typedesc, typeformat, + required, default, description, node): + a = Attribute(typedesc, typeformat, description, required, default) if name not in self.attributes: self.attributes[name] = AttributeSet(name, context, a, node) else: @@ -359,7 +406,7 @@ def write_schema(app, exception): title = f'{config.project} v{config.version}' domain = cast(CpsDomain, app.env.get_domain('cps')) - schema = JsonSchema(title, config.schema_id) + schema = jsb.JsonSchema(title, config.schema_id) object_attributes = {} for attribute_set in domain.attributes.values(): @@ -367,7 +414,9 @@ def write_schema(app, exception): schema.add_attribute( attribute_set.name, i, attribute.typedesc, + attribute.typeformat, attribute.description, + attribute.default, ) for context, attribute_ref in attribute_set.context.items(): @@ -385,7 +434,7 @@ def write_schema(app, exception): schema.add_object_type(name, description, object_attributes[name]) output_path = os.path.join(app.outdir, config.schema_filename) - schema.write(config.schema_root_object, output_path) + schema.write(config.schema_root_object, output_path, config.schema_indent) # ============================================================================= def setup(app): @@ -393,6 +442,7 @@ def setup(app): app.add_config_value('schema_id', '', '', [str]) app.add_config_value('schema_filename', 'schema.json', '', [str]) app.add_config_value('schema_root_object', None, '', [str]) + app.add_config_value('schema_indent', None, '', [NoneType, int]) # Add custom transform to resolve cross-file references app.add_transform(InternalizeLinks) diff --git a/_extensions/cps/pygments_styles.py b/_extensions/cps/pygments_styles.py new file mode 100644 index 0000000..944b559 --- /dev/null +++ b/_extensions/cps/pygments_styles.py @@ -0,0 +1,13 @@ +import pygments.token + +from sphinx.pygments_styles import SphinxStyle + +# ============================================================================= +class CpsDataStyle(SphinxStyle): + background_color = '' + + styles = SphinxStyle.styles + styles.update({ + pygments.token.Name.Tag: '#15a', + pygments.token.Literal.String.Double: '#d32', + }) diff --git a/_packages/jsb.py b/_packages/jsb.py index 91c9f85..4f10b28 100644 --- a/_packages/jsb.py +++ b/_packages/jsb.py @@ -1,6 +1,15 @@ import json import re +BUILTIN_TYPES = { + 'string', +} + +# ============================================================================= +def decompose_typedesc(typedesc): + m = re.match(r'^(list|map)[(](.*)[)]$', typedesc) + return m.groups() if m else (None, typedesc) + # ============================================================================= class JsonSchema: # ------------------------------------------------------------------------- @@ -33,9 +42,8 @@ def add_type(self, typedesc): return - m = re.match(r'^(list|map)[(](.*)[)]$', typedesc) - if m: - outer, inner = m.groups() + outer, inner = decompose_typedesc(typedesc) + if outer: self.add_type(inner) if outer == 'list': @@ -52,7 +60,7 @@ def add_type(self, typedesc): }, } - elif typedesc in {'string'}: + elif typedesc in BUILTIN_TYPES: # Handle simple (non-compound) types self.types[typedesc] = {'type': typedesc} @@ -62,12 +70,21 @@ def add_type(self, typedesc): pass # ------------------------------------------------------------------------- - def add_attribute(self, name, instance, typedesc, description): - self.attributes[f'{name}@{instance}'] = { + def add_attribute(self, name, instance, typedesc, typeformat, + description, default=None): + attr = { 'description': description, '$ref': f'#/definitions/types/{typedesc}', } + if typeformat: + attr['format'] = typeformat + + if default: + attr['default'] = json.loads(default) + + self.attributes[f'{name}@{instance}'] = attr + self.add_type(typedesc) # ------------------------------------------------------------------------- @@ -98,5 +115,9 @@ def _build_schema(self, root_object): return schema # ------------------------------------------------------------------------- - def write(self, root_object, path): - json.dump(self._build_schema(root_object), open(path, 'wt')) + def write(self, root_object, path, indent=None): + json.dump( + self._build_schema(root_object), + fp=open(path, 'wt'), + indent=indent, + ) diff --git a/conf.py b/conf.py index 09cc38c..2bd46e3 100644 --- a/conf.py +++ b/conf.py @@ -1,9 +1,5 @@ # -*- coding: utf-8 -*- -import os -import re -import sys - # -- General configuration ------------------------------------------------ needs_sphinx = '6.2' extensions = ['cps', 'autosectionlabel'] @@ -23,15 +19,16 @@ language = 'en' primary_domain = 'cps' -#default_role = None +# default_role = None -highlight_language = 'none' -pygments_style = 'sphinx' +highlight_language = 'json' +pygments_style = 'cps.pygments_styles.CpsDataStyle' # -- Options for JSON schema output --------------------------------------- schema_filename = 'cps.schema.json' schema_root_object = 'package' schema_id = 'https://cps-org.github.io/cps/' + schema_filename +schema_indent = 2 # -- Options for HTML output ---------------------------------------------- html_style = 'cps.css' diff --git a/pyproject.toml b/pyproject.toml index 4ee70cd..5691964 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ authors = ["The CPS Project <@>"] readme = "readme.md" packages = [ { include = "jsb.py", from = "_packages" }, - { include = "cps.py", from = "_extensions" }, + { include = "cps", from = "_extensions" }, { include = "autosectionlabel.py", from = "_extensions" }, ] diff --git a/sample.rst b/sample.rst index d6f3ed8..ff6bb0d 100644 --- a/sample.rst +++ b/sample.rst @@ -11,4 +11,4 @@ Many of the most commonly used attributes are demonstrated, as is the technique discussed in `Configurations as Flags`_. .. literalinclude:: sample.cps - :language: javascript + :language: json diff --git a/schema-supplement.rst b/schema-supplement.rst index 21dfdc2..eb8a4fb 100644 --- a/schema-supplement.rst +++ b/schema-supplement.rst @@ -90,6 +90,7 @@ By definition, none of the following attributes are required. .. cps:attribute:: website :type: string :context: package + :format: uri Specifies the URI at which the package's website may be found. diff --git a/schema.rst b/schema.rst index 3ff2561..b7d45e2 100644 --- a/schema.rst +++ b/schema.rst @@ -385,6 +385,7 @@ Attribute names are case sensitive. .. cps:attribute:: link_languages :type: list(string) :context: component configuration + :default: ["c"] Specifies the ABI language or languages of a static library (`type`_ :string:`"archive"`). @@ -392,7 +393,6 @@ Attribute names are case sensitive. :string:`"c"` (no special handling required) and :string:`"cpp"` (consuming the static library also requires linking against the C++ standard runtime). - The default is :string:`"c"`. .. ---------------------------------------------------------------------------- .. cps:attribute:: link_libraries @@ -594,6 +594,7 @@ Attribute names are case sensitive. .. cps:attribute:: version_schema :type: string :context: package + :default: "simple" Specifies the structure to which the package's version numbering conforms. @@ -605,7 +606,6 @@ Attribute names are case sensitive. If a package uses :string:`"custom"`, version numbers may be compared, but version ordering is not possible. - The default is :string:`"simple"`. Needless to say, changing a package's version scheme between releases @@ -659,6 +659,16 @@ Notes and using the platform's usual format for such binaries (ELF, PE32, etc.). +JSON Schema +''''''''''' + +A `JSON Schema`_ for CPS can be obtained :schema:`here`. +The schema is generated from this documentation, +and is intended to be used for machine validation of CPS files. +In case of discrepancies, this documentation takes precedence. +(That said, issue reports are welcomed and strongly encouraged; +please refer to our `Development Process`_.) + .. ... .. ... .. ... .. ... .. ... .. ... .. ... .. ... .. ... .. ... .. ... .. .. _Common Language Runtime: https://en.wikipedia.org/wiki/Common_Language_Runtime @@ -667,6 +677,8 @@ Notes .. _Java: https://en.wikipedia.org/wiki/Java_%28programming_language%29 +.. _JSON Schema: https://json-schema.org/ + .. _semver: http://semver.org/ .. ... .. ... .. ... .. ... .. ... .. ... .. ... .. ... .. ... .. ... .. ... ..