GitHubMarkdown
Summary
- Package:
- Doc-string:
documented
File |
Expected |
Covered |
Uncovered |
Coverage |
|---|---|---|---|---|
GitHubMarkdown.py |
30 |
30 |
0 |
100.0% |
Source
pyTooling.Sphinx/GitHubMarkdown.pydocumentedundocumentedignoredexcludednot counted
31"""
32A converter of GitHub-flavoured Markdown into docutils nodes.
33
34The Markdown is e.g. the notes of a GitHub release. :class:`GitHubMarkdown` parses it with markdown-it-py and the
35GFM plugin of mdit-py-plugins - CommonMark plus tables, strikethrough, task lists, alerts, autolinks and footnotes -,
36and :class:`~pyTooling.Sphinx.Markdown.MarkdownTranslator` translates the tokens into the nodes a directive
37returns:
38
39.. code-block:: Python
40
41 converter = GitHubMarkdown("pyTooling", "pyTooling")
42 return converter.Convert(markdown, self.state.document, location=self.get_location())
43
44.. rubric:: GitHub's references
45
46GitHub links what Markdown leaves as text: ``#123``, ``GH-123`` and ``owner/name#123`` to an issue or a pull request,
47``@user`` to a user, a commit's hash (7 to 40 hexadecimal digits) to the commit, and ``:tada:`` is an emoji. The
48converter does the same in text outside of code and links (see :meth:`GitHubMarkdown.Parse`). GitHub links a
49hash only if the commit exists; the converter can't ask, so it links a word of hexadecimal digits holding a digit and a
50letter.
51
52.. rubric:: Emoji
53
54The emoji, their characters and shortcodes, are GitHub's own list: the table :data:`EMOJI_FILE` is generated from
55github/gemoji's :file:`db/emoji.json`, which GitHub, Inc. distributes under the MIT license; the table's header names
56the version and holds the license text. A custom GitHub emoji, e.g. ``:octocat:``, is an image without characters, and
57isn't in the list.
58
59.. rubric:: Headings and raw HTML
60
61A heading becomes a topic or a rubric, never a section, and raw HTML is translated element by element, as
62:mod:`~pyTooling.Sphinx.Markdown` describes.
63
64.. rubric:: LaTeX
65
66An emoji is a node of its own (:class:`~pyTooling.Sphinx.Node.Emoji`), whose characters LaTeX writes as they are, for
67LuaLaTeX and an emoji font.
68
69LaTeX can't include an SVG image, e.g. a badge. :class:`RasterizeShieldsBadges` fetches a shields.io badge as PNG;
70every other SVG image is converted by Sphinx' ImageMagick converter, :mod:`sphinx.ext.imgconverter`, which
71pyTooling.Sphinx loads. Without ImageMagick, Sphinx warns, and LaTeX stops at the image with "Unknown graphics
72extension: .svg".
73
74.. attention::
75
76 Packages ``markdown_it`` and ``mdit_py_plugins`` are imported when a :class:`GitHubMarkdown` is created, not when
77 this module is imported. They are installed with the extra ``markdown``.
78"""
...
99
101__all__ = ["EMOJI_FILE", "OWNER_PATTERN", "REPOSITORY_NAME_PATTERN", "REPOSITORY_PATTERN", "REFERENCE_PATTERN"]
102
103#: Name of the emoji table in :mod:`pyTooling.Sphinx.Resources`: a line per emoji, its code points and its shortcodes,
104#: generated from github/gemoji (MIT license, Copyright (c) 2019 GitHub, Inc.).
105EMOJI_FILE = "GitHubEmoji.txt"
106
107#: A GitHub owner - a user or an organization: ASCII letters, digits and single hyphens, not starting with a hyphen.
108OWNER_PATTERN = re_compile(r"[A-Za-z0-9](?:-?[A-Za-z0-9])*")
109
110#: A GitHub repository's name: ASCII letters, digits, ``.``, ``_`` and ``-``.
111REPOSITORY_NAME_PATTERN = re_compile(r"[A-Za-z0-9._-]+")
112
113#: A GitHub repository with its owner, as a reference writes it: ``<owner>/<name>``.
114REPOSITORY_PATTERN = re_compile(rf"{OWNER_PATTERN.pattern}/{REPOSITORY_NAME_PATTERN.pattern}")
115
116_BEFORE = r"(?<![\w/@#.-])"
117"""What can't precede a reference: a word character, or a character continuing a path, a reference or a URL."""
118
119#: An emoji's shortcode, or one of GitHub's references: an issue or pull request, a commit, or a user.
120REFERENCE_PATTERN = re_compile(
121 r":(?P<shortcode>[a-z0-9_+-]+):"
122 rf"|{_BEFORE}(?:(?P<issueRepository>{REPOSITORY_PATTERN.pattern})#|GH-|#)(?P<issue>[0-9]+)(?![\w-])"
123 rf"|{_BEFORE}(?:(?P<commitRepository>{REPOSITORY_PATTERN.pattern})@)?(?P<sha>[0-9a-f]{{7,40}})(?![\w-])"
124 rf"|{_BEFORE}@(?P<user>{OWNER_PATTERN.pattern})(?![\w/-])"
125)
126
128@export
129class GitHubMarkdown(metaclass=ExtendedType, slots=True):
130 """
131 A converter of GitHub-flavoured Markdown into docutils nodes, which links GitHub's references in a repository's
132 context.
133
134 The owner and the repository are the ones ``#123`` refers to, and the server URL the one every link starts with.
135 :meth:`Convert` converts a Markdown document into docutils nodes, :meth:`Parse` parses a text without Markdown for
136 GitHub's references and emoji.
137
138 GitHub writes a line break of an issue, a comment or a release's notes as a line break, not as a space:
139 :attr:`LineBreaks` selects which.
140 """
141
142 _emojis: ClassVar[Nullable[dict[str, str]]] = None #: The emoji's characters by shortcode, once read.
143 _shortcodes: ClassVar[Nullable[dict[str, str]]] = None #: A shortcode per emoji, by its characters without U+FE0F.
144 _emojiPattern: ClassVar[Nullable[Pattern[str]]] = None #: The pattern finding emoji written as characters.
145
146 _owner: str #: The owner of the repository references refer to, a user or an organization.
147 _repository: str #: The name of the repository references refer to.
148 _serverURL: str #: The URL of the GitHub server, without a trailing ``/``.
149 _lineBreaks: bool #: Whether a line break in a paragraph is written as a line break.
150 _parser: MarkdownIt #: The Markdown parser.
151
152 def __init__(
153 self,
154 owner: str,
155 repository: str,
156 serverURL: str = "https://github.com",
157 lineBreaks: bool = True
158 ) -> None:
159 """
160 Initializes a converter for the Markdown of a repository.
161
162 :param owner: The owner of the repository references refer to, a user or an organization.
163 :param repository: The name of the repository references refer to.
164 :param serverURL: Optional, URL of the GitHub server. Default: ``"https://github.com"``.
165 :param lineBreaks: Optional, whether a line break in a paragraph is written as a line break, as
166 GitHub does in a release's notes. Default: ``True``.
167 :raises ValueError: If parameter ``owner`` is ``None``.
168 :raises TypeError: If parameter ``owner`` isn't of type :class:`str`.
169 :raises ValueError: If parameter ``owner`` isn't a GitHub user's or organization's name.
170 :raises ValueError: If parameter ``repository`` is ``None``.
171 :raises TypeError: If parameter ``repository`` isn't of type :class:`str`.
172 :raises ValueError: If parameter ``repository`` isn't a GitHub repository's name.
173 :raises ValueError: If parameter ``serverURL`` is ``None``.
174 :raises TypeError: If parameter ``serverURL`` isn't of type :class:`str`.
175 :raises ValueError: If parameter ``serverURL`` isn't an HTTP or HTTPS URL.
176 :raises ValueError: If parameter ``lineBreaks`` is ``None``.
177 :raises TypeError: If parameter ``lineBreaks`` isn't of type :class:`bool`.
178 :raises MissingDependencyError: If the extra ``markdown`` isn't installed.
179 """
...
253
254 @staticmethod
255 def _EmojiPattern(shortcodes: dict[str, str]) -> Pattern[str]:
256 """
257 Build the pattern finding the emoji written as characters in a text: a trie of their characters, each optionally
258 followed by variation selector 16 (U+FE0F).
259
260 A single character below U+2190 - e.g. the copyright sign - is an emoji only if followed by U+FE0F; else it is
261 a character of the text font.
262
263 :param shortcodes: The emoji's characters without U+FE0F.
264 :returns: The pattern.
265 """
...
272
273 def alternatives(level: dict[str, Any], depth: int) -> str:
274 """
275 Nested function for recursion.
276
277 :param level: A level of the trie, by character; key ``""`` marks the end of an emoji.
278 :param depth: The level's depth, ``0`` for the first character.
279 :returns: The level's pattern.
280 """
...
294
...
296
297 @readonly
298 def Owner(self) -> str:
299 """
300 Read-only property to access the owner of the repository references refer to (:attr:`_owner`).
301
302 :returns: The owner, a user or an organization.
303 """
...
305
306 @readonly
307 def Repository(self) -> str:
308 """
309 Read-only property to access the name of the repository references refer to (:attr:`_repository`).
310
311 :returns: The repository's name.
312 """
...
314
315 @readonly
316 def ServerURL(self) -> str:
317 """
318 Read-only property to access the URL of the GitHub server (:attr:`_serverURL`).
319
320 :returns: The URL, without a trailing ``/``.
321 """
...
323
324 @readonly
325 def LineBreaks(self) -> bool:
326 """
327 Read-only property to access whether a line break in a paragraph is written as a line break (:attr:`_lineBreaks`).
328
329 :returns: ``True``, if a line break is written as a line break; ``False``, if as a space.
330 """
...
332
333 def Convert(
334 self,
335 markdown: str,
336 document: nodes.document,
337 repeatedTitle: str = "",
338 location: Nullable[str] = None
339 ) -> list[nodes.Node]:
340 """
341 Convert a Markdown document into docutils nodes.
342
343 The nodes are body elements a directive returns; headings become topics and rubrics (see the module's
344 description). A footnote's ID is unique in the document.
345
346 The first heading of the Markdown can repeat the title, which the nodes are put under, e.g. ``# GHDL 5.0.1`` in
347 the notes of a release titled so. Given as ``repeatedTitle``, such a heading is skipped, so the headings after it
348 become the topics.
349
350 :param markdown: The Markdown.
351 :param document: The document the nodes are put into.
352 :param repeatedTitle: Optional, the title the first heading is skipped for; compared without surrounding and
353 repeated whitespace. Default: ``""``, no heading is skipped.
354 :param location: Optional, location the warnings are reported at, e.g. ``index.rst:12``. Default: ``None``.
355 :returns: The nodes.
356 :raises ValueError: If parameter ``markdown`` is ``None``.
357 :raises TypeError: If parameter ``markdown`` isn't of type :class:`str`.
358 :raises ValueError: If parameter ``document`` is ``None``.
359 :raises TypeError: If parameter ``document`` isn't of type :class:`~docutils.nodes.document`.
360 :raises ValueError: If parameter ``repeatedTitle`` is ``None``.
361 :raises TypeError: If parameter ``repeatedTitle`` isn't of type :class:`str`.
362 :raises TypeError: If parameter ``location`` isn't of type :class:`str`.
363 """
...
399
400 def Parse(self, text: str, references: bool = True) -> list[nodes.Node]:
401 """
402 Parse a text for GitHub's references and emoji into docutils nodes, linking the references and writing emoji as
403 :class:`~pyTooling.Sphinx.Node.Emoji` nodes.
404
405 An emoji is written as its shortcode, e.g. ``:tada:``, or as its characters. A shortcode GitHub doesn't know
406 stays text, as does a custom GitHub emoji, which has no characters, e.g. ``:octocat:``.
407
408 :param text: The text, without Markdown.
409 :param references: Optional, whether GitHub's references are linked; ``False`` e.g. for a link's text. Default:
410 ``True``.
411 :returns: The text, references and emoji.
412 :raises ValueError: If parameter ``text`` is ``None``.
413 :raises TypeError: If parameter ``text`` isn't of type :class:`str`.
414 """
...
461
462 def _ConvertEmojiCharacters(self, text: str) -> list[nodes.Node]:
463 """
464 Convert a text into text and :class:`~pyTooling.Sphinx.Node.Emoji` nodes for the emoji written as characters.
465
466 :param text: The text.
467 :returns: The text and the emoji; nothing for an empty text.
468 """
...
486
488@export
489class RasterizeShieldsBadges(SphinxPostTransform):
490 """
491 A post-transform fetching the shields.io badges of converted Markdown as PNG in LaTeX, which can't include SVG.
492
493 A badge's URI is moved to :data:`~pyTooling.Sphinx.Shields.SHIELDS_SERVICE_PNG`, without a suffix ``.svg``. Every
494 other SVG image is left to :mod:`sphinx.ext.imgconverter`, which pyTooling.Sphinx loads and which converts it with
495 ImageMagick.
496 """
497
498 default_priority = 50 #: Priority among the post-transforms: before Sphinx downloads remote images.
499 formats = ("latex", ) #: Output formats the transform runs for.
500
501 def run(self, **kwargs: Any) -> None:
502 """
503 Move the shields.io badges of converted Markdown to the service rasterizing them.
504
505 The images of converted Markdown have class :data:`~pyTooling.Sphinx.Markdown.IMAGE_CLASS`.
506
507 :param kwargs: Keyword arguments Sphinx passes to a post-transform; none is used.
508 """
...