Пример #1
0
class MarkdownGenerator(DocFormatter):
    """Provides methods for generating markdown from Redfish schemas.

    Markdown is targeted to the Slate documentation tool: https://github.com/lord/slate
    """
    def __init__(self, property_data, traverser, config, level=0):
        super(MarkdownGenerator, self).__init__(property_data, traverser,
                                                config, level)
        self.separators = {'inline': ', ', 'linebreak': '\n', 'pattern': ', '}
        self.formatter = FormatUtils()
        self.layout_payloads = 'top'

        # Add some functions we'll use to selectively promote headings when the output mode is slate.
        self.format_head_two = self.formatter.head_two
        if self.markdown_mode == 'slate':
            self.format_head_two = self.formatter.head_one

        self.format_head_three = self.formatter.head_three
        if self.markdown_mode == 'slate':
            self.format_head_three = self.formatter.head_two

        self.format_head_four = self.formatter.head_four
        if self.markdown_mode == 'slate':
            self.format_head_four = self.formatter.head_three

    def format_property_row(self,
                            schema_ref,
                            prop_name,
                            prop_info,
                            prop_path=[],
                            in_array=False,
                            as_action_parameters=False,
                            in_schema_ref=None):
        """Format information for a single property.

        Returns an object with 'row', 'details', 'action_details', and 'profile_conditional_details':

        'row': content for the main table being generated.
        'details': content for the Property details section.
        'action_details': content for the Actions section.
        'profile_conditional_details': populated only in profile_mode, formatted conditional details

        This may include embedded objects with their own properties.
        """

        formatted = []  # The row itself

        within_action = prop_path == ['Actions']
        current_depth = len(prop_path)
        if in_array:
            current_depth = current_depth - 1

        # strip_top_object is used for fragments, to allow output of just the properties
        # without the enclosing object:
        if self.config.get('strip_top_object') and current_depth > 0:
            indentation_string = ' ' * 6 * (current_depth - 1)
        else:
            indentation_string = ' ' * 6 * current_depth

        # If prop_path starts with Actions and is more than 1 deep, we are outputting for an Actions
        # section and should dial back the indentation by one level.
        if len(prop_path) > 1 and prop_path[0] == 'Actions':
            indentation_string = ' ' * 6 * (current_depth - 1)

        collapse_array = False  # Should we collapse a list description into one row? For lists of simple types
        has_enum = False
        format_annotation = None

        if current_depth < self.current_depth:
            for i in range(current_depth, self.current_depth):
                if i in self.current_version:
                    del self.current_version[i]
        self.current_depth = current_depth
        parent_depth = current_depth - 1

        if isinstance(prop_info, list):
            has_enum = 'enum' in prop_info[0]
            is_excerpt = prop_info[0].get('_is_excerpt') or prop_info[0].get(
                'excerptCopy')
            translated_name = prop_info[0].get('translation')
            if 'format' in prop_info[0]:
                format_annotation = prop_info[0]['format']
        elif isinstance(prop_info, dict):
            has_enum = 'enum' in prop_info
            is_excerpt = prop_info.get('_is_excerpt')
            translated_name = prop_info.get('translation')
            if 'format' in prop_info:
                format_annotation = prop_info['format']

        format_annotation = self.format_annotation_strings.get(
            format_annotation, format_annotation)

        version_strings = self.format_version_strings(prop_info)
        if prop_name:
            name_and_version = self.formatter.bold(
                self.escape_for_markdown(prop_name,
                                         self.config.get('escape_chars', [])))
            if translated_name:
                name_and_version += ' ' + self.formatter.italic(
                    self.escape_for_markdown(
                        '(' + translated_name + ')',
                        self.config.get('escape_chars', [])))
        else:
            name_and_version = ''

        if version_strings['version_string']:
            name_and_version += ' ' + self.formatter.italic(
                version_strings['version_string'])
        deprecated_descr = version_strings['deprecated_descr']

        formatted_details = self.parse_property_info(schema_ref, prop_name,
                                                     prop_info, prop_path)

        if formatted_details.get('promote_me'):
            return ({
                'row': '\n'.join(formatted_details['item_description']),
                'details': formatted_details['prop_details'],
                'action_details': formatted_details.get('action_details')
            })

        if self.config.get('strip_top_object') and current_depth == 0:
            # In this case, we're done for this bit of documentation, and we just want the properties of this object.
            formatted.append('\n'.join(
                formatted_details['object_description']))
            return ({
                'row':
                '\n'.join(formatted),
                'details':
                formatted_details['prop_details'],
                'action_details':
                formatted_details.get('action_details'),
                'profile_conditional_details':
                formatted_details.get('profile_conditional_details')
            })

        # Eliminate dups in these these properties and join with a delimiter:
        props = {
            'prop_type': self.separators['inline'],
            'descr': self.separators['linebreak'],
            'object_description': self.separators['linebreak'],
            'item_description': self.separators['linebreak']
        }

        for property_name, delim in props.items():
            if isinstance(formatted_details[property_name], list):
                property_values = []
                self.append_unique_values(formatted_details[property_name],
                                          property_values)
                formatted_details[property_name] = delim.join(property_values)

        if formatted_details['prop_is_object'] and not in_array:
            if formatted_details['object_description'] == '':
                name_and_version += ' {}'
            else:
                name_and_version += ' {'

        if formatted_details['prop_is_array']:
            if formatted_details['item_description'] == '':
                if formatted_details['array_of_objects']:
                    name_and_version += ' [ {} ]'
                else:
                    name_and_version += ' [ ]'
            else:
                if formatted_details['array_of_objects']:
                    name_and_version += ' [ {'
                else:
                    collapse_array = True
                    name_and_version += ' [ ]'
        elif in_array:
            if formatted_details['prop_is_object']:
                name_and_version += ' [ { } ]'
            else:
                name_and_version += ' [ ]'

        if formatted_details['descr'] is None:
            formatted_details['descr'] = ''

        if formatted_details['profile_purpose'] and (
                self.config.get('profile_mode') != 'subset'):
            if formatted_details['descr']:
                formatted_details['descr'] += ' '
            formatted_details['descr'] += self.formatter.bold(
                formatted_details['profile_purpose'])

        if formatted_details['add_link_text']:
            if formatted_details['descr']:
                formatted_details['descr'] += ' '
            formatted_details['descr'] += formatted_details['add_link_text']

        # Append reference info to descriptions, if appropriate:
        if not formatted_details.get('fulldescription_override'):
            if formatted_details[
                    'has_direct_prop_details'] and not formatted_details[
                        'has_action_details']:
                # If there are prop_details (enum details), add a note to the description:
                if has_enum:
                    text_descr = _(
                        'For the possible property values, see %(link)s in Property details.'
                    ) % {
                        'link': prop_name
                    }
                else:
                    text_descr = _(
                        'For more information about this property, see Property details.'
                    )
                formatted_details['descr'] += ' ' + self.formatter.italic(
                    text_descr)

            if formatted_details['has_action_details']:
                text_descr = _(
                    'For more information, see the Actions section below.')
                formatted_details['descr'] += ' ' + self.formatter.italic(
                    text_descr)

        if deprecated_descr:
            formatted_details['descr'] += ' ' + self.formatter.italic(
                deprecated_descr)

        prop_type = formatted_details['prop_type']
        if has_enum:
            prop_type += '<br>(' + _('enum') + ')'

        if format_annotation:
            prop_type += '<br>(' + format_annotation + ')'

        if formatted_details['prop_units']:
            prop_type += '<br>(' + formatted_details['prop_units'] + ')'

        if is_excerpt:
            prop_type += '<br>(' + _('excerpt') + ')'

        if in_array:
            prop_type = 'array (' + prop_type + ')'

        if collapse_array:
            item_list = formatted_details['item_list']
            if len(item_list):
                if isinstance(item_list, list):
                    item_list = ', '.join(item_list)
                prop_type += ' (' + item_list + ')'

        prop_access = ''
        if (not formatted_details['prop_is_object']
                and not formatted_details.get('array_of_objects')
                and not as_action_parameters):
            if formatted_details['read_only']:
                prop_access = _('read-only')
            else:
                # Special case for subset mode; if profile indicates WriteRequirement === None (present and None),
                # emit read-only.
                if ((self.config.get('profile_mode') == 'subset')
                        and formatted_details.get('profile_write_req') and
                    (formatted_details['profile_write_req'] == 'None')):
                    prop_access = _('read-only')
                else:
                    prop_access = _('read-write')

        # Action parameters don't have read/write properties, but they can be required/optional.
        if as_action_parameters:
            if formatted_details['prop_required'] or formatted_details[
                    'required_parameter']:
                prop_access = _('required')
            else:
                prop_access = _('optional')
        else:
            if formatted_details['prop_required'] or formatted_details[
                    'required_parameter']:
                prop_access += ' ' + _('required')
            elif formatted_details['prop_required_on_create']:
                prop_access += ' ' + _('required on create')

        if formatted_details['nullable']:
            prop_access += '<br>' + _('(null)')

        # If profile reqs are present, massage them:
        profile_access = self.format_base_profile_access(formatted_details)

        if self.config.get('profile_mode'
                           ) and self.config.get('profile_mode') != 'subset':
            if profile_access:
                prop_type += '<br><br>' + self.formatter.italic(profile_access)
        elif prop_access:
            prop_type += '<br><br>' + self.formatter.italic(prop_access)

        row = []
        row.append(indentation_string + name_and_version)
        row.append(prop_type)
        row.append(formatted_details['descr'])

        formatted.append('| ' + ' | '.join(row) + ' |')

        if len(formatted_details['object_description']) > 0:
            formatted.append(formatted_details['object_description'])
            formatted.append('| ' + indentation_string + '} |   |   |')

        if not collapse_array and len(
                formatted_details['item_description']) > 0:
            formatted.append(formatted_details['item_description'])
            if formatted_details['array_of_objects']:
                formatted.append('| ' + indentation_string + '} ] |   |   |')
            else:
                formatted.append('| ' + indentation_string + '] |   |   |')

        return ({
            'row':
            '\n'.join(formatted),
            'details':
            formatted_details['prop_details'],
            'action_details':
            formatted_details.get('action_details'),
            'profile_conditional_details':
            formatted_details.get('profile_conditional_details')
        })

    def format_property_details(self,
                                prop_name,
                                prop_type,
                                prop_description,
                                enum,
                                enum_details,
                                supplemental_details,
                                parent_prop_info,
                                profile=None):
        """Generate a formatted table of enum information for inclusion in Property details."""

        contents = []

        parent_version = parent_prop_info.get('versionAdded')
        if parent_version:
            parent_version = self.format_version(parent_version)

        # Are we in profile mode? If so, consult the profile passed in for this property.
        # For Action Parameters, look for ParameterValues/RecommendedValues; for
        # Property enums, look for MinSupportValues/RecommendedValues.
        profile_mode = self.config.get('profile_mode')
        if profile_mode:
            if profile is None:
                profile = {}

            profile_values = profile.get('Values', [])
            profile_min_support_values = profile.get(
                'MinSupportValues', [])  # No longer a valid name?
            profile_parameter_values = profile.get('ParameterValues', [])
            profile_recommended_values = profile.get('RecommendedValues', [])

            # profile_all_values is not used. What were we going for here?
            profile_all_values = (profile_values + profile_min_support_values +
                                  profile_parameter_values +
                                  profile_recommended_values)

            # In subset mode, an action parameter with no Values (property) or ParameterValues (Action)
            # means all values are supported.
            # Otherwise, Values/ParameterValues specifies the set that should be listed.
            if profile_mode == 'subset':
                if len(profile_values):
                    enum = [x for x in enum if x in profile_values]
                elif len(profile_parameter_values):
                    enum = [x for x in enum if x in profile_parameter_values]

        if prop_description:
            contents.append(
                self.formatter.para(
                    self.escape_for_markdown(
                        prop_description, self.config.get('escape_chars',
                                                          []))))

        if isinstance(prop_type, list):
            prop_type = ', '.join(prop_type)

        if supplemental_details:
            contents.append('\n' + supplemental_details + '\n')

        enum_translations = parent_prop_info.get('enumTranslations', {})

        if enum_details:
            if profile_mode and profile_mode != 'subset':
                contents.append('| ' + prop_type + ' | ' + _('Description') +
                                ' | ' + _('Profile Specifies') + ' |')
                contents.append('| --- | --- | --- |')
            else:
                contents.append('| ' + prop_type + ' | ' + _('Description') +
                                ' |')
                contents.append('| --- | --- |')
            enum.sort(key=str.lower)
            for enum_item in enum:
                enum_name = enum_item
                enum_translation = enum_translations.get(enum_item)
                version = version_depr = deprecated_descr = None
                version_display = None
                if parent_prop_info.get('enumVersionAdded'):
                    version_added = parent_prop_info.get(
                        'enumVersionAdded').get(enum_name)
                    if version_added:
                        version = self.format_version(version_added)
                if parent_prop_info.get('enumVersionDeprecated'):
                    version_deprecated = parent_prop_info.get(
                        'enumVersionDeprecated').get(enum_name)
                    if version_deprecated:
                        version_depr = self.format_version(version_deprecated)
                if parent_prop_info.get('enumDeprecated'):
                    deprecated_descr = parent_prop_info.get(
                        'enumDeprecated').get(enum_name)

                if enum_translation:
                    enum_name += ' (' + enum_translation + ')'

                if version:
                    if not parent_version or DocGenUtilities.compare_versions(
                            version, parent_version) > 0:
                        version_display = self.truncate_version(version,
                                                                2) + '+'

                if version_display:
                    if version_depr:
                        deprecated_display = self.truncate_version(
                            version_depr, 2)
                        enum_name += ' ' + self.formatter.italic(
                            _('(v%(version_number)s, deprecated v%(deprecated_version)s)'
                              ) % {
                                  'version_number': version_display,
                                  'deprecated_version': deprecated_display
                              })
                        if deprecated_descr:
                            deprecated_descr = (_(
                                'Deprecated in v%(version_number)s and later. %(explanation)s'
                            ) % {
                                'version_number': deprecated_display,
                                'explanation': deprecated_descr
                            })
                    else:
                        enum_name += ' ' + self.formatter.italic(
                            _('(v%(version_number)s)') %
                            {'version_number': version_display})
                elif version_depr:
                    deprecated_display = self.truncate_version(version_depr, 2)
                    enum_name += ' ' + self.formatter.italic(
                        _('(deprecated v%(version_number)s)') %
                        {'version_number': deprecated_display})
                    if deprecated_descr:
                        deprecated_descr = (_(
                            'Deprecated in v%(version_number)s and later. %(explanation)s'
                        ) % {
                            'version_number': deprecated_display,
                            'explanation': deprecated_descr
                        })

                descr = enum_details.get(enum_item, '')
                if deprecated_descr:
                    if descr:
                        descr += ' ' + self.formatter.italic(deprecated_descr)
                    else:
                        descr = self.formatter.italic(deprecated_descr)

                if profile_mode and profile_mode != 'subset':
                    profile_spec = ''
                    # Note: don't wrap the following strings for trnaslation; self.text_map handles that.
                    if enum_item in profile_values:
                        profile_spec = 'Mandatory'
                    elif enum_item in profile_min_support_values:
                        profile_spec = 'Mandatory'
                    elif enum_item in profile_parameter_values:
                        profile_spec = 'Mandatory'
                    elif enum_item in profile_recommended_values:
                        profile_spec = 'Recommended'
                    contents.append('| ' + enum_name + ' | ' + descr + ' | ' +
                                    self.text_map(profile_spec) + ' |')
                else:
                    contents.append('| ' + enum_name + ' | ' + descr + ' |')

        elif enum:
            if profile_mode and profile_mode != 'subset':
                contents.append('| ' + prop_type + ' | ' +
                                _('Profile Specifies') + ' |')
                contents.append('| --- | --- |')
            else:
                contents.append('| ' + prop_type + ' |')
                contents.append('| --- |')
            for enum_item in enum:
                enum_name = enum_item
                version = version_depr = deprecated_descr = None
                version_display = None

                if parent_prop_info.get('enumVersionAdded'):
                    version_added = parent_prop_info.get(
                        'enumVersionAdded').get(enum_name)
                    if version_added:
                        version = self.format_version(version_added)

                if parent_prop_info('enumVersionDeprecated'):
                    version_deprecated = parent_prop_info.get(
                        'enumVersionDeprecated').get(enum_name)
                    if version_deprecated:
                        version_depr = self.format_version(version_deprecated)

                if parent_prop_info.get('enumDeprecated'):
                    deprecated_descr = parent_prop_info.get(
                        'enumDeprecated').get(enum_name)

                if version:
                    if not parent_version or DocGenUtilities.compare_versions(
                            version, parent_version) > 0:
                        version_text = html.escape(version, False)
                        version_display = self.truncate_version(
                            version_text, 2) + '+'

                if version_display:
                    if version_depr:
                        deprecated_display = self.truncate_version(
                            version_depr, 2)
                        if deprecated_descr:
                            enum_name += ' ' + self.formatter.italic(
                                _('(v%(version_number)s, deprecated v%(deprecated_version)s. %(explanation)s'
                                  ) % {
                                      'version_number': version_display,
                                      'deprecated_version': deprecated_display,
                                      'explanation': deprecated_descr
                                  })
                        else:
                            enum_name += ' ' + self.formatter.italic(
                                _('(v%(version_number)s, deprecated v%(deprecated_version)s)'
                                  ) % {
                                      'version_number': version_display,
                                      'deprecated_version': deprecated_display
                                  })

                    else:
                        enum_name += ' ' + self.formatter.italic(
                            _('(v%(version_number)s)') %
                            {'version_number': version_display})
                else:
                    if version_depr:
                        deprecated_display = self.truncate_version(
                            version_depr, 2)
                        if deprecated_descr:
                            enum_name += ' ' + self.formatter.italic(
                                _('Deprecated in v%(deprecated_version)s and later. %(explanation)s'
                                  ) % {
                                      'deprecated_version': deprecated_display,
                                      'explanation': deprecated_descr
                                  })
                        else:
                            enum_name += ' ' + self.formatter.italic(
                                _('(deprecated in v%(deprecated_version)s and later.)'
                                  ) %
                                {'deprecated_version': deprecated_display})

                if profile_mode and profile_mode != 'subset':
                    profile_spec = ''
                    # Note: don't wrap the following strings for trnaslation; self.text_map handles that.
                    if enum_name in profile_values:
                        profile_spec = 'Mandatory'
                    elif enum_name in profile_min_support_values:
                        profile_spec = 'Mandatory'
                    elif enum_name in profile_parameter_values:
                        profile_spec = 'Mandatory'
                    elif enum_name in profile_recommended_values:
                        profile_spec = 'Recommended'

                    contents.append('| ' + enum_name + ' | ' +
                                    self.text_map(profile_spec) + ' |')
                else:
                    contents.append('| ' + enum_name + ' | ')

        caption = self.formatter.add_table_caption(
            _("%(prop_name)s property values") % {'prop_name': prop_name})
        preamble = self.formatter.add_table_reference(
            _("The defined property values are listed in "))

        return preamble + '\n'.join(contents) + '\n' + caption

    def format_action_details(self, prop_name, action_details):
        """Generate a formatted Actions section from supplemental markup."""

        contents = []
        contents.append(
            self.format_head_four(action_details.get('action_name', prop_name),
                                  self.level))
        if action_details.get('text'):
            contents.append(action_details.get('text'))
        if action_details.get('example'):
            example = '```json\n' + action_details['example'] + '\n```\n'
            contents.append(_('Example Action POST:') + '\n')
            contents.append(example)

        return '\n'.join(contents) + '\n'

    def format_action_parameters(self,
                                 schema_ref,
                                 prop_name,
                                 prop_descr,
                                 action_parameters,
                                 profile,
                                 version_strings=None):
        """Generate a formatted Actions section from parameter data. """

        formatted = []
        version_string = deprecated_descr = None
        if version_strings:
            version_string = version_strings.get('version_string')
            deprecated_descr = version_strings.get('deprecated_descr')

        action_name = prop_name
        if prop_name.startswith('#'):  # expected
            # Example: from #Bios.ResetBios, we want prop_name "ResetBios" and action_name "Bios.ResetBios"
            prop_name_parts = prop_name.split('.')
            prop_name = prop_name_parts[-1]
            action_name = action_name[1:]

        name_and_version = prop_name
        if version_string:
            name_and_version += ' ' + self.formatter.italic(
                version_strings['version_string'])

        if self.markdown_mode == 'slate':
            formatted.append(
                self.formatter.head_five(name_and_version, self.level))
        else:
            formatted.append(
                self.formatter.head_three(name_and_version, self.level))

        if deprecated_descr:
            formatted.append(self.formatter.para(italic(deprecated_descr)))
        formatted.append(self.formatter.head_four(_("Description"),
                                                  self.level))
        formatted.append(self.formatter.para(prop_descr))

        # Add the URIs for this action.
        formatted.append(
            self.format_uri_block_for_action(action_name, self.current_uris))

        param_names = []

        if action_parameters:
            rows = []
            # Table start:
            rows.append("| " + _('Parameter Name') + "     | " + _('Type') +
                        "     | " + _('Notes') + "     |")
            rows.append("| --- | --- | --- |")

            param_names = [x for x in action_parameters.keys()]

            if self.config.get('profile_mode') == 'subset':
                if profile.get('Parameters'):
                    param_names = [
                        x for x in profile['Parameters'].keys()
                        if x in param_names
                    ]
                # If there is no profile for this action, all parameters should be output.

            param_names.sort(key=str.lower)

        heading = self.formatter.head_four(_("Action parameters"), self.level)
        if len(param_names):
            for param_name in param_names:
                formatted_parameters = self.format_property_row(
                    schema_ref, param_name, action_parameters[param_name],
                    ['Actions', prop_name], False, True)
                rows.append(formatted_parameters.get('row'))

            caption = self.formatter.add_table_caption(
                _("%(prop_name)s action parameters") %
                {'prop_name': prop_name})
            preamble = "\n" + heading + "\n\n" + self.formatter.add_table_reference(
                _("The parameters for the action which are included in the POST body to the URI shown in the 'target' property of the Action are summarized in "
                  ))
            formatted.append(preamble + "\n\n" + '\n'.join(rows) + "\n\n" +
                             caption)

        else:
            formatted.append(self.formatter.para(heading))
            formatted.append(
                self.formatter.para(_("This action takes no parameters.")))

        return "\n".join(formatted)

    def _format_profile_access(self,
                               read_only=False,
                               read_req=None,
                               write_req=None,
                               min_count=None):
        """Common formatting logic for profile_access column"""

        profile_access = ''
        if not self.config['profile_mode']:
            return profile_access

        # Each requirement  may be Mandatory, Recommended, IfImplemented, Conditional, or (None)
        if not read_req:
            read_req = 'Mandatory'  # This is the default if nothing is specified.
        if read_only:
            profile_access = self.formatter.nobr(
                self.text_map(read_req)) + ' ' + _('(Read-only)')
        elif read_req == write_req:
            profile_access = self.formatter.nobr(
                self.text_map(read_req)) + ' ' + _('(Read/Write)')
        elif not write_req:
            profile_access = self.formatter.nobr(
                self.text_map(read_req)) + ' ' + _('(Read)')
        else:
            # Presumably Read is Mandatory and Write is Recommended; nothing else makes sense.
            profile_access = (self.formatter.nobr(self.text_map(read_req)) +
                              ' ' + _('(Read)') + ',' +
                              self.formatter.nobr(self.text_map(write_req)) +
                              ' ' + _('(Read/Write)'))

        if min_count:
            if profile_access:
                profile_access += ", "
            profile_access += self.formatter.nobr(
                _('Minimum %(min_count)s') % {'min_count': str(min_count)})

        return profile_access

    def format_as_prop_details(self, prop_name, prop_description, rows):
        """ Take the formatted rows and other strings from prop_info, and create a formatted block suitable for the prop_details section """
        contents = []

        if prop_description:
            contents.append(
                self.formatter.para(
                    self.escape_for_markdown(
                        prop_description, self.config.get('escape_chars',
                                                          []))))

        obj_table = self.formatter.make_table(rows)
        contents.append(obj_table)

        return "\n".join(contents)

    def link_to_own_schema(self, schema_ref, schema_full_uri):
        """Format a reference to a schema."""
        result = super().link_to_own_schema(schema_ref, schema_full_uri)
        return self.formatter.italic(result)

    def link_to_outside_schema(self, schema_full_uri):
        """Format a reference to a schema_uri, which should be a valid URI"""
        return self.formatter.italic('[' + schema_full_uri + '](' +
                                     schema_full_uri + ')')

    def emit(self):
        """ Output contents thus far """

        contents = []

        for section in self.sections:
            contents.append(section.get('heading'))
            if section.get('release_history'):
                contents.append(section['release_history'])
            if section.get('conditional_requirements'):
                contents.append(section['conditional_requirements'])
            if section.get('deprecation_text'):
                contents.append(section['deprecation_text'])
            if section.get('description'):
                contents.append(section['description'])
            if section.get('uris'):
                contents.append(section['uris'])
            if section.get('json_payload') and (
                    self.markdown_mode
                    == 'slate'):  # If not slate, it goes at the end.:
                contents.append(section['json_payload'])

            # something is awry if there are no properties, but ...
            if section.get('properties'):
                caption = self.formatter.add_table_caption(section["head"] +
                                                           " properties")
                preamble = self.formatter.add_table_reference(
                    "The properties defined for the " + section["head"] +
                    " schema are summarized in ")

                # properties are a peer of URIs, if they exist
                # TODO: this should use make_table()
                contents.append(
                    '\n' + self.format_head_three(_('Properties'), self.level))
                contents.append(preamble + "\n")
                contents.append('|Property     |Type     |Notes     |')

                contents.append('| --- | --- | --- |')
                contents.append('\n'.join(section['properties']))
                contents.append(caption + '\n')

            if section.get('profile_conditional_details'):
                # sort them now; these can be sub-properties so may not be in alpha order.
                conditional_details = '\n'.join(
                    sorted(section['profile_conditional_details'],
                           key=str.lower))
                contents.append('\n' + self.format_head_three(
                    _('Conditional Requirements'), self.level))
                contents.append(conditional_details)

            if len(section.get('action_details', [])):
                contents.append('\n' +
                                self.format_head_three('Actions', self.level))
                contents.append('\n\n'.join(section.get('action_details')))
            if section.get('property_details'):
                contents.append(
                    '\n' +
                    self.format_head_three(_('Property details'), self.level))
                detail_names = [x for x in section['property_details'].keys()]
                detail_names.sort(key=str.lower)
                for detail_name in detail_names:
                    contents.append(self.format_head_four(
                        detail_name + ':', 0))
                    det_info = section['property_details'][detail_name]
                    if len(det_info) == 1:
                        for x in det_info.values():
                            contents.append(x['formatted_descr'])
                    else:
                        path_to_ref = {}
                        # Generate path descriptions and sort them.
                        for ref, info in det_info.items():
                            paths_as_text = [
                                ": ".join(x) for x in info['paths']
                            ]
                            paths_as_text = ', '.join(paths_as_text)
                            path_to_ref[paths_as_text] = ref
                        paths_sorted = [x for x in path_to_ref.keys()]
                        paths_sorted.sort(key=str.lower)
                        for path in paths_sorted:
                            info = det_info[path_to_ref[path]]
                            path_text = _("In %(path)s:") % {'path': path}
                            if self.markdown_mode == 'slate':
                                contents.append(
                                    self.formatter.para(
                                        self.formatter.bold(path_text)))
                            else:
                                contents.append(
                                    self.formatter.head_five(path_text))
                            contents.append(info['formatted_descr'])

            if section.get('json_payload') and (
                    self.markdown_mode !=
                    'slate'):  # Otherwise, this was inserted above.
                contents.append(section['json_payload'])

        self.sections = []

        # Profile output may include registry sections
        for section in self.registry_sections:
            contents.append(section.get('heading'))
            contents.append(section.get('requirement'))
            if section.get('description'):
                contents.append(self.formatter.para(section['description']))
            if section.get('messages'):
                contents.append(
                    self.format_head_three(_('Messages'), self.level))
                message_rows = [
                    self.formatter.make_row(x) for x in section['messages']
                ]
                header_cells = ['', _('Requirement')]
                if self.config.get('profile_mode') != 'terse':
                    header_cells.append(_('Description'))
                header_row = self.formatter.make_row(header_cells)
                contents.append(
                    self.formatter.make_table(message_rows, [header_row],
                                              'messages'))
                contents.append('\n')

        return '\n'.join(contents)

    def output_document(self):
        """Return full contents of document"""
        body = self.emit()
        common_properties = self.generate_common_properties_doc()

        supplemental = self.config.get('supplemental', {})

        if 'Title' in supplemental:
            doc_title = supplemental['Title']
        else:
            doc_title = _('Schema Documentation')

        prelude = "---\ntitle: " + doc_title + """

search: true
---
"""

        intro = self.config.get('intro_content')
        if intro:
            intro = self.process_intro(intro)
            prelude += '\n' + intro + '\n'

        contents = [prelude, body]
        postscript = self.config.get('postscript_content')
        if postscript:
            contents.append('\n' + postscript)

        output = '\n'.join(contents)
        if '[insert_common_objects]' in output:
            output = output.replace('[insert_common_objects]',
                                    common_properties, 1)

        if '[insert_collections]' in output:
            collections_doc = self.generate_collections_doc()
            output = output.replace('[insert_collections]', collections_doc, 1)

        # Replace pagebreak markers with HTML pagebreak markup
        output = output.replace('~pagebreak~',
                                '<p style="page-break-before: always"></p>')

        return output

    def process_intro(self, intro_blob):
        """ Process the intro text, generating and inserting any schema fragments """
        parts = []
        intro = []
        part_text = []

        fragment_config = {
            'output_format':
            'slate',
            'normative':
            self.config.get('normative'),
            'cwd':
            self.config.get('cwd'),
            'schema_supplement': {},
            'supplemental': {},
            'excluded_annotations': [],
            'excluded_annotations_by_match': [],
            'excluded_properties': [],
            'excluded_by_match': [],
            'excluded_schemas': [],
            'excluded_schemas_by_match': [],
            'escape_chars': [],
            'schema_link_replacements': {},
            'units_translation':
            self.config.get('units_translation'),
            'profile':
            self.config.get('profile'),
            'profile_mode':
            self.config.get('profile_mode'),
            'profile_resources':
            self.config.get('profile_resources', {}),
            'wants_common_objects':
            self.config.get('wants_common_objects'),
            'actions_in_property_table':
            self.config.get('actions_in_property_table', True),
        }

        for line in intro_blob.splitlines():
            if line.startswith('#include_fragment'):
                if len(part_text):
                    parts.append({
                        'type': 'markdown',
                        'content': '\n'.join(part_text)
                    })
                    part_text = []
                    fragment_id = line[17:].strip()
                    fragment_content = self.generate_fragment_doc(
                        fragment_id, fragment_config)
                    parts.append({
                        'type': 'fragment',
                        'content': fragment_content
                    })
            else:
                part_text.append(line)

        if len(part_text):
            parts.append({'type': 'markdown', 'content': '\n'.join(part_text)})

        for part in parts:
            if part['type'] == 'markdown':
                intro.append(part['content'])
            elif part['type'] == 'fragment':
                intro.append(part['content'])
        return '\n'.join(intro)

    def add_section(self, text, link_id=False, schema_ref=False):
        """ Add a top-level heading """

        self.this_section = {
            'properties': [],
            'property_details': {},
            'head': '',
            'heading': '',
            'schema_ref': '',
        }

        if text:
            self.this_section['head'] = text
            self.this_section['heading'] = '\n' + self.format_head_two(
                text, self.level)

        self.sections.append(self.this_section)

    def add_description(self, text):
        """ Add the schema description """
        self.this_section['description'] = self.format_head_three(
            _('Description'), self.level) + self.formatter.para(text)

    def add_deprecation_text(self, deprecation_text):
        """ Add deprecation text for a schema """
        depr_text = self.formatter.italic(
            _('This schema has been deprecated and use in new implementations is discouraged except to retain compatibility with existing products.'
              )) + ' ' + deprecation_text
        self.this_section['deprecation_text'] = depr_text + '\n'

    def add_uris(self, uris):
        """ Add the URIs (which should be a list) """
        uri_block = self.format_head_three(_('URIs'), self.level)
        for uri in sorted(uris, key=str.lower):
            uri_block += "\n" + self.format_uri(uri)
        self.this_section['uris'] = uri_block + "\n"

    def add_conditional_requirements(self, text):
        """ Add a conditional requirements, which should already be formatted """
        self.this_section['conditional_requirements'] = "\n**" + _(
            'Conditional Requirements') + ":**\n\n" + text + "\n"

    def format_uri_block_for_action(self, action, uris):
        """ Create a URI block for this action & the resource's URIs """
        uri_block = self.formatter.head_four(_("Action URIs"), self.level)
        for uri in sorted(uris, key=str.lower):
            uri = uri + "/Actions/" + action
            uri_block += "\n" + self.format_uri(uri)

        return uri_block

    def format_json_payload(self, json_payload):
        """ Format a json payload for output. """
        return '\n' + json_payload + '\n'

    def add_property_row(self, formatted_text):
        """Add a row (or group of rows) for an individual property in the current section/schema.

        formatted_row should be a chunk of text already formatted for output"""
        self.this_section['properties'].append(formatted_text)

    def add_registry_reqs(self, registry_reqs):
        """Add registry messages. registry_reqs includes profile annotations."""

        terse_mode = self.config.get('profile_mode') == 'terse'

        reg_names = [x for x in registry_reqs.keys()]
        reg_names.sort(key=str.lower)
        for reg_name in reg_names:
            reg = registry_reqs[reg_name]
            this_section = {
                'head': reg_name,
                'description': reg.get('Description', ''),
                'messages': []
            }
            heading = _('%(Name)s Registry v%(version_number)s+') % {
                'Name': reg_name,
                'version_number': reg['minversion']
            }
            if reg.get('current_release',
                       reg['minversion']) != reg['minversion']:
                heading += ' ' + (_('(current release: v%(version_number)s)') %
                                  {
                                      'version_number': reg['current_release']
                                  })

            this_section['heading'] = self.format_head_two(heading, self.level)
            this_section['requirement'] = _('Requirement: %(req)s') % {
                'req': reg.get('profile_requirement')
            }

            msgs = reg.get('Messages', {})
            msg_keys = [x for x in msgs.keys()]
            msg_keys.sort(key=str.lower)

            for msg in msg_keys:
                this_msg = msgs[msg]
                if terse_mode and not this_msg.get('profile_requirement'):
                    continue
                msg_row = [msg, this_msg.get('profile_requirement', '')]
                if not terse_mode:
                    msg_row.append(this_msg.get('Description', ''))
                this_section['messages'].append(msg_row)

            self.registry_sections.append(this_section)

    def escape_text(self, text, chars=None):
        """Escape text in whatever way is appropriate to this output format. """
        if chars is None:
            chars = []
        return self.escape_for_markdown(text, chars)

    @staticmethod
    def escape_for_markdown(text, chars):
        """Escape selected characters in text to prevent auto-formatting in markdown."""
        for char in chars:
            text = text.replace(char, '\\' + char)
        return text

    @staticmethod
    def escape_regexp(text):
        """If escaping is necessary to protect patterns when output format is rendered, do that."""
        chars_to_escape = r'\`*_{}[]()#+-.!|'
        escaped_text = ''
        for char in text:
            if char in chars_to_escape:
                escaped_text += '\\' + char
            else:
                escaped_text += char

        return escaped_text
