1"""
2    sphinx.util.rst
3    ~~~~~~~~~~~~~~~
4
5    reST helper functions.
6
7    :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
8    :license: BSD, see LICENSE for details.
9"""
10
11import re
12from collections import defaultdict
13from contextlib import contextmanager
14from typing import Dict, Generator
15from unicodedata import east_asian_width
16
17from docutils.parsers.rst import roles
18from docutils.parsers.rst.languages import en as english
19from docutils.statemachine import StringList
20from docutils.utils import Reporter
21from jinja2 import Environment, environmentfilter
22
23from sphinx.locale import __
24from sphinx.util import docutils, logging
25
26logger = logging.getLogger(__name__)
27
28docinfo_re = re.compile(':\\w+:.*?')
29symbols_re = re.compile(r'([!-\-/:-@\[-`{-~])')  # symbols without dot(0x2e)
30SECTIONING_CHARS = ['=', '-', '~']
31
32# width of characters
33WIDECHARS = defaultdict(lambda: "WF")   # type: Dict[str, str]
34                                        # WF: Wide + Full-width
35WIDECHARS["ja"] = "WFA"  # In Japanese, Ambiguous characters also have double width
36
37
38def escape(text: str) -> str:
39    text = symbols_re.sub(r'\\\1', text)
40    text = re.sub(r'^\.', r'\.', text)  # escape a dot at top
41    return text
42
43
44def textwidth(text: str, widechars: str = 'WF') -> int:
45    """Get width of text."""
46    def charwidth(char: str, widechars: str) -> int:
47        if east_asian_width(char) in widechars:
48            return 2
49        else:
50            return 1
51
52    return sum(charwidth(c, widechars) for c in text)
53
54
55@environmentfilter
56def heading(env: Environment, text: str, level: int = 1) -> str:
57    """Create a heading for *level*."""
58    assert level <= 3
59    width = textwidth(text, WIDECHARS[env.language])  # type: ignore
60    sectioning_char = SECTIONING_CHARS[level - 1]
61    return '%s\n%s' % (text, sectioning_char * width)
62
63
64@contextmanager
65def default_role(docname: str, name: str) -> Generator[None, None, None]:
66    if name:
67        dummy_reporter = Reporter('', 4, 4)
68        role_fn, _ = roles.role(name, english, 0, dummy_reporter)
69        if role_fn:
70            docutils.register_role('', role_fn)
71        else:
72            logger.warning(__('default role %s not found'), name, location=docname)
73
74    yield
75
76    docutils.unregister_role('')
77
78
79def prepend_prolog(content: StringList, prolog: str) -> None:
80    """Prepend a string to content body as prolog."""
81    if prolog:
82        pos = 0
83        for line in content:
84            if docinfo_re.match(line):
85                pos += 1
86            else:
87                break
88
89        if pos > 0:
90            # insert a blank line after docinfo
91            content.insert(pos, '', '<generated>', 0)
92            pos += 1
93
94        # insert prolog (after docinfo if exists)
95        for lineno, line in enumerate(prolog.splitlines()):
96            content.insert(pos + lineno, line, '<rst_prolog>', lineno)
97
98        content.insert(pos + lineno + 1, '', '<generated>', 0)
99
100
101def append_epilog(content: StringList, epilog: str) -> None:
102    """Append a string to content body as epilog."""
103    if epilog:
104        if 0 < len(content):
105            source, lineno = content.info(-1)
106        else:
107            source = '<generated>'
108            lineno = 0
109        content.append('', source, lineno + 1)
110        for lineno, line in enumerate(epilog.splitlines()):
111            content.append(line, '<rst_epilog>', lineno)
112