| # -*- 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['ts'] = 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-rep:") |
| 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) |