Пример #2
0
class MarkdownGenerator(DocFormatter):
    """Provides methods for generating markdown from Redfish schemas.

    Markdown is targeted to the Slate documentation tool: https://github.com/lord/slate
    """
    def __init__(self, property_data, traverser, config, level=0):
        super(MarkdownGenerator, self).__init__(property_data, traverser,
                                                config, level)
        self.separators = {'inline': ', ', 'linebreak': '\n'}
        self.formatter = FormatUtils()

    def format_property_row(self,
                            schema_ref,
                            prop_name,
                            prop_info,
                            prop_path=[],
                            in_array=False):
        """Format information for a single property.

        Returns an object with 'row', 'details', 'action_details', and 'profile_conditional_details':

        'row': content for the main table being generated.
        'details': content for the Property Details section.
        'action_details': content for the Actions section.
        'profile_conditional_details': populated only in profile_mode, formatted conditional details

        This may include embedded objects with their own properties.
        """

        traverser = self.traverser
        formatted = []  # The row itself

        current_depth = len(prop_path)
        if in_array:
            current_depth = current_depth - 1

        # strip_top_object is used for fragments, to allow output of just the properties
        # without the enclosing object:
        if self.config.get('strip_top_object') and current_depth > 0:
            indentation_string = '&nbsp;' * 6 * (current_depth - 1)
        else:
            indentation_string = '&nbsp;' * 6 * current_depth

        # If prop_path starts with Actions and is more than 1 deep, we are outputting for an Action Details
        # section and should dial back the indentation by one level.
        if len(prop_path) > 1 and prop_path[0] == 'Actions':
            indentation_string = '&nbsp;' * 6 * (current_depth - 1)

        collapse_array = False  # Should we collapse a list description into one row? For lists of simple types
        has_enum = False

        if current_depth < self.current_depth:
            for i in range(current_depth, self.current_depth):
                if i in self.current_version:
                    del self.current_version[i]
        self.current_depth = current_depth
        parent_depth = current_depth - 1

        if isinstance(prop_info, list):
            meta = prop_info[0].get('_doc_generator_meta')
            has_enum = 'enum' in prop_info[0]
        elif isinstance(prop_info, dict):
            meta = prop_info.get('_doc_generator_meta')
            has_enum = 'enum' in prop_info
        if not meta:
            meta = {}

        # We want to modify a local copy of meta, deleting redundant version info
        meta = copy.deepcopy(meta)

        if prop_name:
            name_and_version = self.formatter.bold(
                self.escape_for_markdown(prop_name,
                                         self.config.get('escape_chars', [])))
        else:
            name_and_version = ''

        deprecated_descr = None

        version = meta.get('version')
        self.current_version[current_depth] = version

        # Don't display version if there is a parent version and this is not newer:
        if self.current_version.get(parent_depth) and version:
            version = meta.get('version')
            if DocGenUtilities.compare_versions(
                    version, self.current_version.get(parent_depth)) <= 0:
                del meta['version']

        if meta.get('version', '1.0.0') != '1.0.0':
            version_display = self.truncate_version(meta['version'], 2) + '+'
            if 'version_deprecated' in meta:
                deprecated_display = self.truncate_version(
                    meta['version_deprecated'], 2)
                name_and_version += ' ' + self.formatter.italic(
                    '(v' + version_display + ', deprecated v' +
                    deprecated_display + ')')
                deprecated_descr = ("Deprecated v" + deprecated_display +
                                    '+. ' + self.escape_for_markdown(
                                        meta['version_deprecated_explanation'],
                                        self.config.get('escape_chars', [])))
            else:
                name_and_version += ' ' + self.formatter.italic(
                    '(v' + version_display + ')')
        elif 'version_deprecated' in meta:
            deprecated_display = self.truncate_version(
                meta['version_deprecated'], 2)
            name_and_version += ' ' + self.formatter.italic(
                '(deprecated v' + deprecated_display + ')')
            deprecated_descr = ("Deprecated v" + deprecated_display + '+. ' +
                                self.escape_for_markdown(
                                    meta['version_deprecated_explanation'],
                                    self.config.get('escape_chars', [])))

        formatted_details = self.parse_property_info(schema_ref, prop_name,
                                                     prop_info, prop_path,
                                                     meta.get('within_action'))

        if formatted_details.get('promote_me'):
            return ({
                'row': '\n'.join(formatted_details['item_description']),
                'details': formatted_details['prop_details'],
                'action_details': formatted_details.get('action_details')
            })

        if self.config.get('strip_top_object') and current_depth == 0:
            # In this case, we're done for this bit of documentation, and we just want the properties of this object.
            formatted.append('\n'.join(
                formatted_details['object_description']))
            return ({
                'row':
                '\n'.join(formatted),
                'details':
                formatted_details['prop_details'],
                'action_details':
                formatted_details.get('action_details'),
                'profile_conditional_details':
                formatted_details.get('profile_conditional_details')
            })

        # Eliminate dups in these these properties and join with a delimiter:
        props = {
            'prop_type': self.separators['inline'],
            'descr': self.separators['linebreak'],
            'object_description': self.separators['linebreak'],
            'item_description': self.separators['linebreak']
        }

        for property_name, delim in props.items():
            if isinstance(formatted_details[property_name], list):
                property_values = []
                self.append_unique_values(formatted_details[property_name],
                                          property_values)
                formatted_details[property_name] = delim.join(property_values)

        if formatted_details['prop_is_object'] and not in_array:
            if formatted_details['object_description'] == '':
                name_and_version += ' {}'
            else:
                name_and_version += ' {'

        if formatted_details['prop_is_array']:
            if formatted_details['item_description'] == '':
                if formatted_details['array_of_objects']:
                    name_and_version += ' [ {} ]'
                else:
                    name_and_version += ' [ ]'
            else:
                if formatted_details['array_of_objects']:
                    name_and_version += ' [ {'
                else:
                    collapse_array = True
                    name_and_version += ' [ ]'
        elif in_array:
            if formatted_details['prop_is_object']:
                name_and_version += ' [ { } ]'
            else:
                name_and_version += ' [ ]'

        if formatted_details['descr'] is None:
            formatted_details['descr'] = ''

        if formatted_details['profile_purpose']:
            if formatted_details['descr']:
                formatted_details['descr'] += ' '
            formatted_details['descr'] += self.formatter.bold(
                formatted_details['profile_purpose'])

        if formatted_details['descr'] is None:
            formatted_details['descr'] = ''

        if formatted_details['profile_purpose']:
            if formatted_details['descr']:
                formatted_details['descr'] += ' '
            formatted_details['descr'] += self.formatter.bold(
                formatted_details['profile_purpose'])

        if formatted_details['add_link_text']:
            if formatted_details['descr']:
                formatted_details['descr'] += ' '
            formatted_details['descr'] += formatted_details['add_link_text']

        # Append reference info to descriptions, if appropriate:
        if not formatted_details.get('fulldescription_override'):
            if formatted_details[
                    'has_direct_prop_details'] and not formatted_details[
                        'has_action_details']:
                # If there are prop_details (enum details), add a note to the description:
                if has_enum:
                    text_descr = 'See ' + prop_name + ' in Property Details, below, for the possible values of this property.'
                else:
                    text_descr = 'See Property Details, below, for more information about this property.'
                formatted_details['descr'] += ' ' + self.formatter.italic(
                    text_descr)

            if formatted_details['has_action_details']:
                text_descr = 'For more information, see the Action Details section below.'
                formatted_details['descr'] += ' ' + self.formatter.italic(
                    text_descr)

        if deprecated_descr:
            formatted_details['descr'] += ' ' + self.formatter.italic(
                deprecated_descr)

        prop_type = formatted_details['prop_type']
        if has_enum:
            prop_type += '<br>(enum)'

        if formatted_details['prop_units']:
            prop_type += '<br>(' + formatted_details['prop_units'] + ')'

        if in_array:
            prop_type = 'array (' + prop_type + ')'

        if collapse_array:
            item_list = formatted_details['item_list']
            if len(item_list):
                if isinstance(item_list, list):
                    item_list = ', '.join(item_list)
                prop_type += ' (' + item_list + ')'

        prop_access = ''
        if not formatted_details['prop_is_object']:
            if formatted_details['read_only']:
                prop_access = 'read-only'
            else:
                prop_access = 'read-write'

        if formatted_details['prop_required_on_create']:
            prop_access += ' required on create'
        elif formatted_details['prop_required'] or formatted_details[
                'required_parameter']:
            prop_access += ' required'

        if formatted_details['nullable']:
            prop_access += '<br>(null)'

        # If profile reqs are present, massage them:
        profile_access = self.format_base_profile_access(formatted_details)

        if self.config.get('profile_mode'):
            if profile_access:
                prop_type += '<br><br>' + self.formatter.italic(profile_access)
        elif prop_access:
            prop_type += '<br><br>' + self.formatter.italic(prop_access)

        row = []
        row.append(indentation_string + name_and_version)
        row.append(prop_type)
        row.append(formatted_details['descr'])

        formatted.append('| ' + ' | '.join(row) + ' |')

        if len(formatted_details['object_description']) > 0:
            formatted.append(formatted_details['object_description'])
            formatted.append('| ' + indentation_string + '} |   |   |')

        if not collapse_array and len(
                formatted_details['item_description']) > 0:
            formatted.append(formatted_details['item_description'])
            if formatted_details['array_of_objects']:
                formatted.append('| ' + indentation_string + '} ] |   |   |')
            else:
                formatted.append('| ' + indentation_string + '] |   |   |')

        return ({
            'row':
            '\n'.join(formatted),
            'details':
            formatted_details['prop_details'],
            'action_details':
            formatted_details.get('action_details'),
            'profile_conditional_details':
            formatted_details.get('profile_conditional_details')
        })

    def format_property_details(self,
                                prop_name,
                                prop_type,
                                prop_description,
                                enum,
                                enum_details,
                                supplemental_details,
                                meta,
                                anchor=None,
                                profile=None):
        """Generate a formatted table of enum information for inclusion in Property Details."""

        contents = []
        contents.append(self.formatter.head_three(prop_name + ':', self.level))

        parent_version = meta.get('version')
        enum_meta = meta.get('enum', {})

        # Are we in profile mode? If so, consult the profile passed in for this property.
        # For Action Parameters, look for ParameterValues/RecommendedValues; for
        # Property enums, look for MinSupportValues/RecommendedValues.
        profile_mode = self.config.get('profile_mode')
        if profile_mode:
            if profile is None:
                profile = {}

            profile_values = profile.get('Values', [])
            profile_min_support_values = profile.get('MinSupportValues', [])
            profile_parameter_values = profile.get('ParameterValues', [])
            profile_recommended_values = profile.get('RecommendedValues', [])

            profile_all_values = (profile_values + profile_min_support_values +
                                  profile_parameter_values +
                                  profile_recommended_values)

        if prop_description:
            contents.append(
                self.formatter.para(
                    self.escape_for_markdown(
                        prop_description, self.config.get('escape_chars',
                                                          []))))

        if isinstance(prop_type, list):
            prop_type = ', '.join(prop_type)

        if supplemental_details:
            contents.append('\n' + supplemental_details + '\n')

        if enum_details:
            if profile_mode:
                contents.append('| ' + prop_type +
                                ' | Description | Profile Specifies |')
                contents.append('| --- | --- | --- |')
            else:
                contents.append('| ' + prop_type + ' | Description |')
                contents.append('| --- | --- |')
            enum.sort(key=str.lower)
            for enum_item in enum:
                enum_name = enum_item
                enum_item_meta = enum_meta.get(enum_item, {})
                version_display = None
                deprecated_descr = None
                if 'version' in enum_item_meta:
                    version = enum_item_meta['version']
                    if not parent_version or DocGenUtilities.compare_versions(
                            version, parent_version) > 0:
                        version_display = self.truncate_version(version,
                                                                2) + '+'
                if version_display:
                    if 'version_deprecated' in enum_item_meta:
                        version_depr = enum_item_meta['version_deprecated']
                        deprecated_display = self.truncate_version(
                            version_depr, 2)
                        enum_name += ' ' + self.formatter.italic(
                            '(v' + version_display + ', deprecated v' +
                            deprecated_display + ')')
                        if enum_item_meta.get(
                                'version_deprecated_explanation'):
                            deprecated_descr = (
                                "Deprecated v" + deprecated_display + '+. ' +
                                enum_item_meta[
                                    'version_deprecated_explanation'])
                    else:
                        enum_name += ' ' + self.formatter.italic(
                            '(v' + version_display + ')')
                else:
                    if 'version_deprecated' in enum_item_meta:
                        version_depr = enum_item_meta['version_deprecated']
                        deprecated_display = self.truncate_version(
                            version_depr, 2)
                        enum_name += ' ' + self.formatter.italic(
                            '(deprecated v' + deprecated_display + ')')
                        if enum_item_meta.get(
                                'version_deprecated_explanation'):
                            deprecated_descr = (
                                "Deprecated v" + deprecated_display + '+. ' +
                                enum_item_meta[
                                    'version_deprecated_explanation'])
                descr = enum_details.get(enum_item, '')
                if deprecated_descr:
                    if descr:
                        descr += ' ' + self.formatter.italic(deprecated_descr)
                    else:
                        descr = self.formatter.italic(deprecated_descr)

                if profile_mode:
                    profile_spec = ''
                    if enum_name in profile_values:
                        profile_spec = 'Required'
                    elif enum_name in profile_min_support_values:
                        profile_spec = 'Required'
                    elif enum_name in profile_parameter_values:
                        profile_spec = 'Required'
                    elif enum_name in profile_recommended_values:
                        profile_spec = 'Recommended'
                    contents.append('| ' + enum_name + ' | ' + descr + ' | ' +
                                    profile_spec + ' |')
                else:
                    contents.append('| ' + enum_name + ' | ' + descr + ' |')

        elif enum:
            if profile_mode:
                contents.append('| ' + prop_type + ' | Profile Specifies |')
                contents.append('| --- | --- |')
            else:
                contents.append('| ' + prop_type + ' |')
                contents.append('| --- |')
            for enum_item in enum:
                enum_name = enum_item
                enum_item_meta = enum_meta.get(enum_item, {})
                version_display = None

                if 'version' in enum_item_meta:
                    version = enum_item_meta['version']
                    if not parent_version or DocGenUtilities.compare_versions(
                            version, parent_version) > 0:
                        version_display = self.truncate_version(version,
                                                                2) + '+'
                if version_display:
                    if 'version_deprecated' in enum_item_meta:
                        version_depr = enum_item_meta['version_deprecated']
                        deprecated_display = self.truncate_version(
                            version_depr, 2)
                        enum_name += ' ' + self.formatter.italic(
                            '(v' + version_display + ', deprecated v' +
                            deprecated_display + ')')
                        if enum_item_meta.get(
                                'version_deprecated_explanation'):
                            deprecated_descr = (
                                'Deprecated v' + deprecated_display + '+. ' +
                                enum_item_meta[
                                    'version_deprecated_explanation'])
                    else:
                        enum_name += ' ' + self.formatter.italic(
                            '(v' + version_display + ')')
                else:
                    if 'version_deprecated' in enum_item_meta:
                        version_depr = enum_item_meta['version_deprecated']
                        deprecated_display = self.truncate_version(
                            version_depr, 2)
                        enum_name += ' ' + self.formatter.italic(
                            '(deprecated v' + deprecated_display + ')')
                        if enum_item_meta.get(
                                'version_deprecated_explanation'):
                            enum_name += ' ' + self.formatter.italic(
                                'Deprecated v' + deprecated_display + '+. ' +
                                enum_item_meta['version_deprecated_explanation']
                            )

                if profile_mode:
                    profile_spec = ''
                    if enum_name in profile_values:
                        profile_spec = 'Required'
                    elif enum_name in profile_min_support_values:
                        profile_spec = 'Required'
                    elif enum_name in profile_parameter_values:
                        profile_spec = 'Required'
                    elif enum_name in profile_recommended_values:
                        profile_spec = 'Recommended'

                    contents.append('| ' + enum_name + ' | ' + profile_spec +
                                    ' |')
                else:
                    contents.append('| ' + enum_name + ' | ')

        return '\n'.join(contents) + '\n'

    def format_action_details(self, prop_name, action_details):
        """Generate a formatted Actions section from supplemental markup."""

        contents = []
        contents.append(
            self.formatter.head_three(
                action_details.get('action_name', prop_name), self.level))
        if action_details.get('text'):
            contents.append(action_details.get('text'))
        if action_details.get('example'):
            example = '```json\n' + action_details['example'] + '\n```\n'
            contents.append('Example Action POST:\n')
            contents.append(example)

        return '\n'.join(contents) + '\n'

    def format_action_parameters(self, schema_ref, prop_name, prop_descr,
                                 action_parameters):
        """Generate a formatted Actions section from parameter data. """

        formatted = []

        if prop_name.startswith('#'):  # expected
            prop_name_parts = prop_name.split('.')
            prop_name = prop_name_parts[-1]

        formatted.append(self.formatter.head_four(prop_name, self.level))
        formatted.append(self.formatter.para(prop_descr))

        if action_parameters:
            rows = []
            # Table start:
            rows.append("|     |     |     |")
            rows.append("| --- | --- | --- |")

            # Add a "start object" row for this parameter:
            rows.append('| ' + ' | '.join(['{', ' ', ' ', ' ']) + ' |')
            param_names = [x for x in action_parameters.keys()]
            param_names.sort(key=str.lower)
            for param_name in param_names:
                formatted_parameters = self.format_property_row(
                    schema_ref, param_name, action_parameters[param_name],
                    ['Actions', prop_name])
                rows.append(formatted_parameters.get('row'))

            # Add a closing } row:
            rows.append('| ' + ' | '.join(['}', ' ', ' ', ' ']) + ' |')

            formatted.append(
                self.formatter.para(
                    'The following table shows the parameters for the action which are included in the POST body to the URI shown in the "target" property of the Action.'
                ))

            formatted.append('\n'.join(rows))

        else:
            formatted.append(
                self.formatter.para("(This action takes no parameters.)"))

        return "\n".join(formatted)

    def _format_profile_access(self,
                               read_only=False,
                               read_req=None,
                               write_req=None,
                               min_count=None):
        """Common formatting logic for profile_access column"""

        profile_access = ''
        if not self.config['profile_mode']:
            return profile_access

        # Each requirement  may be Mandatory, Recommended, IfImplemented, Conditional, or (None)
        if not read_req:
            read_req = 'Mandatory'  # This is the default if nothing is specified.
        if read_only:
            profile_access = self.formatter.nobr(
                self.text_map(read_req)) + ' (Read-only)'
        elif read_req == write_req:
            profile_access = self.formatter.nobr(
                self.text_map(read_req)) + ' (Read/Write)'
        elif not write_req:
            profile_access = self.formatter.nobr(
                self.text_map(read_req)) + ' (Read)'
        else:
            # Presumably Read is Mandatory and Write is Recommended; nothing else makes sense.
            profile_access = (self.formatter.nobr(self.text_map(read_req)) +
                              ' (Read),' +
                              self.formatter.nobr(self.text_map(write_req)) +
                              ' (Read/Write)')

        if min_count:
            if profile_access:
                profile_access += ", "
            profile_access += self.formatter.nobr("Minimum " + str(min_count))

        return profile_access

    def link_to_own_schema(self, schema_ref, schema_full_uri):
        """Format a reference to a schema."""
        result = super().link_to_own_schema(schema_ref, schema_full_uri)
        return self.formatter.italic(result)

    def link_to_outside_schema(self, schema_full_uri):
        """Format a reference to a schema_uri, which should be a valid URI"""
        return self.formatter.italic('[' + schema_full_uri + '](' +
                                     schema_full_uri + ')')

    def emit(self):
        """ Output contents thus far """

        contents = []

        for section in self.sections:
            contents.append(section.get('heading'))
            if section.get('description'):
                contents.append(section['description'])
            if section.get('uris'):
                contents.append(section['uris'])
            if section.get('json_payload'):
                contents.append(section['json_payload'])
            # something is awry if there are no properties, but ...
            if section.get('properties'):
                contents.append('|     |     |     |')
                contents.append('| --- | --- | --- |')
                contents.append('\n'.join(section['properties']))

            if section.get('profile_conditional_details'):
                # sort them now; these can be sub-properties so may not be in alpha order.
                conditional_details = '\n'.join(
                    sorted(section['profile_conditional_details'],
                           key=str.lower))
                contents.append('\n' + self.formatter.head_two(
                    'Conditional Requirements', self.level))
                contents.append(conditional_details)

            if len(section.get('action_details', [])):
                contents.append(
                    '\n' +
                    self.formatter.head_two('Action Details', self.level))
                contents.append('\n\n'.join(section.get('action_details')))
            if section.get('property_details'):
                contents.append(
                    '\n' +
                    self.formatter.head_two('Property Details', self.level))
                contents.append('\n'.join(section['property_details']))

        self.sections = []

        # Profile output may include registry sections
        for section in self.registry_sections:
            contents.append(section.get('heading'))
            contents.append(section.get('requirement'))
            if section.get('description'):
                contents.append(self.formatter.para(section['description']))
            if section.get('messages'):
                contents.append(self.formatter.head_two(
                    'Messages', self.level))
                message_rows = [
                    self.formatter.make_row(x) for x in section['messages']
                ]
                header_cells = ['', 'Requirement']
                if self.config.get('profile_mode') != 'terse':
                    header_cells.append('Description')
                header_row = self.formatter.make_row(header_cells)
                contents.append(
                    self.formatter.make_table(message_rows, [header_row],
                                              'messages'))
                contents.append('\n')

        return '\n'.join(contents)

    def output_document(self):
        """Return full contents of document"""
        body = self.emit()
        common_properties = self.generate_common_properties_doc()

        supplemental = self.config.get('supplemental', {})

        if 'Title' in supplemental:
            doc_title = supplemental['Title']
        else:
            doc_title = 'Schema Documentation'

        prelude = "---\ntitle: " + doc_title + """

search: true
---
"""

        intro = supplemental.get('Introduction')
        if intro:
            intro = self.process_intro(intro)
            prelude += '\n' + intro + '\n'

        contents = [prelude, body]
        if 'Postscript' in supplemental:
            contents.append('\n' + supplemental['Postscript'])

        output = '\n'.join(contents)
        if '[insert_common_objects]' in output:
            output = output.replace('[insert_common_objects]',
                                    common_properties, 1)

        if '[insert_collections]' in output:
            collections_doc = self.generate_collections_doc()
            output = output.replace('[insert_collections]', collections_doc, 1)

        return output

    def process_intro(self, intro_blob):
        """ Process the intro text, generating and inserting any schema fragments """
        parts = []
        intro = []
        part_text = []

        fragment_config = {
            'output_format': 'markdown',
            'normative': self.config.get('normative'),
            'cwd': self.config.get('cwd'),
            'schema_supplement': {},
            'supplemental': {},
            'excluded_annotations': [],
            'excluded_annotations_by_match': [],
            'excluded_properties': [],
            'excluded_by_match': [],
            'excluded_schemas': [],
            'excluded_schemas_by_match': [],
            'escape_chars': [],
            'uri_replacements': {},
            'units_translation': self.config.get('units_translation'),
            'profile': self.config.get('profile'),
            'profile_mode': self.config.get('profile_mode'),
            'profile_resources': self.config.get('profile_resources', {}),
            'wants_common_objects': self.config.get('wants_common_objects'),
        }

        for line in intro_blob.splitlines():
            if line.startswith('#include_fragment'):
                if len(part_text):
                    parts.append({
                        'type': 'markdown',
                        'content': '\n'.join(part_text)
                    })
                    part_text = []
                    fragment_id = line[17:].strip()
                    fragment_content = self.generate_fragment_doc(
                        fragment_id, fragment_config)
                    parts.append({
                        'type': 'fragment',
                        'content': fragment_content
                    })
            else:
                part_text.append(line)

        if len(part_text):
            parts.append({'type': 'markdown', 'content': '\n'.join(part_text)})

        for part in parts:
            if part['type'] == 'markdown':
                intro.append(part['content'])
            elif part['type'] == 'fragment':
                intro.append(part['content'])
        return '\n'.join(intro)

    def add_section(self, text, link_id=False):
        """ Add a top-level heading """
        self.this_section = {
            'head': text,
            'heading': '\n' + self.formatter.head_one(text, self.level),
            'properties': [],
            'property_details': []
        }
        self.sections.append(self.this_section)

    def add_description(self, text):
        """ Add the schema description """
        self.this_section['description'] = text + '\n'

    def add_uris(self, uris):
        """ Add the URIs (which should be a list) """
        uri_block = "**URIs**:\n"
        for uri in sorted(uris, key=str.lower):
            uri_block += "\n" + self.format_uri(uri)

        self.this_section['uris'] = uri_block + "\n"

    def add_json_payload(self, json_payload):
        """ Add a JSON payload for the current section """
        if json_payload:
            self.this_section['json_payload'] = '\n' + json_payload + '\n'
        else:
            self.this_section['json_payload'] = None

    def add_property_row(self, formatted_text):
        """Add a row (or group of rows) for an individual property in the current section/schema.

        formatted_row should be a chunk of text already formatted for output"""
        self.this_section['properties'].append(formatted_text)

    def add_property_details(self, formatted_details):
        """Add a chunk of property details information for the current section/schema."""
        self.this_section['property_details'].append(formatted_details)

    def add_registry_reqs(self, registry_reqs):
        """Add registry messages. registry_reqs includes profile annotations."""

        terse_mode = self.config.get('profile_mode') == 'terse'

        reg_names = [x for x in registry_reqs.keys()]
        reg_names.sort(key=str.lower)
        for reg_name in reg_names:
            reg = registry_reqs[reg_name]
            this_section = {
                'head': reg_name,
                'description': reg.get('Description', ''),
                'messages': []
            }
            heading = reg_name + ' Registry v' + reg['minversion'] + '+'
            if reg.get('current_release',
                       reg['minversion']) != reg['minversion']:
                heading += ' (current release: v' + reg['current_release'] + ')'

            this_section['heading'] = self.formatter.head_one(
                heading, self.level)
            this_section['requirement'] = 'Requirement: ' + reg.get(
                'profile_requirement', '')

            msgs = reg.get('Messages', {})
            msg_keys = [x for x in msgs.keys()]
            msg_keys.sort(key=str.lower)

            for msg in msg_keys:
                this_msg = msgs[msg]
                if terse_mode and not this_msg.get('profile_requirement'):
                    continue
                msg_row = [msg, this_msg.get('profile_requirement', '')]
                if not terse_mode:
                    msg_row.append(this_msg.get('Description', ''))
                this_section['messages'].append(msg_row)

            self.registry_sections.append(this_section)

    @staticmethod
    def escape_for_markdown(text, chars):
        """Escape selected characters in text to prevent auto-formatting in markdown."""
        for char in chars:
            text = text.replace(char, '\\' + char)
        return text
