summaryrefslogtreecommitdiffstats
path: root/sphinx/util/rst.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/util/rst.py')
-rw-r--r--sphinx/util/rst.py110
1 files changed, 110 insertions, 0 deletions
diff --git a/sphinx/util/rst.py b/sphinx/util/rst.py
new file mode 100644
index 0000000..1e8fd66
--- /dev/null
+++ b/sphinx/util/rst.py
@@ -0,0 +1,110 @@
+"""reST helper functions."""
+
+from __future__ import annotations
+
+import re
+from collections import defaultdict
+from contextlib import contextmanager
+from typing import TYPE_CHECKING
+from unicodedata import east_asian_width
+
+from docutils.parsers.rst import roles
+from docutils.parsers.rst.languages import en as english
+from docutils.parsers.rst.states import Body
+from docutils.utils import Reporter
+from jinja2 import Environment, pass_environment
+
+from sphinx.locale import __
+from sphinx.util import docutils, logging
+
+if TYPE_CHECKING:
+ from collections.abc import Generator
+
+ from docutils.statemachine import StringList
+
+logger = logging.getLogger(__name__)
+
+FIELD_NAME_RE = re.compile(Body.patterns['field_marker'])
+symbols_re = re.compile(r'([!-\-/:-@\[-`{-~])') # symbols without dot(0x2e)
+SECTIONING_CHARS = ['=', '-', '~']
+
+# width of characters
+WIDECHARS: dict[str, str] = defaultdict(lambda: "WF") # WF: Wide + Full-width
+WIDECHARS["ja"] = "WFA" # In Japanese, Ambiguous characters also have double width
+
+
+def escape(text: str) -> str:
+ text = symbols_re.sub(r'\\\1', text)
+ text = re.sub(r'^\.', r'\.', text) # escape a dot at top
+ return text
+
+
+def textwidth(text: str, widechars: str = 'WF') -> int:
+ """Get width of text."""
+ def charwidth(char: str, widechars: str) -> int:
+ if east_asian_width(char) in widechars:
+ return 2
+ else:
+ return 1
+
+ return sum(charwidth(c, widechars) for c in text)
+
+
+@pass_environment
+def heading(env: Environment, text: str, level: int = 1) -> str:
+ """Create a heading for *level*."""
+ assert level <= 3
+ width = textwidth(text, WIDECHARS[env.language])
+ sectioning_char = SECTIONING_CHARS[level - 1]
+ return f'{text}\n{sectioning_char * width}'
+
+
+@contextmanager
+def default_role(docname: str, name: str) -> Generator[None, None, None]:
+ if name:
+ dummy_reporter = Reporter('', 4, 4)
+ role_fn, _ = roles.role(name, english, 0, dummy_reporter)
+ if role_fn: # type: ignore[truthy-function]
+ docutils.register_role('', role_fn) # type: ignore[arg-type]
+ else:
+ logger.warning(__('default role %s not found'), name, location=docname)
+
+ yield
+
+ docutils.unregister_role('')
+
+
+def prepend_prolog(content: StringList, prolog: str) -> None:
+ """Prepend a string to content body as prolog."""
+ if prolog:
+ pos = 0
+ for line in content:
+ if FIELD_NAME_RE.match(line):
+ pos += 1
+ else:
+ break
+
+ if pos > 0:
+ # insert a blank line after docinfo
+ content.insert(pos, '', '<generated>', 0)
+ pos += 1
+
+ # insert prolog (after docinfo if exists)
+ lineno = 0
+ for lineno, line in enumerate(prolog.splitlines()):
+ content.insert(pos + lineno, line, '<rst_prolog>', lineno)
+
+ content.insert(pos + lineno + 1, '', '<generated>', 0)
+
+
+def append_epilog(content: StringList, epilog: str) -> None:
+ """Append a string to content body as epilog."""
+ if epilog:
+ if len(content) > 0:
+ source, lineno = content.info(-1)
+ else:
+ source = '<generated>'
+ lineno = 0
+ content.append('', source, lineno + 1)
+ for lineno, line in enumerate(epilog.splitlines()):
+ content.append(line, '<rst_epilog>', lineno)