示例#1
0
    def get_admonition(self, token, name, classes, title):

        line = token.map[0]

        if name == "admonition":
            # parse title
            node = nodes.admonition(title, classes=classes[1:].split(","))
            state_machine = MockStateMachine(self, line)
            state = MockState(self, state_machine, line)
            textnodes, messages = state.inline_text(title, line)
            title_node = nodes.title(title, "", *textnodes)
            self.add_line_and_source_path(title_node, token)
            node += title_node
            node += messages
            return node

        node_cls = STD_ADMONITIONS.get(name, None)
        if node_cls is None:
            self.current_node.append(
                self.reporter.warning(
                    f"admonition name not recognised, defaulting to note: {name}",
                    line=line,
                ))
            node_cls = nodes.note
        return node_cls(title, classes=classes[1:].split(","))
示例#2
0
    def get_admonition(self, token, name, classes, title):
        """Create an admonition node. """
        line = token.map[0]

        if name == "admonition":
            # parse title
            node = nodes.admonition(title, classes=classes[1:].split(","))
            state_machine = MockStateMachine(self, line)
            state = MockState(self, state_machine, line)
            textnodes, messages = state.inline_text(title, line)
            title_node = nodes.title(title, "", *textnodes)
            self.add_line_and_source_path(title_node, token)
            node += title_node
            node += messages
            return node

        node_cls = STD_ADMONITIONS.get(name)
        return node_cls(title, classes=classes[1:].split(","))
