doc/ext/traffic-server.cmake.in.py (312 lines of code) (raw):

# -*- coding: utf-8 -*- # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ TS Sphinx Directives ~~~~~~~~~~~~~~~~~~~~~~~~~ Sphinx Docs directives for Apache Traffic Server :copyright: Copyright 2013 by the Apache Software Foundation :license: Apache """ from docutils import nodes from docutils.parsers import rst from docutils.parsers.rst import directives from sphinx.domains import Domain, ObjType, std from sphinx.roles import XRefRole from sphinx.locale import _ import sphinx import os import subprocess import re import yaml # 2/3 compat logic try: basestring def is_string_type(s): return isinstance(s, basestring) except NameError: def is_string_type(s): return isinstance(s, str) def get_code(data): # if needed we can add a type representer, # def float_representer(dumper, value): # return dumper.represent_scalar(u'tag:yaml.org,2002:float', str(value), style="'") # yaml.add_representer(float, float_representer) return yaml.dump(data) class TSConfVar(std.Target): """ Description of a traffic server configuration variable. Argument is the variable as defined in records.yaml. Descriptive text should follow, indented. Then the bulk description (if any) undented. This should be considered equivalent to the Doxygen short and long description. """ option_spec = { 'class': rst.directives.class_option, 'reloadable': rst.directives.flag, 'deprecated': rst.directives.flag, 'overridable': rst.directives.flag, 'units': rst.directives.unchanged, 'legacy': rst.directives.flag, } required_arguments = 3 optional_arguments = 1 # default is optional, special case if omitted final_argument_whitespace = True has_content = True def __generate_code(self, cv_name, cv_default, cv_type): def add_object(config, var, value, type=None): def get_value(type, value): def have_multipliers(value, mps): for m in mps: if value.endswith(m): return True return False if value is None or value == 'nullptr' or value == 'NULL': return None # We want to make the type right when inserting the element into the dict. if type == 'FLOAT': return float(value) elif type == 'INT': # We need to make it YAML compliant as this will be an int, so if contains # any special character like hex or a multiplier, then we make a string. ATS will # parse it as string anyway. if value.startswith('0x') or have_multipliers(value, ['K', 'M', 'G', 'T']): return str(value) else: return int(value) elif type == 'STRING': return str(value) return None obj = {} key = '' index = var.find('.') if index < 0: # last part config[var] = get_value(type, value) else: key = var[:index] if key not in config: config[key] = {} add_object(config[key], var[index + 1:], value, type=type) name = cv_name if name.startswith("proxy.config."): name = name[len("proxy.config."):] elif name.startswith("local.config."): name = name[len("local.config."):] ts = {} config = {} # Build the object add_object(config, name, cv_default, cv_type) ts['records'] = config code = get_code(ts) literal = nodes.literal_block(code, code) literal['linenos'] = True literal['force'] = True literal['language'] = 'yaml' literal['caption'] = 'records.yaml' return literal def make_field(self, tag, value): field = nodes.field() field.append(nodes.field_name(text=tag)) body = nodes.field_body() if is_string_type(value): body.append(sphinx.addnodes.compact_paragraph(text=value)) else: body.append(value) field.append(body) return field # External entry point def run(self): env = self.state.document.settings.env cv_default = None cv_scope, cv_name, cv_type = self.arguments[0:3] if (len(self.arguments) > 3): cv_default = self.arguments[3] # First, make a generic desc() node to be the parent. node = sphinx.addnodes.desc() node.document = self.state.document node['objtype'] = 'cv' # Next, make a signature node. This creates a permalink and a # highlighted background when the link is selected. title = sphinx.addnodes.desc_signature(cv_name, '') title['ids'].append(nodes.make_id(cv_name)) title['ids'].append(cv_name) title['names'].append(cv_name) title['first'] = False title['objtype'] = 'cv' self.add_name(title) title['classes'].append('ts-cv-title') # Finally, add a desc_name() node to display the name of the # configuration variable. title += sphinx.addnodes.desc_name(cv_name, cv_name) node.append(title) if ('class' in self.options): title['classes'].append(self.options.get('class')) # This has to be a distinct node before the title. if nested then # the browser will scroll forward to just past the title. nodes.target('', '', names=[cv_name]) # Second (optional) arg is 'msgNode' - no idea what I should pass for that # or if it even matters, although I now think it should not be used. self.state.document.note_explicit_target(title) env.domaindata['ts']['cv'][cv_name] = env.docname fl = nodes.field_list() fl.append(self.make_field('Scope', cv_scope)) fl.append(self.make_field('Type', cv_type)) if (cv_default): fl.append(self.make_field('Default', cv_default)) else: fl.append(self.make_field('Default', sphinx.addnodes.literal_emphasis(text='*NONE*'))) if ('units' in self.options): fl.append(self.make_field('Units', self.options['units'])) if ('reloadable' in self.options): fl.append(self.make_field('Reloadable', 'Yes')) if ('overridable' in self.options): fl.append(self.make_field('Overridable', 'Yes')) if ('deprecated' in self.options): fl.append(self.make_field('Deprecated', 'Yes')) # add yaml rep if record is not legacy. code_block = None code_block_title = None if 'legacy' not in self.options: code_block = self.__generate_code(cv_name, cv_default, cv_type) code_block_title = sphinx.addnodes.compact_paragraph(text="yaml:") self.add_name(code_block_title) self.add_name(code_block) # Get any contained content nn = nodes.compound() self.state.nested_parse(self.content, self.content_offset, nn) # Create an index node so that Sphinx adds this config variable to the # index. nodes.make_id() specifies the link anchor name that is # implicitly generated by the anchor node above. indexnode = sphinx.addnodes.index(entries=[]) if sphinx.version_info >= (1, 4): indexnode['entries'].append( ('single', _('%s') % cv_name, nodes.make_id(cv_name), '', '') ) else: indexnode['entries'].append( ('single', _('%s') % cv_name, nodes.make_id(cv_name), '') ) if code_block is None: return [indexnode, node, fl, nn] else: return [indexnode, node, fl, code_block_title, code_block, nn] class TSConfVarRef(XRefRole): def process_link(self, env, ref_node, explicit_title_p, title, target): return title, target def metrictypes(typename): return directives.choice(typename.lower(), ('counter', 'gauge', 'derivative', 'flag', 'text')) def metricunits(unitname): return directives.choice( unitname.lower(), ( 'ratio', 'percent', 'kbits', 'mbits', 'bytes', 'kbytes', 'mbytes', 'nanoseconds', 'microseconds', 'milliseconds', 'seconds')) class TSStat(std.Target): """ Description of a traffic server statistic. Argument is the JSON stat group ("global", etc.) in which the statistic is returned, then the statistic name as used by traffic_ctl/stats_over_http, followed by the value type of the statistic ('string', 'integer'), and finally an example value. Descriptive text should follow, indented. Then the bulk description (if any) undented. This should be considered equivalent to the Doxygen short and long description. """ option_spec = { 'type': metrictypes, 'units': metricunits, 'introduced': rst.directives.unchanged, 'deprecated': rst.directives.unchanged, 'ungathered': rst.directives.flag } required_arguments = 3 optional_arguments = 1 # example value is optional final_argument_whitespace = True has_content = True def make_field(self, tag, value): field = nodes.field() field.append(nodes.field_name(text=tag)) body = nodes.field_body() if is_string_type(value): body.append(sphinx.addnodes.compact_paragraph(text=value)) else: body.append(value) field.append(body) return field # External entry point def run(self): env = self.state.document.settings.env stat_example = None stat_group, stat_name, stat_type = self.arguments[0:3] if (len(self.arguments) > 3): stat_example = self.arguments[3] # First, make a generic desc() node to be the parent. node = sphinx.addnodes.desc() node.document = self.state.document node['objtype'] = 'stat' # Next, make a signature node. This creates a permalink and a # highlighted background when the link is selected. title = sphinx.addnodes.desc_signature(stat_name, '') title['ids'].append(nodes.make_id('stat-' + stat_name)) title['names'].append(stat_name) title['first'] = False title['objtype'] = 'stat' self.add_name(title) title['classes'].append('ts-stat-title') # Finally, add a desc_name() node to display the name of the # configuration variable. title += sphinx.addnodes.desc_name(stat_name, stat_name) node.append(title) # This has to be a distinct node before the title. if nested then # the browser will scroll forward to just past the title. nodes.target('', '', names=[stat_name]) # Second (optional) arg is 'msgNode' - no idea what I should pass for that # or if it even matters, although I now think it should not be used. self.state.document.note_explicit_target(title) env.domaindata['ts']['stat'][stat_name] = env.docname fl = nodes.field_list() fl.append(self.make_field('Collection', stat_group)) if ('type' in self.options): fl.append(self.make_field('Type', self.options['type'])) if ('units' in self.options): fl.append(self.make_field('Units', self.options['units'])) fl.append(self.make_field('Datatype', stat_type)) if ('introduced' in self.options and len(self.options['introduced']) > 0): fl.append(self.make_field('Introduced', self.options['introduced'])) if ('deprecated' in self.options): if (len(self.options['deprecated']) > 0): fl.append(self.make_field('Deprecated', self.options['deprecated'])) else: fl.append(self.make_field('Deprecated', 'Yes')) if ('ungathered' in self.options): fl.append(self.make_field('Gathered', 'No')) if (stat_example): fl.append(self.make_field('Example', stat_example)) # Get any contained content nn = nodes.compound() self.state.nested_parse(self.content, self.content_offset, nn) # Create an index node so that Sphinx adds this statistic to the # index. nodes.make_id() specifies the link anchor name that is # implicitly generated by the anchor node above. indexnode = sphinx.addnodes.index(entries=[]) if sphinx.version_info >= (1, 4): indexnode['entries'].append( ('single', _('%s') % stat_name, nodes.make_id(stat_name), '', '') ) else: indexnode['entries'].append( ('single', _('%s') % stat_name, nodes.make_id(stat_name), '') ) return [indexnode, node, fl, nn] class TSStatRef(XRefRole): def process_link(self, env, ref_node, explicit_title_p, title, target): return title, target class TrafficServerDomain(Domain): """ Apache Traffic Server Documentation. """ name = 'ts' label = 'Traffic Server' data_version = 2 object_types = { 'cv': ObjType(_('configuration variable'), 'cv'), 'stat': ObjType(_('statistic'), 'stat') } directives = {'cv': TSConfVar, 'stat': TSStat} roles = {'cv': TSConfVarRef(), 'stat': TSStatRef()} initial_data = { 'cv': {}, # full name -> docname 'stat': {} } dangling_warnings = { 'cv': "No definition found for configuration variable '%(target)s'", 'stat': "No definition found for statistic '%(target)s'" } def clear_doc(self, docname): cv_list = self.data['cv'] for var, doc in list(cv_list.items()): if doc == docname: del cv_list[var] stat_list = self.data['stat'] for var, doc in list(stat_list.items()): if doc == docname: del stat_list[var] def find_doc(self, key, obj_type): zret = None if obj_type == 'cv': obj_list = self.data['cv'] elif obj_type == 'stat': obj_list = self.data['stat'] else: obj_list = None if obj_list and key in obj_list: zret = obj_list[key] return zret def resolve_xref(self, env, src_doc, builder, obj_type, target, node, cont_node): dst_doc = self.find_doc(target, obj_type) if (dst_doc): return sphinx.util.nodes.make_refnode(builder, src_doc, dst_doc, nodes.make_id(target), cont_node, 'records.yaml') # Python 2/3 compat - iteritems is 2, items is 3 # Although perhaps the lists are small enough items could be used in Python 2. try: {}.iteritems() def get_objects(self): for var, doc in self.data['cv'].iteritems(): yield var, var, 'cv', doc, var, 1 for var, doc in self.data['stat'].iteritems(): yield var, var, 'stat', doc, var, 1 except AttributeError: def get_objects(self): for var, doc in self.data['cv'].items(): yield var, var, 'cv', doc, var, 1 for var, doc in self.data['stat'].items(): yield var, var, 'stat', doc, var, 1 # get the branch this documentation is building for in X.X.x form REPO_ROOT = '@PROJECT_SOURCE_DIR@' ts_version = '@TS_VERSION_STRING@' # get the current branch the local repository is on REPO_GIT_DIR = os.path.join(REPO_ROOT, ".git") git_branch = subprocess.check_output(['git', '--git-dir', REPO_GIT_DIR, 'rev-parse', '--abbrev-ref', 'HEAD']) def make_github_link(name, rawtext, text, lineno, inliner, options=None, content=None): """ This docutils role lets us link to source code via the handy :ts:git: markup. Link references are rooted at the top level source directory. All links resolve to GitHub. Examples: To link to proxy/Main.cc: Hi, here is a link to the proxy entry point: :ts:git:`proxy/Main.cc`. To link to CONTRIBUTING.md: If you want to contribute, take a look at :ts:git:`CONTRIBUTING.md`. """ if options is None: options = {} if content is None: content = [] url = 'https://github.com/apache/trafficserver/blob/{}/{}' ref = ts_version if ts_version == git_branch else 'master' node = nodes.reference(rawtext, text, refuri=url.format(ref, text), **options) return [node], [] def setup(app): app.add_crossref_type('configfile', 'file', objname='Configuration file', indextemplate='pair: %s; Configuration files') # Very ugly, but as of Sphinx 1.8 it must be done. There is an `override` option to add_crossref_type # but it only applies to the directive, not the role (`file` in this case). If this isn't cleared # explicitly the build will fail out due to the conflict. In this case, since the role action is the # same in all cases, the output is correct. This does assume the config file names and log files # names are disjoint sets. del app.registry.domain_roles['std']['file'] app.add_crossref_type('logfile', 'file', objname='Log file', indextemplate='pair: %s; Log files') rst.roles.register_generic_role('arg', nodes.emphasis) rst.roles.register_generic_role('const', nodes.literal) app.add_domain(TrafficServerDomain) # this lets us do :ts:git:`<file_path>` and link to the file on github app.add_role_to_domain('ts', 'git', make_github_link)