From 662389b8b08e2c0dccba36b69590bb0cc4dc424d Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Thu, 2 May 2019 13:46:10 +0900 Subject: [PATCH 1/2] Add rst.heading() --- sphinx/util/rst.py | 35 ++++++++++++++++++++++++++++++++++- tests/test_util_rst.py | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/sphinx/util/rst.py b/sphinx/util/rst.py index c3d32feb7..c897b075a 100644 --- a/sphinx/util/rst.py +++ b/sphinx/util/rst.py @@ -9,11 +9,14 @@ """ import re +from collections import defaultdict from contextlib import contextmanager +from unicodedata import east_asian_width from docutils.parsers.rst import roles from docutils.parsers.rst.languages import en as english from docutils.utils import Reporter +from jinja2 import environmentfilter from sphinx.locale import __ from sphinx.util import docutils @@ -21,13 +24,20 @@ from sphinx.util import logging if False: # For type annotation - from typing import Generator # NOQA + from typing import Callable, Dict, Generator # NOQA from docutils.statemachine import StringList # NOQA + from jinja2 import Environment # NOQA logger = logging.getLogger(__name__) docinfo_re = re.compile(':\\w+:.*?') symbols_re = re.compile(r'([!-\-/:-@\[-`{-~])') # symbols without dot(0x2e) +SECTIONING_CHARS = ['=', '-', '~'] + +# width of characters +WIDECHARS = defaultdict(lambda: "WF") # type: Dict[str, str] + # WF: Wide + Full-width +WIDECHARS["ja"] = "WFA" # In Japanese, Ambiguous characters also have double width def escape(text): @@ -37,6 +47,29 @@ def escape(text): return text +def textwidth(text, widechars='WF'): + # type: (str, str) -> int + """Get width of text.""" + def charwidth(char, widechars): + # type: (str, str) -> int + if east_asian_width(char) in widechars: + return 2 + else: + return 1 + + return sum(charwidth(c, widechars) for c in text) + + +@environmentfilter +def heading(env, text, level=1): + # type: (Environment, str, int) -> str + """Create a heading for *level*.""" + assert level <= 3 + width = textwidth(text, WIDECHARS[env.language]) # type: ignore + sectioning_char = SECTIONING_CHARS[level - 1] + return '%s\n%s' % (text, sectioning_char * width) + + @contextmanager def default_role(docname, name): # type: (str, str) -> Generator diff --git a/tests/test_util_rst.py b/tests/test_util_rst.py index ba836ff1e..1e72eda45 100644 --- a/tests/test_util_rst.py +++ b/tests/test_util_rst.py @@ -9,8 +9,11 @@ """ from docutils.statemachine import StringList +from jinja2 import Environment -from sphinx.util.rst import append_epilog, escape, prepend_prolog +from sphinx.util.rst import ( + append_epilog, escape, heading, prepend_prolog, textwidth +) def test_escape(): @@ -83,3 +86,34 @@ def test_prepend_prolog_without_CR(app): ('', 0, ''), ('dummy.rst', 0, 'hello Sphinx world'), ('dummy.rst', 1, 'Sphinx is a document generator')] + + +def test_textwidth(): + assert textwidth('Hello') == 5 + assert textwidth('русский язык') == 12 + assert textwidth('русский язык', 'WFA') == 23 # Cyrillic are ambiguous chars + + +def test_heading(): + env = Environment() + env.extend(language=None) + + assert heading(env, 'Hello') == ('Hello\n' + '=====') + assert heading(env, 'Hello', 1) == ('Hello\n' + '=====') + assert heading(env, 'Hello', 2) == ('Hello\n' + '-----') + assert heading(env, 'Hello', 3) == ('Hello\n' + '~~~~~') + assert heading(env, 'русский язык', 1) == ( + 'русский язык\n' + '============' + ) + + # language=ja: ambiguous + env.language = 'ja' + assert heading(env, 'русский язык', 1) == ( + 'русский язык\n' + '=======================' + ) From d960f2265d7a035459f0b0200c26d07dd999c4b5 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Thu, 2 May 2019 13:46:22 +0900 Subject: [PATCH 2/2] Add ReSTRenderer --- sphinx/util/template.py | 16 +++++++++++++++- tests/test_util_template.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 tests/test_util_template.py diff --git a/sphinx/util/template.py b/sphinx/util/template.py index c33e16819..b521c5c79 100644 --- a/sphinx/util/template.py +++ b/sphinx/util/template.py @@ -15,7 +15,7 @@ from jinja2.sandbox import SandboxedEnvironment from sphinx import package_dir from sphinx.jinja2glue import SphinxFileSystemLoader from sphinx.locale import get_translator -from sphinx.util import texescape +from sphinx.util import rst, texescape if False: # For type annotation @@ -84,3 +84,17 @@ class LaTeXRenderer(SphinxRenderer): self.env.variable_end_string = '%>' self.env.block_start_string = '<%' self.env.block_end_string = '%>' + + +class ReSTRenderer(SphinxRenderer): + def __init__(self, template_path=None, language=None): + # type: (str, str) -> None + super().__init__(template_path) + + # add language to environment + self.env.extend(language=language) + + # use texescape as escape filter + self.env.filters['e'] = rst.escape + self.env.filters['escape'] = rst.escape + self.env.filters['heading'] = rst.heading diff --git a/tests/test_util_template.py b/tests/test_util_template.py new file mode 100644 index 000000000..b25e9dc87 --- /dev/null +++ b/tests/test_util_template.py @@ -0,0 +1,37 @@ +""" + test_util_template + ~~~~~~~~~~~~~~~~~~ + + Tests sphinx.util.template functions. + + :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +from sphinx.util.template import ReSTRenderer + + +def test_ReSTRenderer_escape(): + r = ReSTRenderer() + template = '{{ "*hello*" | e }}' + assert r.render_string(template, {}) == r'\*hello\*' + + +def test_ReSTRenderer_heading(): + r = ReSTRenderer() + + template = '{{ "hello" | heading }}' + assert r.render_string(template, {}) == 'hello\n=====' + + template = '{{ "hello" | heading(1) }}' + assert r.render_string(template, {}) == 'hello\n=====' + + template = '{{ "русский язык" | heading(2) }}' + assert r.render_string(template, {}) == ('русский язык\n' + '------------') + + # language: ja + r.env.language = 'ja' + template = '{{ "русский язык" | heading }}' + assert r.render_string(template, {}) == ('русский язык\n' + '=======================')