示例#3
0
    def render_directive(self, token: Token):
        """Render special fenced code blocks as directives."""
        first_line = token.info.split(maxsplit=1)
        name = first_line[0][1:-1]
        arguments = "" if len(first_line) == 1 else first_line[1]
        # TODO directive name white/black lists
        content = token.content
        position = token.map[0]
        self.document.current_line = position

        # get directive class
        directive_class, messages = directives.directive(
            name, self.language_module,
            self.document)  # type: (Directive, list)
        if not directive_class:
            error = self.reporter.error(
                'Unknown directive type "{}".\n'.format(name),
                # nodes.literal_block(content, content),
                line=position,
            )
            self.current_node += [error] + messages
            return

        try:
            arguments, options, body_lines = parse_directive_text(
                directive_class, arguments, content)
        except DirectiveParsingError as error:
            error = self.reporter.error(
                "Directive '{}': {}".format(name, error),
                nodes.literal_block(content, content),
                line=position,
            )
            self.current_node += [error]
            return

        # initialise directive
        if issubclass(directive_class, Include):
            directive_instance = MockIncludeDirective(
                self,
                name=name,
                klass=directive_class,
                arguments=arguments,
                options=options,
                body=body_lines,
                token=token,
            )
        else:
            state_machine = MockStateMachine(self, position)
            state = MockState(self, state_machine, position)
            directive_instance = directive_class(
                name=name,
                # the list of positional arguments
                arguments=arguments,
                # a dictionary mapping option names to values
                options=options,
                # the directive content line by line
                content=StringList(body_lines, self.document["source"]),
                # the absolute line number of the first line of the directive
                lineno=position,
                # the line offset of the first line of the content
                content_offset=
                0,  # TODO get content offset from `parse_directive_text`
                # a string containing the entire directive
                block_text="\n".join(body_lines),
                state=state,
                state_machine=state_machine,
            )

        # run directive
        try:
            result = directive_instance.run()
        except DirectiveError as error:
            msg_node = self.reporter.system_message(error.level,
                                                    error.msg,
                                                    line=position)
            msg_node += nodes.literal_block(content, content)
            result = [msg_node]
        except MockingError as exc:
            error = self.reporter.error(
                "Directive '{}' cannot be mocked: {}: {}".format(
                    name, exc.__class__.__name__, exc),
                nodes.literal_block(content, content),
                line=position,
            )
            self.current_node += [error]
            return
        assert isinstance(
            result,
            list), 'Directive "{}" must return a list of nodes.'.format(name)
        for i in range(len(result)):
            assert isinstance(
                result[i], nodes.Node
            ), 'Directive "{}" returned non-Node object (index {}): {}'.format(
                name, i, result[i])
        self.current_node += result
    def run_directive(self, name: str, first_line: str, content: str,
                      position: int) -> List[nodes.Element]:
        """Run a directive and return the generated nodes.

        :param name: the name of the directive
        :param first_line: The text on the same line as the directive name.
            May be an argument or body text, dependent on the directive
        :param content: All text after the first line. Can include options.
        :param position: The line number of the first line

        """
        # TODO directive name white/black lists

        self.document.current_line = position

        # get directive class
        directive_class, messages = directives.directive(
            name, self.language_module_rst,
            self.document)  # type: (Directive, list)
        if not directive_class:
            error = self.reporter.error(
                'Unknown directive type "{}".\n'.format(name),
                # nodes.literal_block(content, content),
                line=position,
            )
            return [error] + messages

        if issubclass(directive_class, Include):
            # this is a Markdown only option,
            # to allow for altering relative image reference links
            directive_class.option_spec["relative-images"] = directives.flag
            directive_class.option_spec["relative-docs"] = directives.path

        try:
            arguments, options, body_lines = parse_directive_text(
                directive_class, first_line, content)
        except DirectiveParsingError as error:
            error = self.reporter.error(
                "Directive '{}': {}".format(name, error),
                nodes.literal_block(content, content),
                line=position,
            )
            return [error]

        # initialise directive
        if issubclass(directive_class, Include):
            directive_instance = MockIncludeDirective(
                self,
                name=name,
                klass=directive_class,
                arguments=arguments,
                options=options,
                body=body_lines,
                lineno=position,
            )
        else:
            state_machine = MockStateMachine(self, position)
            state = MockState(self, state_machine, position)
            directive_instance = directive_class(
                name=name,
                # the list of positional arguments
                arguments=arguments,
                # a dictionary mapping option names to values
                options=options,
                # the directive content line by line
                content=StringList(body_lines, self.document["source"]),
                # the absolute line number of the first line of the directive
                lineno=position,
                # the line offset of the first line of the content
                content_offset=
                0,  # TODO get content offset from `parse_directive_text`
                # a string containing the entire directive
                block_text="\n".join(body_lines),
                state=state,
                state_machine=state_machine,
            )

        # run directive
        try:
            result = directive_instance.run()
        except DirectiveError as error:
            msg_node = self.reporter.system_message(error.level,
                                                    error.msg,
                                                    line=position)
            msg_node += nodes.literal_block(content, content)
            result = [msg_node]
        except MockingError as exc:
            error_msg = self.reporter.error(
                "Directive '{}' cannot be mocked: {}: {}".format(
                    name, exc.__class__.__name__, exc),
                nodes.literal_block(content, content),
                line=position,
            )
            return [error_msg]

        assert isinstance(
            result,
            list), 'Directive "{}" must return a list of nodes.'.format(name)
        for i in range(len(result)):
            assert isinstance(
                result[i], nodes.Node
            ), 'Directive "{}" returned non-Node object (index {}): {}'.format(
                name, i, result[i])
        return result
    def dict_to_fm_field_list(self,
                              data: Dict[str, Any],
                              language_code: str,
                              line: int = 0) -> nodes.field_list:
        """Render each key/val pair as a docutils ``field_node``.

        Bibliographic keys below will be parsed as Markdown,
        all others will be left as literal text.

        The field list should be at the start of the document,
        and will then be converted to a `docinfo` node during the
        `docutils.docutils.transforms.frontmatter.DocInfo` transform (priority 340),
        and bibliographic keys (or their translation) will be converted to nodes::

            {'author': docutils.nodes.author,
            'authors': docutils.nodes.authors,
            'organization': docutils.nodes.organization,
            'address': docutils.nodes.address,
            'contact': docutils.nodes.contact,
            'version': docutils.nodes.version,
            'revision': docutils.nodes.revision,
            'status': docutils.nodes.status,
            'date': docutils.nodes.date,
            'copyright': docutils.nodes.copyright,
            'dedication': docutils.nodes.topic,
            'abstract': docutils.nodes.topic}

        Also, the 'dedication' and 'abstract' will be placed outside the `docinfo`,
        and so will always be shown in the document.

        If using sphinx, this `docinfo` node will later be extracted from the AST,
        by the `DoctreeReadEvent` transform (priority 880),
        calling `MetadataCollector.process_doc`.
        In this case keys and values will be converted to strings and stored in
        `app.env.metadata[app.env.docname]`

        See
        https://www.sphinx-doc.org/en/master/usage/restructuredtext/field-lists.html
        for docinfo fields used by sphinx.

        """
        field_list = nodes.field_list()

        bibliofields = get_language(language_code).bibliographic_fields
        state_machine = MockStateMachine(self, line)
        state = MockState(self, state_machine, line)

        for key, value in data.items():
            if not isinstance(value, (str, int, float, date, datetime)):
                value = json.dumps(value)
            value = str(value)
            if key in bibliofields:
                para_nodes, _ = state.inline_text(value, line)
                body_children = [nodes.paragraph("", "", *para_nodes)]
            else:
                body_children = [nodes.Text(value, value)]

            field_node = nodes.field()
            field_node.source = value
            field_node += nodes.field_name(key, "", nodes.Text(key, key))
            field_node += nodes.field_body(value, *body_children)
            field_list += field_node

        return field_list
    def render_substitution(self, token, inline: bool):
        """Substitutions are rendered by:

        1. Combining global substitutions with front-matter substitutions
           to create a variable context (front-matter takes priority)
        2. Add the sphinx `env` to the variable context (if available)
        3. Create the string content with Jinja2 (passing it the variable context)
        4. If the substitution is inline and not a directive,
           parse to nodes ignoring block syntaxes (like lists or block-quotes),
           otherwise parse to nodes with all syntax rules.

        """
        position = token.map[0]

        # front-matter substitutions take priority over config ones
        variable_context = {
            **self.config.get("myst_substitutions", {}),
            **getattr(self.document, "fm_substitutions", {}),
        }
        try:
            variable_context["env"] = self.document.settings.env
        except AttributeError:
            pass  # if not sphinx renderer

        # fail on undefined variables
        env = jinja2.Environment(undefined=jinja2.StrictUndefined)

        # try rendering
        try:
            rendered = env.from_string(f"{{{{{token.content}}}}}").render(
                variable_context)
        except Exception as error:
            error_msg = self.reporter.error(
                f"Substitution error:{error.__class__.__name__}: {error}",
                line=position,
            )
            self.current_node += [error_msg]
            return

        # handle circular references
        ast = env.parse(f"{{{{{token.content}}}}}")
        references = {
            n.name
            for n in ast.find_all(jinja2.nodes.Name) if n.name != "env"
        }
        self.document.sub_references = getattr(self.document, "sub_references",
                                               set())
        cyclic = references.intersection(self.document.sub_references)
        if cyclic:
            error_msg = self.reporter.error(
                f"circular substitution reference: {cyclic}",
                line=position,
            )
            self.current_node += [error_msg]
            return

        # parse rendered text
        state_machine = MockStateMachine(self, position)
        state = MockState(self, state_machine, position)

        # TODO improve error reporting;
        # at present, for a multi-line substitution,
        # an error may point to a line lower than the substitution
        # should it point to the source of the substitution?
        # or the error message should at least indicate that its a substitution

        # we record used references before nested parsing, then remove them after
        self.document.sub_references.update(references)

        try:
            if inline and not REGEX_DIRECTIVE_START.match(rendered):
                sub_nodes, _ = state.inline_text(rendered, position)
            else:
                base_node = nodes.Element()
                state.nested_parse(
                    StringList(rendered.splitlines(), self.document["source"]),
                    0,
                    base_node,
                )
                sub_nodes = base_node.children
        finally:
            self.document.sub_references.difference_update(references)

        self.current_node.extend(sub_nodes)
