Skip to content

Commit

Permalink
Merge pull request #6332 from tk0miya/rest_renderer
Browse files Browse the repository at this point in the history
Add ReSTRenderer; a helper for rendering reST text
  • Loading branch information
tk0miya authored May 3, 2019
2 parents 1039937 + d960f22 commit 035d550
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 3 deletions.
35 changes: 34 additions & 1 deletion sphinx/util/rst.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,35 @@
"""

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
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):
Expand All @@ -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
Expand Down
16 changes: 15 additions & 1 deletion sphinx/util/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
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
Expand Down Expand Up @@ -84,3 +84,17 @@ def __init__(self, template_path=None):
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
36 changes: 35 additions & 1 deletion tests/test_util_rst.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -83,3 +86,34 @@ def test_prepend_prolog_without_CR(app):
('<generated>', 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'
'======================='
)
37 changes: 37 additions & 0 deletions tests/test_util_template.py
Original file line number Diff line number Diff line change
@@ -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'
'=======================')

0 comments on commit 035d550

Please sign in to comment.