Пример #3
0
class PropertyIndexGenerator(DocFormatter):
    """Provides methods for generating Property Index docs from Redfish schemas."""

    def __init__(self, property_data, traverser, config, level=0):
        """
        property_data: pre-processed schemas.
        traverser: SchemaTraverser object
        config: configuration dict
        """
        super(PropertyIndexGenerator, self).__init__(property_data, traverser, config, level)
        self.collapse_list_of_simple_type = False

        # If there's a file to write config to, check it now.
        self.write_config_fh = False
        if config.get('write_config_to'):
            try:
                config_out = open(config['write_config_to'], 'w', encoding="utf8")
                self.write_config_fh = config_out
            except (OSError) as ex:
                warnings.warn('Unable to open %(filename)s to write: %(message)s', {'fileanme': config['write_config_to'], 'message': str(ex)})

        self.properties_by_name = {}
        self.coalesced_properties = {}
        # Shorthand for the overrides.
        self.overrides = config.get('description_overrides', {})

        # Force some config here:
        self.config['omit_version_in_headers'] = True # This puts just the schema name in the section head.
        self.config['wants_common_objects'] = True

        # get the formatter, so we can use the appropriate markup.
        output_format = self.config.get('output_format', 'slate')
        if output_format == 'html':
            from format_utils import HtmlUtils
            self.formatter = HtmlUtils()
        else:  # CSV also uses the markdown formatter.
            from format_utils import FormatUtils
            self.formatter = FormatUtils()


    def emit(self):
        """ Return the data! """
        self.coalesce_properties()
        output_format = self.config.get('output_format', 'slate')
        output = ''
        frontmatter = self.config.get('intro_content', '')
        backmatter = self.config.get('postscript_content', '')
        if output_format == 'html':
            if frontmatter:
                output = self.formatter.markdown_to_html(frontmatter)
            else:
                output = self.formatter.head_one("Property Index", 0)
            output += self.format_tabular_output()
            output += self.formatter.markdown_to_html(backmatter)
            toc = self.generate_toc(output)
            if '[add_toc]' in output:
                output = output.replace('[add_toc]', toc, 1)

            output = self.add_html_boilerplate(output)

        if output_format in ['slate', 'markdown']:
            if frontmatter:
                output = frontmatter
            else:
                output = self.formatter.head_one(_('Property Index'), 0)
            output += self.format_tabular_output()
            output += backmatter

        if output_format == 'csv':
            output = self.output_csv()

        return output


    def add_section(self, text, link_id=False, schema_ref=False):
        """ Start gathering info for this schema. """

        self.this_section = {
            'properties': [],
            'property_details': {},
            'head': '',
            'heading': '',
            'schema_name': text
            }


    def format_property_row(self, schema_ref, prop_name, prop_info, prop_path=[], in_array=False, as_action_parameters=False,
                                in_schema_ref=None):
        """ Instead of formatting this data, add info to self.properties_by_name. """

        if not prop_name:
            # We've drilled down to a simple type.
            return

        within_action = prop_path == ['Actions']
        has_enum = False

        if isinstance(prop_info, list):
            has_enum = 'enum' in prop_info[0]
        elif isinstance(prop_info, dict):
            has_enum = 'enum' in prop_info

        if within_action:
            prop_name_parts = prop_name.split('.')
            if len(prop_name_parts) == 2:
                prop_name = _('%(property_name)s (Action)') % {'property_name': prop_name_parts[1]}

        details = self.parse_property_info(schema_ref, prop_name, prop_info, prop_path)

        schema_path_formatted = self.this_section['schema_name']
        schema_path = [ self.this_section['schema_name'] ]
        if len(prop_path):
            schema_path += prop_path

        prop_type = details.get('prop_type')
        if isinstance(prop_type, list):
            prop_type_values = []
            self.append_unique_values(prop_type, prop_type_values)
            prop_type = ', '.join(sorted(prop_type_values))

        if has_enum:
            prop_type += ' ' + _('(enum)')

        prop_units = details.get('prop_units')
        if prop_units:
            prop_type += self.formatter.br() + '(' + prop_units + ')'

        description_entry = {
            'schemas': [ schema_path ], 'prop_type': prop_type,
            }

        # Check for an override:
        override_description = False
        if self.overrides.get(prop_name):
            for override_entry in self.overrides.get(prop_name):
                if not override_entry.get('overrideDescription'):
                    continue
                if override_entry.get('globalOverride') and override_entry.get('type') == prop_type:
                    override_description = override_entry.get('overrideDescription')
                    if override_description:
                        break
                elif override_entry.get('type') == prop_type and '/'.join(schema_path) in override_entry.get('schemas', []):
                    override_description = override_entry.get('overrideDescription')
                    if override_description:
                        break


        if override_description:
            description_entry['description'] = override_description
        elif self.config.get('normative') and details.get('normative_descr'):
            description_entry['description'] = details.get('normative_descr')
        else:
            description_entry['description'] = details.get('descr')

        if prop_name not in self.properties_by_name:
            self.properties_by_name[prop_name] = []

        if description_entry['description']:
            self.properties_by_name[prop_name].append(description_entry)


    def append_unique_values(self, value_list, target_list):
        """ Unwind possibly-nested list, producing a list of unique strings found.

        We don't want nulls reflected in the property index!
        """
        super(PropertyIndexGenerator, self).append_unique_values(value_list, target_list)
        for i in range(0, len(target_list)):
            if target_list[i] == 'null':
                del target_list[i]


    def format_property_details(self, prop_name, prop_type, prop_description, enum, enum_details,
                                supplemental_details, parent_prop_info, profile=None):
        """ Handle enum information """
        pass


    def format_action_parameters(self, schema_ref, prop_name, prop_descr, action_parameters, profile, version_strings):
        """Generate a formatted Actions section from parameters data"""
        return ''


    def format_action_response(self, schema_ref, action_param_name, action_response):
        """Format the data from an actionResponse"""
        return ''


    def add_registry_reqs(self, registry_reqs):
        """ output doesn't include registry requirements. """
        pass


    # TODO: generate_toc is the same as in html_generator and could probably be moved to HtmlUtils
    def generate_toc(self, html_blob):
        """ Generate a TOC for an HTML blob (probably the body of this document) """

        toc = ''
        levels = ['h1', 'h2']
        parser = ToCParser(levels)
        parser.feed(html_blob)
        toc_data = parser.close()

        current_level = 0
        for entry in toc_data:
            level = levels.index(entry['level'])
            if level > current_level:
                toc += "<ul>\n"
            if level < current_level:
                toc += "</ul>\n"
            current_level = level

            toc += "<li>" + '<a href="#' + entry['link_id'] +'">' + entry['text'] + "</a></li>\n"

        while current_level > 0:
            current_level = current_level - 1
            toc += "</ul>\n"

        toc = '<div class="toc">' + "<ul>\n" + toc + "</ul>\n</div>\n"

        return toc


    def is_excluded(self, prop_name):
        """ True if prop_name is in the excluded or excluded-by-match list.

        Many properties are excluded in the parent doc_generator code, but for other output
        modes we sometimes include them in sub-properties. """
        if prop_name in self.config['excluded_properties']:
            return True
        if prop_name in self.config['excluded_by_match']:
            pass

        return False


    def coalesce_properties(self):
        """ Group the info in self.properties_by_name based on prop_type and description match. """

        # Group the property info by prop_name, type, description:
        coalesced_info = {}
        prop_names = self.exclude_prop_names(self.properties_by_name.keys(),
                                             self.config['excluded_properties'],
                                             self.config['excluded_by_match'])

        for property_name in prop_names:
            property_infos = self.properties_by_name[property_name]
            coalesced_info[property_name] = {}
            for info in property_infos:
                prop_type = info['prop_type']
                description = info['description']
                schemas = info['schemas']
                if prop_type not in coalesced_info[property_name]:
                    coalesced_info[property_name][prop_type] = {}
                if description not in coalesced_info[property_name][prop_type]:
                    coalesced_info[property_name][prop_type][description] = []
                coalesced_info[property_name][prop_type][description] += schemas

        self.coalesced_properties = coalesced_info


    def generate_updated_config(self):
        """ Update property_index_config data.

        Flag any properties that were found to have more than one type, or more than one
        description. If the property already appears in self.config
        and it has a globalDescription, flag only entries with a different *type*. """

        updated = copy.deepcopy(self.config)
        overrides = updated['description_overrides'] # NB: this should already be in place.

        # Sorting isn't necessary in this method, but it's nice to have for troubleshooting.
        property_names = sorted(self.coalesced_properties.keys(), key=str.lower)

        for prop_name in property_names:
            prop_config = overrides.get(prop_name)
            info = self.coalesced_properties[prop_name]
            prop_types = sorted(info.keys(), key=str.lower)

            # If we don't already have prop_config and we have multiple types, capture them all:
            num_prop_types = len(prop_types)

            done_with_prop_name = False
            if not prop_config and num_prop_types > 1:
                prop_config = overrides[prop_name] = []
                for prop_type in prop_types:
                    descriptions = sorted(info[prop_type].keys(), key=str.lower)
                    for description in descriptions:
                        schemas = info[prop_type][description]
                        found_entry = {
                            "type": prop_type,
                            "description": description,
                            'knownException': False,
                            "schemas": ['/'.join(x) for x in schemas]
                            }
                        prop_config.append(found_entry)
                done_with_prop_name = True

            else:
                for prop_type in prop_types:
                    descriptions = sorted(info[prop_type].keys(), key=str.lower)
                    num_descriptions = len(descriptions)
                    done_with_prop_type = False

                    if not prop_config:
                        # If we found multiple descriptions and we have no overrides, capture each:
                        if num_descriptions > 1:
                            prop_config = overrides[prop_name] = []
                            for description in descriptions:
                                schemas = info[prop_type][description]
                                found_entry = {
                                    "type": prop_type,
                                    "description": description,
                                    'knownException': False,
                                    "schemas": ['/'.join(x) for x in schemas]
                                    }
                                prop_config.append(found_entry)
                            done_with_prop_name = True

                    else:
                        self.update_config_for_prop_name_and_type(prop_name, prop_type, info, prop_config)

        return updated


    def update_config_for_prop_name_and_type(self, prop_name, prop_type, info, prop_config):
        """ Update a property name/type selection of prop_config based on coalesced info. Updates prop_config. """

        # Do we have a globalOverride for this prop_type? If so, we're done. Again.
        for over_info in prop_config:
            if over_info.get('type') == prop_type and over_info.get('globalOverride', False):
                return

        # check each entry against prop_config
        descriptions = sorted(info[prop_type].keys(), key=str.lower)
        for description in descriptions:
            self.update_config_for_prop_name_and_type_and_description(prop_name, prop_type, description, info, prop_config)


    def update_config_for_prop_name_and_type_and_description(self, prop_name, prop_type, description, info, prop_config):
        """ Update a property name/type/description selection of prop_config based on coalesced info. Updates prop_config. """

        """ Info is arranged by prop_name: prop_type: description: schemas (list).
        prop_config, conversely, is arranged as a list of dicts with keys schemas, type, description, overrideDescription, knownException.

        If we applied an override, the description in "info" will match the overrideDescription in prop_config. """

        config_by_schema = {}
        for config in prop_config:
            # Note, we ignore globalOverrides in this method.
            if config.get('type') == prop_type:
                for schema in config.get('schemas', []):
                    config_by_schema[schema] = config

        schemas = info[prop_type][description]
        for schema_path in schemas:
            schema_name = '/'.join(schema_path)

            if config_by_schema.get(schema_name):
                # We have an entry for this schema name. It's still good if it has an overrideDescription, or if the description matches.
                if config_by_schema[schema_name].get('overrideDescription'):
                    break
                elif config_by_schema[schema_name].get('description') == description:
                    break
                else:
                    config_by_schema[schema_name]['description'] = description
                    config_by_schema[schema_name]['knownException'] = False

            else:
                # If we already have this description, add the schema there.
                for config in prop_config:
                    if config.get('type') == prop_type and config.get('description') == description:
                        config['schemas'].append(schema_name)
                        break

                # We didn't find a matching description, so create a new entry:
                found_entry = {
                    "type": prop_type,
                    "description": description,
                    'knownException': False,
                    "schemas": [ schema_name ]
                    }
                prop_config.append(found_entry)
                config_by_schema[schema_name] = found_entry


    def escape_text(self, text, chars=None):
        """Escape text in whatever way is appropriate to this output format. """
        return html.escape(text, False)


    def format_tabular_output(self):
        """ Format output in the 'usual' way, as a tabular document """

        rows = []
        property_names = sorted(self.coalesced_properties.keys(), key=str.lower)

        for prop_name in property_names:
            info = self.coalesced_properties[prop_name]
            prop_types = sorted(info.keys(), key=str.lower)
            first_row = True

            for prop_type in prop_types:
                descriptions = sorted(info[prop_type].keys(), key=str.lower)
                for description in descriptions:
                    schema_list = [self.format_schema_path(x) for x in info[prop_type][description] ]
                    if first_row:
                        first_col = self.formatter.bold(prop_name)
                        first_row = False
                    else:
                        first_col = ''
                    rows.append(self.formatter.make_row([first_col,
                                                    self.format_schema_list(schema_list, self.formatter),
                                                    prop_type, description]))

        if self.write_config_fh:
            config_out = self.write_config_fh
            updated_config = self.generate_updated_config()
            json.dump(updated_config, config_out, indent=4, sort_keys=True)
            config_out.close()

        headers = self.formatter.make_header_row([_('Property Name'), _('Defined In Schema(s)'), _('Type'), _('Description')])
        table = self.formatter.make_table(rows, [headers])
        return table


    @staticmethod
    def add_html_boilerplate(htmlblob):

        headlines = ['<head>', '<meta charset="utf-8">', '<title>' + _('Property Index') + '</title>']
        styles = """
<style>
table{
    max-width: 100%;
    background-color: transparent;
    border-collapse: separate;
    border-spacing: 0;
    margin-bottom: 1.25em;
    border: 1px solid #999999;
    border-width: 0 1px 1px 0;
 }
 td, th{
    padding: .5em;
    text-align: left;
    vertical-align: top;
    border: 1px solid #999999;
    border-width: 1px 0 0 1px;
}
table.properties{
    width: 100%;
}
</style>
"""
        headlines.append(styles)
        headlines.append('</head>')
        head = '\n'.join(headlines)
        return '\n'.join(['<!doctype html>', '<html>', head, '<body>', htmlblob, '</body></html>'])


    @staticmethod
    def format_schema_list(schema_list, formatter):
        sep = ', ' + formatter.br()
        if len(schema_list) > 10:
            return formatter.italic(_('various')) + formatter.br() + '(' + sep.join(schema_list[:2]) + ' ... )'
        else:
            return sep.join(schema_list)


    @staticmethod
    def format_schema_path(sl):
        formatted = sl[0]
        if len(sl) > 1:
            formatted += ' (' + ' > '.join(sl[1:]) + ')'
        return formatted


    def output_csv(self):
        """ Generate CSV output. """

        import csv
        import io

        csv_out = io.StringIO()
        writer = csv.writer(csv_out)

        rows = []
        rows.append([_('Property Name'), _('Schema'), _('Type'), _('Description')])

        property_names = sorted(self.coalesced_properties.keys())
        for prop_name in property_names:
            info = self.coalesced_properties[prop_name]
            prop_types = sorted(info.keys())

            for prop_type in prop_types:
                descriptions = sorted(info[prop_type].keys())
                for description in descriptions:
                    schema_list = [self.format_schema_path(x) for x in info[prop_type][description] ]
                    for schema_str in schema_list:
                        rows.append([prop_name, schema_str, prop_type, description])

        for row in rows:
            writer.writerow(row)

        result = csv_out.getvalue()
        csv_out.close()
        return result



    def add_description(self, text):
        """ This is for the schema description. We don't actually use this. """
        pass

    def add_uris(self, uris):
        """ omit URIs """
        pass


    def add_json_payload(self, json_payload):
        """ JSON payloads don't make sense for PropertyIndex  """
        pass
