mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	Fixes #15205. https://python-markdown.github.io/change_log/release-3.0/#homegrown-ordereddict-has-been-replaced-with-a-purpose-built-registry https://python-markdown.github.io/change_log/release-3.0/#md_globals-keyword-deprecated-from-extension-api The priority numbers are arbitrarily chosen to preserve the existing order. Signed-off-by: Anders Kaseorg <anders@zulip.com>
		
			
				
	
	
		
			155 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			155 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
import json
 | 
						|
import os
 | 
						|
import re
 | 
						|
from typing import Any, Dict, List, Mapping, Sequence
 | 
						|
 | 
						|
import markdown
 | 
						|
from django.utils.html import escape as escape_html
 | 
						|
from markdown.extensions import Extension
 | 
						|
from markdown.preprocessors import Preprocessor
 | 
						|
 | 
						|
from zerver.openapi.openapi import get_openapi_parameters, likely_deprecated_parameter
 | 
						|
 | 
						|
REGEXP = re.compile(r'\{generate_api_arguments_table\|\s*(.+?)\s*\|\s*(.+)\s*\}')
 | 
						|
 | 
						|
 | 
						|
class MarkdownArgumentsTableGenerator(Extension):
 | 
						|
    def __init__(self, configs: Mapping[str, Any] = {}) -> None:
 | 
						|
        self.config = {
 | 
						|
            'base_path': ['.', 'Default location from which to evaluate relative paths for the JSON files.'],
 | 
						|
        }
 | 
						|
        for key, value in configs.items():
 | 
						|
            self.setConfig(key, value)
 | 
						|
 | 
						|
    def extendMarkdown(self, md: markdown.Markdown) -> None:
 | 
						|
        md.preprocessors.register(
 | 
						|
            APIArgumentsTablePreprocessor(md, self.getConfigs()), 'generate_api_arguments', 505
 | 
						|
        )
 | 
						|
 | 
						|
 | 
						|