示例#7
0
    def render_substitution(self, token, inline: bool):

        import jinja2

        # TODO token.map was None when substituting an image in a table
        position = token.map[0] if token.map else 9999

        # front-matter substitutions take priority over config ones
        context = {
            **self.config.get("substitutions", {}),
            **getattr(self.document, "fm_substitutions", {}),
        }
        try:
            context["env"] = self.document.settings.env
        except AttributeError:
            pass  # if not sphinx renderer

        # fail on undefined variables
        env = jinja2.Environment(undefined=jinja2.StrictUndefined)

        # try rendering
        try:
            rendered = env.from_string(f"{{{{{token.content}}}}}").render(
                context)
        except Exception as error:
            error = self.reporter.error(
                f"Substitution error:{error.__class__.__name__}: {error}",
                line=position,
            )
            self.current_node += [error]
            return

        # handle circular references
        ast = env.parse(f"{{{{{token.content}}}}}")
        references = {
            n.name
            for n in ast.find_all(jinja2.nodes.Name) if n.name != "env"
        }
        self.document.sub_references = getattr(self.document, "sub_references",
                                               set())
        cyclic = references.intersection(self.document.sub_references)
        if cyclic:
            error = self.reporter.error(
                f"circular substitution reference: {cyclic}",
                line=position,
            )
            self.current_node += [error]
            return

        # parse rendered text
        state_machine = MockStateMachine(self, position)
        state = MockState(self, state_machine, position)
        # TODO improve error reporting;
        # at present, for a multi-line substitution,
        # an error may point to a line lower than the substitution
        # should it point to the source of the substitution?
        # or the error message should at least indicate that its a substitution
        self.document.sub_references.update(references)
        base_node = nodes.Element()
        try:
            state.nested_parse(
                StringList(rendered.splitlines(), self.document["source"]),
                0,
                base_node,
            )
        finally:
            self.document.sub_references.difference_update(references)

        sub_nodes = base_node.children
        if (inline and len(base_node.children) == 1
                and isinstance(base_node.children[0], nodes.paragraph)):
            # just add the contents of the paragraph
            sub_nodes = base_node.children[0].children

        # TODO add more checking for inline compatibility?

        self.current_node.extend(sub_nodes)