GitHubMarkdown

Summary

Package:

pyTooling.Sphinx

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		"""
		...