class APIArgumentsTablePreprocessor(Preprocessor):
 | 
						|
    def __init__(self, md: markdown.Markdown, config: Dict[str, Any]) -> None:
 | 
						|
        super().__init__(md)
 | 
						|
        self.base_path = config['base_path']
 | 
						|
 | 
						|
    def run(self, lines: List[str]) -> List[str]:
 | 
						|
        done = False
 | 
						|
        while not done:
 | 
						|
            for line in lines:
 | 
						|
                loc = lines.index(line)
 | 
						|
                match = REGEXP.search(line)
 | 
						|
 | 
						|
                if not match:
 | 
						|
                    continue
 | 
						|
 | 
						|
                filename = match.group(1)
 | 
						|
                doc_name = match.group(2)
 | 
						|
                filename = os.path.expanduser(filename)
 | 
						|
 | 
						|
                is_openapi_format = filename.endswith('.yaml')
 | 
						|
 | 
						|
                if not os.path.isabs(filename):
 | 
						|
                    parent_dir = self.base_path
 | 
						|
                    filename = os.path.normpath(os.path.join(parent_dir, filename))
 | 
						|
 | 
						|
                if is_openapi_format:
 | 
						|
                    endpoint, method = doc_name.rsplit(':', 1)
 | 
						|
                    arguments: List[Dict[str, Any]] = []
 | 
						|
 | 
						|
                    try:
 | 
						|
                        arguments = get_openapi_parameters(endpoint, method)
 | 
						|
                    except KeyError as e:
 | 
						|
                        # Don't raise an exception if the "parameters"
 | 
						|
                        # field is missing; we assume that's because the
 | 
						|
                        # endpoint doesn't accept any parameters
 | 
						|
                        if e.args != ('parameters',):
 | 
						|
                            raise e
 | 
						|
                else:
 | 
						|
                    with open(filename) as fp:
 | 
						|
                        json_obj = json.load(fp)
 | 
						|
                        arguments = json_obj[doc_name]
 | 
						|
 | 
						|
                if arguments:
 | 
						|
                    text = self.render_table(arguments)
 | 
						|
                else:
 | 
						|
                    text = ['This endpoint does not accept any parameters.']
 | 
						|
                # The line that contains the directive to include the macro
 | 
						|
                # may be preceded or followed by text or tags, in that case
 | 
						|
                # we need to make sure that any preceding or following text
 | 
						|
                # stays the same.
 | 
						|
                line_split = REGEXP.split(line, maxsplit=0)
 | 
						|
                preceding = line_split[0]
 | 
						|
                following = line_split[-1]
 | 
						|
                text = [preceding, *text, following]
 | 
						|
                lines = lines[:loc] + text + lines[loc+1:]
 | 
						|
                break
 | 
						|
            else:
 | 
						|
                done = True
 | 
						|
        return lines
 | 
						|
 | 
						|
    def render_table(self, arguments: Sequence[Mapping[str, Any]]) -> List[str]:
 | 
						|
        # TODO: Fix naming now that this no longer renders a table.
 | 
						|
        table = []
 | 
						|
        argument_template = """
 | 
						|
<div class="api-argument" id="parameter-{argument}">
 | 
						|
    <p class="api-argument-name"><strong>{argument}</strong> {required} {deprecated} <a href="#parameter-{argument}" class="api-argument-hover-link"><i class="fa fa-chain"></i></a></p>
 | 
						|
    <div class="api-example">
 | 
						|
        <span class="api-argument-example-label">Example</span>: <code>{example}</code>
 | 
						|
    </div>
 | 
						|
    <div class="api-description">{description}</div>
 | 
						|
    <hr>
 | 
						|
</div>"""
 | 
						|
 | 
						|
        md_engine = markdown.Markdown(extensions=[])
 | 
						|
        arguments = sorted(arguments, key=lambda argument: 'deprecated' in argument)
 | 
						|
        for argument in arguments:
 | 
						|
            description = argument['description']
 | 
						|
            oneof = ['`' + str(item) + '`'
 | 
						|
                     for item in argument.get('schema', {}).get('enum', [])]
 | 
						|
            if oneof:
 | 
						|
                description += '\nMust be one of: {}.'.format(', '.join(oneof))
 | 
						|
 | 
						|
            default = argument.get('schema', {}).get('default')
 | 
						|
            if default is not None:
 | 
						|
                description += f'\nDefaults to `{json.dumps(default)}`.'
 | 
						|
 | 
						|
            # TODO: OpenAPI allows indicating where the argument goes
 | 
						|
            # (path, querystring, form data...).  We should document this detail.
 | 
						|
            example = ""
 | 
						|
            if 'example' in argument:
 | 
						|
                example = argument['example']
 | 
						|
            else:
 | 
						|
                example = json.dumps(argument['content']['application/json']['example'])
 | 
						|
 | 
						|
            required_string: str = "required"
 | 
						|
            if argument.get('in', '') == 'path':
 | 
						|
                # Any path variable is required
 | 
						|
                assert argument['required']
 | 
						|
                required_string = 'required in path'
 | 
						|
 | 
						|
            if argument.get('required', False):
 | 
						|
                required_block = f'<span class="api-argument-required">{required_string}</span>'
 | 
						|
            else:
 | 
						|
                required_block = '<span class="api-argument-optional">optional</span>'
 | 
						|
 | 
						|
            # Test to make sure deprecated parameters are marked so.
 | 
						|
            if likely_deprecated_parameter(description):
 | 
						|
                assert(argument['deprecated'])
 | 
						|
            if argument.get('deprecated', False):
 | 
						|
                deprecated_block = '<span class="api-argument-deprecated">Deprecated</span>'
 | 
						|
            else:
 | 
						|
                deprecated_block = ''
 | 
						|
 | 
						|
            table.append(argument_template.format(
 | 
						|
                argument=argument.get('argument') or argument.get('name'),
 | 
						|
                example=escape_html(example),
 | 
						|
                required=required_block,
 | 
						|
                deprecated=deprecated_block,
 | 
						|
                description=md_engine.convert(description),
 | 
						|
            ))
 | 
						|
 | 
						|
        return table
 | 
						|
 | 
						|
def makeExtension(*args: Any, **kwargs: str) -> MarkdownArgumentsTableGenerator:
 | 
						|
    return MarkdownArgumentsTableGenerator(kwargs)
 |