File: __init__.py

package info (click to toggle)
python-docutils 0.22%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: experimental
  • size: 11,448 kB
  • sloc: python: 53,302; lisp: 14,475; xml: 1,807; javascript: 1,032; makefile: 102; sh: 96
file content (398 lines) | stat: -rw-r--r-- 16,032 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
# $Id: __init__.py 10136 2025-05-20 15:48:27Z milde $
# :Author: Günter Milde <milde@users.sf.net>
#          Based on the html4css1 writer by David Goodger.
# :Maintainer: docutils-develop@lists.sourceforge.net
# :Copyright: © 2005, 2009, 2015 Günter Milde,
#             portions from html4css1 © David Goodger.
# :License: Released under the terms of the `2-Clause BSD license`_, in short:
#
#    Copying and distribution of this file, with or without modification,
#    are permitted in any medium without royalty provided the copyright
#    notice and this notice are preserved.
#    This file is offered as-is, without any warranty.
#
# .. _2-Clause BSD license: https://opensource.org/licenses/BSD-2-Clause

# Use "best practice" as recommended by the W3C:
# http://www.w3.org/2009/cheatsheet/

"""
Plain HyperText Markup Language document tree Writer.

The output conforms to the `HTML 5` specification.

The cascading style sheet "minimal.css" is required for proper viewing,
the style sheet "plain.css" improves reading experience.
"""

from __future__ import annotations

__docformat__ = 'reStructuredText'

import os.path
from pathlib import Path

import docutils
from docutils import frontend, nodes
from docutils.writers import _html_base


class Writer(_html_base.Writer):

    supported = ('html5', 'xhtml', 'html')
    """Formats this writer supports."""

    default_stylesheets = ['minimal.css', 'plain.css']
    default_stylesheet_dirs = ['.', docutils._datadir(os.path.abspath(__file__))]
    default_template = Path(docutils._datadir(os.path.abspath(__file__))) / 'template.txt'

    # use a copy of the parent spec with some modifications
    settings_spec = frontend.filter_settings_spec(
        _html_base.Writer.settings_spec,
        template=(
            f'Template file. (UTF-8 encoded, default: "{default_template}")',
            ['--template'],
            {'default': default_template, 'metavar': '<file>'}),
        stylesheet_path=(
            'Comma separated list of stylesheet paths. '
            'Relative paths are expanded if a matching file is found in '
            'the --stylesheet-dirs. With --link-stylesheet, '
            'the path is rewritten relative to the output HTML file. '
            '(default: "%s")' % ','.join(default_stylesheets),
            ['--stylesheet-path'],
            {'metavar': '<file[,file,...]>', 'overrides': 'stylesheet',
             'validator': frontend.validate_comma_separated_list,
             'default': default_stylesheets}),
        stylesheet_dirs=(
            'Comma-separated list of directories where stylesheets are found. '
            'Used by --stylesheet-path when expanding relative path '
            'arguments. (default: "%s")' % ','.join(default_stylesheet_dirs),
            ['--stylesheet-dirs'],
            {'metavar': '<dir[,dir,...]>',
             'validator': frontend.validate_comma_separated_list,
             'default': default_stylesheet_dirs}),
        initial_header_level=(
            'Specify the initial header level. Does not affect document '
            'title & subtitle (see --no-doc-title). (default: 2 for "<h2>")',
            ['--initial-header-level'],
            {'choices': '1 2 3 4 5 6'.split(), 'default': '2',
             'metavar': '<level>'}),
        no_xml_declaration=(
            'Omit the XML declaration (default).',
            ['--no-xml-declaration'],
            {'dest': 'xml_declaration', 'action': 'store_false'}),
    )
    settings_spec = settings_spec + (
        'HTML5 Writer Options',
        '',
        ((frontend.SUPPRESS_HELP,  # Obsoleted by "--image-loading"
          ['--embed-images'],
          {'action': 'store_true',
           'validator': frontend.validate_boolean}),
         (frontend.SUPPRESS_HELP,  # Obsoleted by "--image-loading"
          ['--link-images'],
          {'dest': 'embed_images', 'action': 'store_false'}),
         ('Suggest at which point images should be loaded: '
          '"embed", "link" (default), or "lazy".',
          ['--image-loading'],
          {'choices': ('embed', 'link', 'lazy'),
           # 'default': 'link'  # default set in _html_base.py
           }),
         ('Append a self-link to section headings.',
          ['--section-self-link'],
          {'default': False, 'action': 'store_true'}),
         ('Do not append a self-link to section headings. (default)',
          ['--no-section-self-link'],
          {'dest': 'section_self_link', 'action': 'store_false'}),
         )
        )

    config_section = 'html5 writer'

    def __init__(self) -> None:
        self.parts = {}
        self.translator_class = HTMLTranslator