Пример #4
0
class MarkdownGenerator(DocFormatter):
    """Provides methods for generating markdown from Redfish schemas.

    Markdown is targeted to the Slate documentation tool: https://github.com/lord/slate
    """


    def __init__(self, property_data, traverser, config, level=0):
        super(MarkdownGenerator, self).__init__(property_data, traverser, config, level)
        self.separators = {
            'inline': ', ',
            'linebreak': '\n',
            'pattern': ', '
            }
        self.formatter = FormatUtils()
        self.layout_payloads = 'top'


    def format_property_row(self, schema_ref, prop_name, prop_info, prop_path=[], in_array=False):
        """Format information for a single property.

        Returns an object with 'row', 'details', 'action_details', and 'profile_conditional_details':

        'row': content for the main table being generated.
        'details': content for the Property Details section.
        'action_details': content for the Actions section.
        'profile_conditional_details': populated only in profile_mode, formatted conditional details

        This may include embedded objects with their own properties.
        """

        traverser = self.traverser
        formatted = []     # The row itself

        current_depth = len(prop_path)
        if in_array:
            current_depth = current_depth -1

        # strip_top_object is used for fragments, to allow output of just the properties
        # without the enclosing object:
        if self.config.get('strip_top_object') and current_depth > 0:
            indentation_string = '&nbsp;' * 6 * (current_depth -1)
        else:
            indentation_string = '&nbsp;' * 6 * current_depth

        # If prop_path starts with Actions and is more than 1 deep, we are outputting for an Actions
        # section and should dial back the indentation by one level.
        if len(prop_path) > 1 and prop_path[0] == 'Actions':
            indentation_string = '&nbsp;' * 6 * (current_depth -1)

        collapse_array = False # Should we collapse a list description into one row? For lists of simple types
        has_enum = False

        if current_depth < self.current_depth:
            for i in range(current_depth, self.current_depth):
                if i in self.current_version:
                    del self.current_version[i]
        self.current_depth = current_depth
        parent_depth = current_depth - 1

        if isinstance(prop_info, list):
            meta = prop_info[0].get('_doc_generator_meta')
            has_enum = 'enum' in prop_info[0]
            is_excerpt = prop_info[0].get('_is_excerpt') or prop_info[0].get('excerptCopy')
        elif isinstance(prop_info, dict):
            meta = prop_info.get('_doc_generator_meta')
            has_enum = 'enum' in prop_info
            is_excerpt = prop_info[0].get('_is_excerpt')
        if not meta:
            meta = {}

        # We want to modify a local copy of meta, deleting redundant version info
        meta = copy.deepcopy(meta)

        if prop_name:
            name_and_version = self.formatter.bold(self.escape_for_markdown(prop_name,
                                                                  self.config.get('escape_chars', [])))
        else:
            name_and_version = ''

        deprecated_descr = None

        version = meta.get('version')
        self.current_version[current_depth] = version

        # Don't display version if there is a parent version and this is not newer:
        if self.current_version.get(parent_depth) and version:
            version = meta.get('version')
            if DocGenUtilities.compare_versions(version, self.current_version.get(parent_depth)) <= 0:
                del meta['version']

        if meta.get('version', '1.0.0') != '1.0.0':
            version_display = self.truncate_version(meta['version'], 2) + '+'
            if 'version_deprecated' in meta:
                deprecated_display = self.truncate_version(meta['version_deprecated'], 2)
                name_and_version += ' ' + self.formatter.italic('(v' + version_display +
                                                      ', deprecated v' + deprecated_display +  ')')
                deprecated_descr = ("Deprecated v" + deprecated_display + '+. ' +
                                    self.escape_for_markdown(meta['version_deprecated_explanation'], self.config.get('escape_chars', [])))
            else:
                name_and_version += ' ' + self.formatter.italic('(v' + version_display + ')')
        elif 'version_deprecated' in meta:
            deprecated_display = self.truncate_version(meta['version_deprecated'], 2)
            name_and_version += ' ' + self.formatter.italic('(deprecated v' + deprecated_display +  ')')
            deprecated_descr =  ("Deprecated v" + deprecated_display + '+. ' +
                                 self.escape_for_markdown(meta['version_deprecated_explanation'],
                                                          self.config.get('escape_chars', [])))

        formatted_details = self.parse_property_info(schema_ref, prop_name, prop_info, prop_path,
                                                     meta.get('within_action'))

        if formatted_details.get('promote_me'):
            return({'row': '\n'.join(formatted_details['item_description']), 'details':formatted_details['prop_details'],
                    'action_details':formatted_details.get('action_details')})

        if self.config.get('strip_top_object') and current_depth == 0:
            # In this case, we're done for this bit of documentation, and we just want the properties of this object.
            formatted.append('\n'.join(formatted_details['object_description']))
            return({'row': '\n'.join(formatted), 'details':formatted_details['prop_details'],
                    'action_details':formatted_details.get('action_details'),
                    'profile_conditional_details': formatted_details.get('profile_conditional_details')})


        # Eliminate dups in these these properties and join with a delimiter:
        props = {
            'prop_type': self.separators['inline'],
            'descr': self.separators['linebreak'],
            'object_description': self.separators['linebreak'],
            'item_description': self.separators['linebreak']
            }

        for property_name, delim in props.items():
            if isinstance(formatted_details[property_name], list):
                property_values = []
                self.append_unique_values(formatted_details[property_name], property_values)
                formatted_details[property_name] = delim.join(property_values)

        if formatted_details['prop_is_object'] and not in_array:
            if formatted_details['object_description'] == '':
                name_and_version += ' {}'
            else:
                name_and_version += ' {'

        if formatted_details['prop_is_array']:
            if formatted_details['item_description'] == '':
                if formatted_details['array_of_objects']:
                    name_and_version += ' [ {} ]'
                else:
                    name_and_version += ' [ ]'
            else:
                if formatted_details['array_of_objects']:
                    name_and_version += ' [ {'
                else:
                    collapse_array = True
                    name_and_version += ' [ ]'
        elif in_array:
            if formatted_details['prop_is_object']:
                name_and_version += ' [ { } ]'
            else:
                name_and_version += ' [ ]'

        if formatted_details['descr'] is None:
            formatted_details['descr'] = ''

        if formatted_details['profile_purpose']:
            if formatted_details['descr']:
                formatted_details['descr'] += ' '
            formatted_details['descr'] += self.formatter.bold(formatted_details['profile_purpose'])

        if formatted_details['descr'] is None:
            formatted_details['descr'] = ''

        if formatted_details['profile_purpose']:
            if formatted_details['descr']:
                formatted_details['descr'] += ' '
            formatted_details['descr'] += self.formatter.bold(formatted_details['profile_purpose'])

        if formatted_details['add_link_text']:
            if formatted_details['descr']:
                formatted_details['descr'] += ' '
            formatted_details['descr'] += formatted_details['add_link_text']

        # Append reference info to descriptions, if appropriate:
        if not formatted_details.get('fulldescription_override'):
            if formatted_details['has_direct_prop_details'] and not formatted_details['has_action_details']:
                # If there are prop_details (enum details), add a note to the description:
                if has_enum:
                    text_descr = 'See ' + prop_name + ' in Property Details, below, for the possible values of this property.'
                else:
                    text_descr = 'See Property Details, below, for more information about this property.'
                formatted_details['descr'] += ' ' + self.formatter.italic(text_descr)

            if formatted_details['has_action_details']:
                text_descr = 'For more information, see the Actions section below.'
                formatted_details['descr'] += ' ' + self.formatter.italic(text_descr)

        if deprecated_descr:
            formatted_details['descr'] += ' ' + self.formatter.italic(deprecated_descr)

        prop_type = formatted_details['prop_type']
        if has_enum:
            prop_type += '<br>(enum)'

        if formatted_details['prop_units']:
            prop_type += '<br>(' + formatted_details['prop_units'] + ')'

        if is_excerpt:
            prop_type += '<br>(excerpt)'

        if in_array:
            prop_type = 'array (' + prop_type + ')'

        if collapse_array:
            item_list = formatted_details['item_list']
            if len(item_list):
                if isinstance(item_list, list):
                    item_list = ', '.join(item_list)
                prop_type += ' (' + item_list + ')'

        prop_access = ''
        if not meta.get('is_pattern') and not formatted_details['prop_is_object']:
            if formatted_details['read_only']:
                prop_access = 'read-only'
            else:
                prop_access = 'read-write'

        if formatted_details['prop_required'] or formatted_details['required_parameter']:
            prop_access += ' required'
        elif formatted_details['prop_required_on_create']:
            prop_access += ' required on create'

        if formatted_details['nullable']:
            prop_access += '<br>(null)'

        # If profile reqs are present, massage them:
        profile_access = self.format_base_profile_access(formatted_details)

        if self.config.get('profile_mode'):
            if profile_access:
                prop_type += '<br><br>' + self.formatter.italic(profile_access)
        elif prop_access:
            prop_type += '<br><br>' + self.formatter.italic(prop_access)


        row = []
        row.append(indentation_string + name_and_version)
        row.append(prop_type)
        row.append(formatted_details['descr'])

        formatted.append('| ' + ' | '.join(row) + ' |')

        if len(formatted_details['object_description']) > 0:
            formatted.append(formatted_details['object_description'])
            formatted.append('| ' + indentation_string + '} |   |   |')

        if not collapse_array and len(formatted_details['item_description']) > 0:
            formatted.append(formatted_details['item_description'])
            if formatted_details['array_of_objects']:
                formatted.append('| ' + indentation_string + '} ] |   |   |')
            else:
                formatted.append('| ' + indentation_string + '] |   |   |')

        return({'row': '\n'.join(formatted), 'details':formatted_details['prop_details'],
                'action_details':formatted_details.get('action_details'),
                'profile_conditional_details': formatted_details.get('profile_conditional_details')})


    def format_property_details(self, prop_name, prop_type, prop_description, enum, enum_details,
                                supplemental_details, meta, anchor=None, profile=None):
        """Generate a formatted table of enum information for inclusion in Property Details."""

        contents = []
        contents.append(self.formatter.head_three(prop_name + ':', self.level))

        parent_version = meta.get('version')
        enum_meta = meta.get('enum', {})

        # Are we in profile mode? If so, consult the profile passed in for this property.
        # For Action Parameters, look for ParameterValues/RecommendedValues; for
        # Property enums, look for MinSupportValues/RecommendedValues.
        profile_mode = self.config.get('profile_mode')
        if profile_mode:
            if profile is None:
                profile = {}

            profile_values = profile.get('Values', [])
            profile_min_support_values = profile.get('MinSupportValues', [])
            profile_parameter_values = profile.get('ParameterValues', [])
            profile_recommended_values = profile.get('RecommendedValues', [])

            profile_all_values = (profile_values + profile_min_support_values + profile_parameter_values
                                  + profile_recommended_values)

        if prop_description:
            contents.append(self.formatter.para(self.escape_for_markdown(prop_description, self.config.get('escape_chars', []))))

        if isinstance(prop_type, list):
            prop_type = ', '.join(prop_type)

        if supplemental_details:
            contents.append('\n' + supplemental_details + '\n')

        if enum_details:
            if profile_mode:
                contents.append('| ' + prop_type + ' | Description | Profile Specifies |')
                contents.append('| --- | --- | --- |')
            else:
                contents.append('| ' + prop_type + ' | Description |')
                contents.append('| --- | --- |')
            enum.sort(key=str.lower)
            for enum_item in enum:
                enum_name = enum_item
                enum_item_meta = enum_meta.get(enum_item, {})
                version_display = None
                deprecated_descr = None
                if 'version' in enum_item_meta:
                    version = enum_item_meta['version']
                    if not parent_version or DocGenUtilities.compare_versions(version, parent_version) > 0:
                        version_display = self.truncate_version(version, 2) + '+'
                if version_display:
                    if 'version_deprecated' in enum_item_meta:
                        version_depr = enum_item_meta['version_deprecated']
                        deprecated_display = self.truncate_version(version_depr, 2)
                        enum_name += ' ' + self.formatter.italic('(v' + version_display + ', deprecated v' + deprecated_display + ')')
                        if enum_item_meta.get('version_deprecated_explanation'):
                            deprecated_descr = ("Deprecated v" + deprecated_display + '+. ' +
                                                enum_item_meta['version_deprecated_explanation'])
                    else:
                        enum_name += ' ' + self.formatter.italic('(v' + version_display + ')')
                else:
                    if 'version_deprecated' in enum_item_meta:
                        version_depr = enum_item_meta['version_deprecated']
                        deprecated_display = self.truncate_version(version_depr, 2)
                        enum_name += ' ' + self.formatter.italic('(deprecated v' + deprecated_display + ')')
                        if enum_item_meta.get('version_deprecated_explanation'):
                            deprecated_descr = ("Deprecated v" + deprecated_display + '+. ' +
                                                enum_item_meta['version_deprecated_explanation'])
                descr = enum_details.get(enum_item, '')
                if deprecated_descr:
                    if descr:
                        descr += ' ' + self.formatter.italic(deprecated_descr)
                    else:
                        descr = self.formatter.italic(deprecated_descr)

                if profile_mode:
                    profile_spec = ''
                    if enum_name in profile_values:
                        profile_spec = 'Required'
                    elif enum_name in profile_min_support_values:
                        profile_spec = 'Required'
                    elif enum_name in profile_parameter_values:
                        profile_spec = 'Required'
                    elif enum_name in profile_recommended_values:
                        profile_spec = 'Recommended'
                    contents.append('| ' + enum_name + ' | ' + descr + ' | ' + profile_spec + ' |')
                else:
                    contents.append('| ' + enum_name + ' | ' + descr + ' |')

        elif enum:
            if profile_mode:
                contents.append('| ' + prop_type + ' | Profile Specifies |')
                contents.append('| --- | --- |')
            else:
                contents.append('| ' + prop_type + ' |')
                contents.append('| --- |')
            for enum_item in enum:
                enum_name = enum_item
                enum_item_meta = enum_meta.get(enum_item, {})
                version_display = None

                if 'version' in enum_item_meta:
                    version = enum_item_meta['version']
                    if not parent_version or DocGenUtilities.compare_versions(version, parent_version) > 0:
                        version_display = self.truncate_version(version, 2) + '+'
                if version_display:
                    if 'version_deprecated' in enum_item_meta:
                        version_depr = enum_item_meta['version_deprecated']
                        deprecated_display = self.truncate_version(version_depr, 2)
                        enum_name += ' ' + self.formatter.italic('(v' + version_display + ', deprecated v' + deprecated_display + ')')
                        if enum_item_meta.get('version_deprecated_explanation'):
                            deprecated_descr = ('Deprecated v' + deprecated_display + '+. ' +
                                                enum_item_meta['version_deprecated_explanation'])
                    else:
                        enum_name += ' ' + self.formatter.italic('(v' + version_display + ')')
                else:
                    if 'version_deprecated' in enum_item_meta:
                        version_depr = enum_item_meta['version_deprecated']
                        deprecated_display = self.truncate_version(version_depr, 2)
                        enum_name += ' ' + self.formatter.italic('(deprecated v' + deprecated_display + ')')
                        if enum_item_meta.get('version_deprecated_explanation'):
                            enum_name += ' ' + self.formatter.italic('Deprecated v' + deprecated_display + '+. ' +
                                                           enum_item_meta['version_deprecated_explanation'])

                if profile_mode:
                    profile_spec = ''
                    if enum_name in profile_values:
                        profile_spec = 'Required'
                    elif enum_name in profile_min_support_values:
                        profile_spec = 'Required'
                    elif enum_name in profile_parameter_values:
                        profile_spec = 'Required'
                    elif enum_name in profile_recommended_values:
                        profile_spec = 'Recommended'

                    contents.append('| ' + enum_name + ' | ' + profile_spec + ' |')
                else:
                    contents.append('| ' + enum_name + ' | ')

        return '\n'.join(contents) + '\n'


    def format_action_details(self, prop_name, action_details):
        """Generate a formatted Actions section from supplemental markup."""

        contents = []
        contents.append(self.formatter.head_three(action_details.get('action_name', prop_name), self.level))
        if action_details.get('text'):
            contents.append(action_details.get('text'))
        if action_details.get('example'):
            example = '```json\n' + action_details['example'] + '\n```\n'
            contents.append('Example Action POST:\n')
            contents.append(example)

        return '\n'.join(contents) + '\n'


    def format_action_parameters(self, schema_ref, prop_name, prop_descr, action_parameters):
        """Generate a formatted Actions section from parameter data. """

        formatted = []

        action_name = prop_name
        if prop_name.startswith('#'): # expected
            # Example: from #Bios.ResetBios, we want prop_name "ResetBios" and action_name "Bios.ResetBios"
            prop_name_parts = prop_name.split('.')
            prop_name = prop_name_parts[-1]
            action_name = action_name[1:]

        formatted.append(self.formatter.head_four(prop_name, self.level))
        formatted.append(self.formatter.para(prop_descr))

        # Add the URIs for this action.
        formatted.append(self.format_uri_block_for_action(action_name, self.current_uris));

        if action_parameters:
            rows = []
            # Table start:
            rows.append("|     |     |     |")
            rows.append("| --- | --- | --- |")

            # Add a "start object" row for this parameter:
            rows.append('| ' + ' | '.join(['{', ' ',' ',' ']) + ' |')

            param_names = [x for x in action_parameters.keys()]
            param_names.sort(key=str.lower)
            for param_name in param_names:
                formatted_parameters = self.format_property_row(schema_ref, param_name, action_parameters[param_name], ['Actions', prop_name])
                rows.append(formatted_parameters.get('row'))

            # Add a closing } row:
            rows.append('| ' + ' | '.join(['}', ' ',' ',' ']) + ' |')

            formatted.append(self.formatter.para('The following table shows the parameters for the action which are included in the POST body to the URI shown in the "target" property of the Action.'))

            formatted.append('\n'.join(rows))

        else:
            formatted.append(self.formatter.para("(This action takes no parameters.)"))

        return "\n".join(formatted)


    def _format_profile_access(self, read_only=False, read_req=None, write_req=None, min_count=None):
        """Common formatting logic for profile_access column"""

        profile_access = ''
        if not self.config['profile_mode']:
            return profile_access

        # Each requirement  may be Mandatory, Recommended, IfImplemented, Conditional, or (None)
        if not read_req:
            read_req = 'Mandatory' # This is the default if nothing is specified.
        if read_only:
            profile_access = self.formatter.nobr(self.text_map(read_req)) + ' (Read-only)'
        elif read_req == write_req:
            profile_access = self.formatter.nobr(self.text_map(read_req)) + ' (Read/Write)'
        elif not write_req:
            profile_access = self.formatter.nobr(self.text_map(read_req)) + ' (Read)'
        else:
            # Presumably Read is Mandatory and Write is Recommended; nothing else makes sense.
            profile_access = (self.formatter.nobr(self.text_map(read_req)) + ' (Read),' +
                              self.formatter.nobr(self.text_map(write_req)) + ' (Read/Write)')

        if min_count:
            if profile_access:
                profile_access += ", "
            profile_access += self.formatter.nobr("Minimum " + str(min_count))

        return profile_access


    def link_to_own_schema(self, schema_ref, schema_full_uri):
        """Format a reference to a schema."""
        result = super().link_to_own_schema(schema_ref, schema_full_uri)
        return self.formatter.italic(result)


    def link_to_outside_schema(self, schema_full_uri):
        """Format a reference to a schema_uri, which should be a valid URI"""
        return self.formatter.italic('['+ schema_full_uri + '](' + schema_full_uri + ')')


    def emit(self):
        """ Output contents thus far """

        contents = []

        for section in self.sections:
            contents.append(section.get('heading'))
            if section.get('release_history'):
                contents.append(section['release_history'])
            if section.get('description'):
                contents.append(section['description'])
            if section.get('uris'):
                contents.append(section['uris'])
            if section.get('json_payload'):
                contents.append(section['json_payload'])
            # something is awry if there are no properties, but ...
            if section.get('properties'):
                contents.append('|     |     |     |')
                contents.append('| --- | --- | --- |')
                contents.append('\n'.join(section['properties']))

            if section.get('profile_conditional_details'):
                # sort them now; these can be sub-properties so may not be in alpha order.
                conditional_details = '\n'.join(sorted(section['profile_conditional_details'], key=str.lower))
                contents.append('\n' + self.formatter.head_two('Conditional Requirements', self.level))
                contents.append(conditional_details)

            if len(section.get('action_details', [])):
                contents.append('\n' + self.formatter.head_two('Actions', self.level))
                contents.append('\n\n'.join(section.get('action_details')))
            if section.get('property_details'):
                contents.append('\n' + self.formatter.head_two('Property Details', self.level))
                contents.append('\n'.join(section['property_details']))

        self.sections = []

        # Profile output may include registry sections
        for section in self.registry_sections:
            contents.append(section.get('heading'))
            contents.append(section.get('requirement'))
            if section.get('description'):
                contents.append(self.formatter.para(section['description']))
            if section.get('messages'):
                contents.append(self.formatter.head_two('Messages', self.level))
                message_rows = [self.formatter.make_row(x) for x in section['messages']]
                header_cells = ['', 'Requirement']
                if self.config.get('profile_mode') != 'terse':
                    header_cells.append('Description')
                header_row = self.formatter.make_row(header_cells)
                contents.append(self.formatter.make_table(message_rows, [header_row], 'messages'))
                contents.append('\n')

        return '\n'.join(contents)


    def output_document(self):
        """Return full contents of document"""
        body = self.emit()
        common_properties = self.generate_common_properties_doc()

        supplemental = self.config.get('supplemental', {})

        if 'Title' in supplemental:
            doc_title = supplemental['Title']
        else:
            doc_title = 'Schema Documentation'

        prelude = "---\ntitle: " + doc_title + """

search: true
---
"""

        intro = supplemental.get('Introduction')
        if intro:
            intro = self.process_intro(intro)
            prelude += '\n' + intro + '\n'

        contents = [prelude, body]
        if 'Postscript' in supplemental:
            contents.append('\n' + supplemental['Postscript'])

        output = '\n'.join(contents)
        if '[insert_common_objects]' in output:
            output = output.replace('[insert_common_objects]', common_properties, 1)

        if '[insert_collections]' in output:
            collections_doc = self.generate_collections_doc()
            output = output.replace('[insert_collections]', collections_doc, 1)

        # Replace pagebreak markers with HTML pagebreak markup
        output = output.replace('~pagebreak~', '<p style="page-break-before: always"></p>')

        return output


    def process_intro(self, intro_blob):
        """ Process the intro text, generating and inserting any schema fragments """
        parts = []
        intro = []
        part_text = []

        fragment_config = {
            'output_format': 'markdown',
            'normative': self.config.get('normative'),
            'cwd': self.config.get('cwd'),
            'schema_supplement': {},
            'supplemental': {},
            'excluded_annotations': [],
            'excluded_annotations_by_match': [],
            'excluded_properties': [],
            'excluded_by_match': [],
            'excluded_schemas': [],
            'excluded_schemas_by_match': [],
            'escape_chars': [],
            'uri_replacements': {},
            'units_translation': self.config.get('units_translation'),
            'profile': self.config.get('profile'),
            'profile_mode': self.config.get('profile_mode'),
            'profile_resources': self.config.get('profile_resources', {}),
            'wants_common_objects': self.config.get('wants_common_objects'),
            'actions_in_property_table': self.config.get('actions_in_property_table', True),
            }

        for line in intro_blob.splitlines():
            if line.startswith('#include_fragment'):
                if len(part_text):
                    parts.append({'type': 'markdown', 'content': '\n'.join(part_text)})
                    part_text = []
                    fragment_id = line[17:].strip()
                    fragment_content = self.generate_fragment_doc(fragment_id, fragment_config)
                    parts.append({'type': 'fragment', 'content': fragment_content})
            else:
                part_text.append(line)

        if len(part_text):
            parts.append({'type': 'markdown', 'content': '\n'.join(part_text)})

        for part in parts:
            if part['type'] == 'markdown':
                intro.append(part['content'])
            elif part['type'] == 'fragment':
                intro.append(part['content'])
        return '\n'.join(intro)


    def add_section(self, text, link_id=False):
        """ Add a top-level heading """
        self.this_section = {'head': text,
                             'heading': '\n' + self.formatter.head_one(text, self.level),
                             'properties': [],
                             'property_details': []
                            }
        self.sections.append(self.this_section)


    def add_description(self, text):
        """ Add the schema description """
        self.this_section['description'] = text + '\n'


    def add_uris(self, uris):
        """ Add the URIs (which should be a list) """
        uri_block = "**URIs**:\n"
        for uri in sorted(uris, key=str.lower):
            uri_block += "\n" + self.format_uri(uri)

        self.this_section['uris'] = uri_block + "\n"


    def format_uri_block_for_action(self, action, uris):
        """ Create a URI block for this action & the resource's URIs """
        uri_block = "**URIs**:\n"
        for uri in sorted(uris, key=str.lower):
            uri = uri + "/Actions/" + action
            uri_block += "\n" + self.format_uri(uri)

        return uri_block


    def format_json_payload(self, json_payload):
        """ Format a json payload for output. """
        return '\n' + json_payload + '\n'


    def add_property_row(self, formatted_text):
        """Add a row (or group of rows) for an individual property in the current section/schema.

        formatted_row should be a chunk of text already formatted for output"""
        self.this_section['properties'].append(formatted_text)


    def add_property_details(self, formatted_details):
        """Add a chunk of property details information for the current section/schema."""
        self.this_section['property_details'].append(formatted_details)


    def add_registry_reqs(self, registry_reqs):
        """Add registry messages. registry_reqs includes profile annotations."""

        terse_mode = self.config.get('profile_mode') == 'terse'

        reg_names = [x for x in registry_reqs.keys()]
        reg_names.sort(key=str.lower)
        for reg_name in reg_names:
            reg = registry_reqs[reg_name]
            this_section = {
                'head': reg_name,
                'description': reg.get('Description', ''),
                'messages': []
                }
            heading = reg_name + ' Registry v' + reg['minversion']  + '+'
            if reg.get('current_release', reg['minversion']) != reg['minversion']:
                heading += ' (current release: v' + reg['current_release'] + ')'

            this_section['heading'] = self.formatter.head_one(heading, self.level)
            this_section['requirement'] = 'Requirement: ' + reg.get('profile_requirement', '')

            msgs = reg.get('Messages', {})
            msg_keys = [x for x in msgs.keys()]
            msg_keys.sort(key=str.lower)

            for msg in msg_keys:
                this_msg = msgs[msg]
                if terse_mode and not this_msg.get('profile_requirement'):
                    continue
                msg_row = [msg, this_msg.get('profile_requirement', '')]
                if not terse_mode:
                    msg_row.append(this_msg.get('Description', ''))
                this_section['messages'].append(msg_row)

            self.registry_sections.append(this_section)


    @staticmethod
    def escape_for_markdown(text, chars):
        """Escape selected characters in text to prevent auto-formatting in markdown."""
        for char in chars:
            text = text.replace(char, '\\' + char)
        return text

    @staticmethod
    def escape_regexp(text):
        """If escaping is necessary to protect patterns when output format is rendered, do that."""
        chars_to_escape = r'\`*_{}[]()#+-.!|'
        escaped_text = ''
        for char in text:
            if char in chars_to_escape:
                escaped_text += '\\' + char
            else:
                escaped_text += char

        return escaped_text