#! /usr/bin/env python3
# $Id: test_html5_polyglot.py 10135 2025-05-20 15:07:28Z milde $
# Author: reggie dugard
# Maintainer: docutils-develop@lists.sourceforge.net
# Copyright: This module has been placed in the public domain.
"""Test HTML5 writer output ("fragment"/"body" part).
This is the document body (not HTML ).
"""
from pathlib import Path
import sys
import unittest
if __name__ == '__main__':
# prepend the "docutils root" to the Python library path
# so we import the local `docutils` package.
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
import docutils
import docutils.core
from docutils.parsers.rst.directives.images import PIL
from docutils.utils.code_analyzer import with_pygments
from docutils.writers import html5_polyglot
if with_pygments:
import pygments
if tuple(map(int, pygments.__version__.split('.')[:2])) < (2, 14):
# pygments output changed in version 2.14
with_pygments = False
# TEST_ROOT is ./test/ from the docutils root
TEST_ROOT = Path(__file__).parents[1]
DATA_ROOT = TEST_ROOT / 'data'
ROOT_PREFIX = (TEST_ROOT / 'functional/input').as_posix()
# Pillow/PIL is optional:
if PIL:
REQUIRES_PIL = ''
ONLY_LOCAL = 'Cannot get file path corresponding to https://dummy.png.'
# Pillow versions vary in their error output (cf. bugs: #485 and #500)
try:
PIL.Image.open(Path('dummy.png'))
except OSError as err:
DUMMY_PNG_NOT_FOUND = str(err)
HEIGHT_ATTR = 'height="32" '
WIDTH_ATTR = 'width="32" '
NO_PIL_SYSTEM_MESSAGE = ''
else:
REQUIRES_PIL = '\n Requires Python Imaging Library.'
ONLY_LOCAL = 'Requires Python Imaging Library.'
DUMMY_PNG_NOT_FOUND = 'Requires Python Imaging Library.'
HEIGHT_ATTR = ''
WIDTH_ATTR = ''
NO_PIL_SYSTEM_MESSAGE = (
'\n'
'System Message:'
' WARNING/2 ('
'<string> , line 1)
\n'
'Cannot scale image!\n'
' Could not get size from "/data/blue%20square.png":\n'
' Requires Python Imaging Library.
\n'
' \n')
class Html5WriterPublishPartsTestCase(unittest.TestCase):
"""Test case for HTML5 writer via the publish_parts() interface."""
maxDiff = None
def test_publish(self):
for name, (settings_overrides, cases) in totest.items():
for casenum, (case_input, case_expected) in enumerate(cases):
with self.subTest(id=f'totest[{name!r}][{casenum}]'):
if name == 'syntax_highlight' and not with_pygments:
self.skipTest('syntax highlight requires pygments')
parts = docutils.core.publish_parts(
source=case_input,
writer=html5_polyglot.Writer(),
settings_overrides={
'_disable_config': True,
'strict_visitor': True,
'stylesheet_path': '',
'section_self_link': True,
**settings_overrides,
}
)
self.assertEqual(case_expected, parts['body'])
totest = {} # expected samples contain only the "body" part of the HMTL output
totest['standard'] = ({}, [
["""\
Simple String
""",
'Simple String
\n',
],
["""\
Simple String with *markup*
""",
'Simple String with markup
\n',
],
["""\
Simple String with an even simpler ``inline literal``
""",
'Simple String with an even simpler inline literal
\n',
],
["""\
A simple `anonymous reference`__
__ http://www.test.com/test_url
""",
'A simple anonymous reference
\n',
],
["""\
One paragraph.
Two paragraphs.
""",
"""\
One paragraph.
Two paragraphs.
""",
],
["""\
A simple `named reference`_ with stuff in between the
reference and the target.
.. _`named reference`: http://www.test.com/test_url
""",
"""\
A simple named reference with stuff in between the
reference and the target.
""",
],
["""\
.. [CIT2022] A citation.
""",
"""\
""",
],
[f"""\
.. image:: {DATA_ROOT.as_uri()}/circle.svg
:loading: embed
:width: 50%
:height: 30
:align: left
""",
"""\
"""],
])
totest['no_title_promotion'] = ({'doctitle_xform': False}, [
["""\
+++++
Title
+++++
Not A Subtitle
==============
Some stuff
Section
-------
Some more stuff
Another Section
...............
And even more stuff
""",
"""\
Title
Not A Subtitle
Some stuff
Section
Some more stuff
Another Section
And even more stuff
""",
],
["""\
* bullet
* list
""",
"""\
""",
],
["""\
.. table::
:align: right
:width: 320
+-----+-----+
| 1 | 2 |
+-----+-----+
| 3 | 4 |
+-----+-----+
""",
"""\
""",
],
["""\
Not a docinfo.
:This: .. _target:
is
:a:
:simple:
:field: list
""",
"""\
Not a docinfo.
This:
is
a:
simple:
field:
list
""",
],
["""\
Not a docinfo.
:This is: a
:simple field list with loooong field: names
""",
"""\
Not a docinfo.
This is:
a
simple field list with loooong field:
names
""",
],
["""\
Not a docinfo.
.. class:: field-indent-200
:This: is a
:simple: field list with custom indent.
""",
"""\
Not a docinfo.
This:
is a
simple:
field list with custom indent.
""",
],
["""\
Not a docinfo.
.. class:: field-indent-200uf
:This: is a
:simple: field list without custom indent,
because the unit "uf" is invalid.
""",
"""\
Not a docinfo.
This:
is a
simple:
field list without custom indent,
because the unit "uf" is invalid.
""",
],
["""\
.. figure:: dummy.png
:figname: fig:dummy
The figure's caption.
A legend.
The legend's second paragraph.
""",
"""\
The figure's caption.
A legend.
The legend's second paragraph.
""",
],
["""\
.. figure:: dummy.png
The figure's caption, no legend.
""",
"""\
The figure's caption, no legend.
""",
],
["""\
.. figure:: dummy.png
..
A legend without caption.
""",
"""\
A legend without caption.
""",
],
["""\
.. figure:: dummy.png
No caption nor legend.
""",
"""\
No caption nor legend.
""",
],
[f"""\
.. include:: {DATA_ROOT}/multiple-term-definition.xml
:parser: xml
""",
"""\
New in Docutils 0.22
A definition list item may contain several
terms with optional classifier(s).
However, there is currently no corresponding
reStructuredText syntax.
term 2a
term 2b
definition 2
term 3aclassifier 3a classifier 3aa term 3bclassifier 3b
definition 3
""",
],
])
totest['lazy_loading'] = ({'image_loading': 'lazy',
'report_level': 4}, [
["""\
Lazy loading by default, overridden by :loading: option
("cannot embed" warning ignored).
.. image:: dummy.png
.. image:: dummy.png
:loading: link
.. figure:: dummy.png
.. figure:: dummy.png
:loading: embed
""",
"""\
Lazy loading by default, overridden by :loading: option
("cannot embed" warning ignored).
""",
],
])
totest['root_prefix'] = ({'root_prefix': ROOT_PREFIX,
'image_loading': 'embed',
'warning_stream': '',
}, [
["""\
.. image:: /data/blue%20square.png
:scale: 100%
.. figure:: /data/blue%20square.png
""",
f' \n{NO_PIL_SYSTEM_MESSAGE}'
'\n'
' \n'
' \n',
],
])
totest['no_backlinks'] = ({'footnote_backlinks': False}, [
["""\
Two footnotes [#f1]_ [#f2]_ and two citations [once]_ [twice]_.
The latter are referenced a second time [#f2]_ [twice]_.
.. [#f1] referenced once
.. [#f2] referenced twice
.. [once] citation referenced once
.. [twice] citation referenced twice
""",
"""\
Two footnotes and two citations [once] [twice] .
The latter are referenced a second time [twice] .
[ once]
citation referenced once
[ twice]
citation referenced twice
""",
],
])
totest['syntax_highlight'] = ({'syntax_highlight': 'short',
}, [
["""\
.. code:: shell
cat <cat <<EOF
Hello World
EOF
""",
],
["""\
.. role:: shell(code)
:language: shell
:shell:`cat <cat <<EOF Hello World EOF
""",
],
])
totest['system_messages'] = ({'math_output': 'mathml',
'warning_stream': '',
}, [
["""\
.. image:: https://dummy.png
:loading: embed
""",
"""\
System Message: ERROR/3 \
(<string> , line 1)
Cannot embed image "https://dummy.png":
Cannot get file path corresponding to https://dummy.png.
""",
],
[f"""\
.. image:: {DATA_ROOT.as_uri()}/circle-broken.svg
:loading: embed
""",
f"""\
System Message: ERROR/3 (<string> , line 1)
Cannot parse SVG image "{DATA_ROOT.as_uri()}/circle-broken.svg":
not well-formed (invalid token): line 3, column 48
"""
],
[r"""Broken :math:`\sin \my`.
""",
"""\
Broken \\sin \\my .
System Message: WARNING/2 (<string> , line 1)
Unknown LaTeX command "\\my".
"""],
])
totest['system_messages-PIL'] = ({'math_output': 'mathml',
'warning_stream': '',
}, [
["""\
.. image:: dummy.png
:scale: 100%
:loading: embed
""",
f"""\
System Message: WARNING/2 \
(<string> , line 1)
Cannot scale image!
Could not get size from "dummy.png":
{DUMMY_PNG_NOT_FOUND}
System Message: ERROR/3 \
(<string> , line 1)
Cannot embed image "dummy.png":
[Errno 2] No such file or directory: 'dummy.png'
""",
],
["""\
.. image:: dummy.mp4
:scale: 100%
""",
f"""\
dummy.mp4
System Message: WARNING/2 \
(<string> , line 1)
Cannot scale image!
Could not get size from "dummy.mp4":{REQUIRES_PIL}
PIL cannot read video images.
""",
],
["""\
.. image:: https://dummy.png
:scale: 100%
:loading: embed
""",
f"""\
System Message: WARNING/2 \
(<string> , line 1)
Cannot scale image!
Could not get size from "https://dummy.png":
{ONLY_LOCAL}
System Message: ERROR/3 \
(<string> , line 1)
Cannot embed image "https://dummy.png":
Cannot get file path corresponding to https://dummy.png.
""",
],
])
totest['no_system_messages'] = ({'math_output': 'mathml',
'report_level': 4,
'warning_stream': '',
}, [
["""\
.. image:: dummy.png
:scale: 100%
:loading: embed
.. image:: dummy.mp4
:scale: 100%
""",
"""\
dummy.mp4
""",
],
[f"""\
.. image:: {DATA_ROOT.as_uri()}/circle-broken.svg
:loading: embed
""",
"""\
"""],
[r'Broken :math:`\sin \my`.',
'Broken \\sin \\my .
\n'
],
])
if __name__ == '__main__':
unittest.main()