class HTMLTranslator(_html_base.HTMLTranslator):
    """
    This writer generates `polyglot markup`: HTML5 that is also valid XML.

    Safe subclassing: when overriding, treat ``visit_*`` and ``depart_*``
    methods as a unit to prevent breaks due to internal changes. See the
    docstring of docutils.writers._html_base.HTMLTranslator for details
    and examples.
    """

    # self.starttag() arguments for the main document
    documenttag_args = {'tagname': 'main'}

    # add meta tag to fix rendering in mobile browsers
    def __init__(self, document) -> None:
        super().__init__(document)
        self.meta.append('<meta name="viewport" '
                         'content="width=device-width, initial-scale=1" />\n')

    # <acronym> tag obsolete in HTML5. Use the <abbr> tag instead.
    def visit_acronym(self, node) -> None:
        # @@@ implementation incomplete ("title" attribute)
        self.body.append(self.starttag(node, 'abbr', ''))

    def depart_acronym(self, node) -> None:
        self.body.append('</abbr>')

    # no standard meta tag name in HTML5, use separate "author" meta tags
    # https://www.w3.org/TR/html5/document-metadata.html#standard-metadata-names
    def visit_authors(self, node) -> None:
        self.visit_docinfo_item(node, 'authors', meta=False)
        for subnode in node:
            self.meta.append('<meta name="author" content='
                             f'"{self.attval(subnode.astext())}" />\n')

    def depart_authors(self, node) -> None:
        self.depart_docinfo_item()

    # use the <figcaption> semantic tag.
    def visit_caption(self, node) -> None:
        if isinstance(node.parent, nodes.figure):
            self.body.append(self.starttag(node, 'figcaption'))
        self.body.append('<p>')

    def depart_caption(self, node) -> None:
        self.body.append('</p>\n')
        # <figcaption> is closed in depart_figure(), as legend may follow.

    # use HTML block-level tags if matching class value found
    supported_block_tags = {'ins', 'del'}

    def visit_container(self, node) -> None:
        # If there is exactly one of the "supported block tags" in
        # the list of class values, use it as tag name:
        classes = node['classes']
        tags = [cls for cls in classes
                if cls in self.supported_block_tags]
        if len(tags) == 1:
            node.html5tagname = tags[0]
            classes.remove(tags[0])
        else:
            node.html5tagname = 'div'
        self.body.append(self.starttag(node, node.html5tagname,
                                       CLASS='docutils container'))

    def depart_container(self, node) -> None:
        self.body.append(f'</{node.html5tagname}>\n')
        del node.html5tagname

    # no standard meta tag name in HTML5, use dcterms.rights
    # see https://wiki.whatwg.org/wiki/MetaExtensions
    def visit_copyright(self, node) -> None:
        self.visit_docinfo_item(node, 'copyright', meta=False)
        self.meta.append('<meta name="dcterms.rights" '
                         f'content="{self.attval(node.astext())}" />\n')

    def depart_copyright(self, node) -> None:
        self.depart_docinfo_item()

    # no standard meta tag name in HTML5, use dcterms.date
    def visit_date(self, node) -> None:
        self.visit_docinfo_item(node, 'date', meta=False)
        self.meta.append('<meta name="dcterms.date" '
                         f'content="{self.attval(node.astext())}" />\n')

    def depart_date(self, node) -> None:
        self.depart_docinfo_item()

    # use new HTML5 <figure> and <figcaption> elements
    def visit_figure(self, node) -> None:
        atts = {}
        if 'width' in node:
            atts['style'] = f"width: {node['width']}"
        if node.get('align'):
            atts['class'] = f"align-{node['align']}"
        self.body.append(self.starttag(node, 'figure', **atts))

    def depart_figure(self, node) -> None:
        if len(node) > 1:
            self.body.append('</figcaption>\n')
        self.body.append('</figure>\n')

    # use HTML5 <footer> element
    def visit_footer(self, node) -> None:
        self.context.append(len(self.body))

    def depart_footer(self, node) -> None:
        start = self.context.pop()
        footer = [self.starttag(node, 'footer')]
        footer.extend(self.body[start:])
        footer.append('</footer>\n')
        self.footer.extend(footer)
        self.body_suffix[:0] = footer
        del self.body[start:]

    # use HTML5 <header> element
    def visit_header(self, node) -> None:
        self.context.append(len(self.body))

    def depart_header(self, node) -> None:
        start = self.context.pop()
        header = [self.starttag(node, 'header')]
        header.extend(self.body[start:])
        header.append('</header>\n')
        self.body_prefix.extend(header)
        self.header.extend(header)
        del self.body[start:]

    # use HTML text-level tags if matching class value found
    supported_inline_tags = {'code', 'kbd', 'dfn', 'samp', 'var',
                             'bdi', 'del', 'ins', 'mark', 'small',
                             'b', 'i', 'q', 's', 'u'}

    # Use `supported_inline_tags` if found in class values
    def visit_inline(self, node) -> None:
        classes = node['classes']
        node.html5tagname = 'span'
        # Special handling for "code" directive content
        if (isinstance(node.parent, nodes.literal_block)
            and 'code' in node.parent.get('classes')
            or isinstance(node.parent, nodes.literal)
            and getattr(node.parent, 'html5tagname', None) == 'code'):
            if classes == ['ln']:
                # line numbers are not part of the "fragment of computer code"
                if self.body[-1] == '<code>':
                    del self.body[-1]
                else:
                    self.body.append('</code>')
                node.html5tagname = 'small'
        else:
            tags = [cls for cls in self.supported_inline_tags
                    if cls in classes]
            if len(tags):
                node.html5tagname = tags[0]
                classes.remove(node.html5tagname)
        self.body.append(self.starttag(node, node.html5tagname, ''))

    def depart_inline(self, node) -> None:
        self.body.append(f'</{node.html5tagname}>')
        if (node.html5tagname == 'small' and node.get('classes') == ['ln']
            and isinstance(node.parent, nodes.literal_block)):
            self.body.append(f'<code data-lineno="{node.astext()}">')
        del node.html5tagname

    # place inside HTML5 <figcaption> element (together with caption)
    def visit_legend(self, node) -> None:
        if not isinstance(node.previous_sibling(), nodes.caption):
            self.body.append('<figcaption>\n')
        self.body.append(self.starttag(node, 'div', CLASS='legend'))

    def depart_legend(self, node) -> None:
        self.body.append('</div>\n')
        # <figcaption> closed in visit_figure()

    # use HTML5 text-level tags if matching class value found
    def visit_literal(self, node):
        classes = node['classes']
        html5tagname = 'span'
        tags = [cls for cls in self.supported_inline_tags
                if cls in classes]
        if len(tags):
            html5tagname = tags[0]
            classes.remove(html5tagname)
        if html5tagname == 'code':
            node.html5tagname = html5tagname
            self.body.append(self.starttag(node, html5tagname, ''))
            return
        self.body.append(
            self.starttag(node, html5tagname, '', CLASS='docutils literal'))
        text = node.astext()
        # remove hard line breaks (except if in a parsed-literal block)
        if not isinstance(node.parent, nodes.literal_block):
            text = text.replace('\n', ' ')
        # Protect text like ``--an-option`` and the regular expression
        # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
        for token in self.words_and_spaces.findall(text):
            if token.strip() and self.in_word_wrap_point.search(token):
                self.body.append(
                    f'<span class="pre">{self.encode(token)}</span>')
            else:
                self.body.append(self.encode(token))
        self.body.append(f'</{html5tagname}>')
        # Content already processed:
        raise nodes.SkipNode

    def depart_literal(self, node) -> None:
        # skipped unless literal element is from "code" role:
        self.depart_inline(node)

    # Meta tags: 'lang' attribute replaced by 'xml:lang' in XHTML 1.1
    # HTML5/polyglot recommends using both
    def visit_meta(self, node) -> None:
        if node.hasattr('lang'):
            node['xml:lang'] = node['lang']
        self.meta.append(self.emptytag(node, 'meta',
                                       **node.non_default_attributes()))

    def depart_meta(self, node) -> None:
        pass

    # no standard meta tag name in HTML5
    def visit_organization(self, node) -> None:
        self.visit_docinfo_item(node, 'organization', meta=False)

    def depart_organization(self, node) -> None:
        self.depart_docinfo_item()

    # use the new HTML5 element <section>
    def visit_section(self, node) -> None:
        self.section_level += 1
        self.body.append(
            self.starttag(node, 'section'))

    def depart_section(self, node) -> None:
        self.section_level -= 1
        self.body.append('</section>\n')

    # use the new HTML5 element <aside>
    def visit_sidebar(self, node) -> None:
        self.body.append(
            self.starttag(node, 'aside', CLASS='sidebar'))
        self.in_sidebar = True

    def depart_sidebar(self, node) -> None:
        self.body.append('</aside>\n')
        self.in_sidebar = False

    # Use new HTML5 element <aside> or <nav>
    # Add class value to <body>, if there is a ToC in the document
    # (see responsive.css how this is used for a navigation sidebar).
    def visit_topic(self, node) -> None:
        atts = {'classes': ['topic']}
        if 'contents' in node['classes']:
            node.html5tagname = 'nav'
            del atts['classes']
            if isinstance(node.parent, nodes.document):
                atts['role'] = 'doc-toc'
                self.body_prefix[0] = '</head>\n<body class="with-toc">\n'
        elif 'abstract' in node['classes']:
            node.html5tagname = 'div'
            atts['role'] = 'doc-abstract'
        elif 'dedication' in node['classes']:
            node.html5tagname = 'div'
            atts['role'] = 'doc-dedication'
        else:
            node.html5tagname = 'aside'
        self.body.append(self.starttag(node, node.html5tagname, **atts))

    def depart_topic(self, node) -> None:
        self.body.append(f'</{node.html5tagname}>\n')
        del node.html5tagname

    # append self-link
    def section_title_tags(self, node):
        start_tag, close_tag = super().section_title_tags(node)
        ids = node.parent['ids']
        if (ids and getattr(self.settings, 'section_self_link', None)
            and not isinstance(node.parent, nodes.document)):
            self_link = ('<a class="self-link" title="link to this section"'
                         f' href="#{ids[0]}"></a>')
            close_tag = close_tag.replace('</h', self_link + '</h')
        return start_tag, close_tag