diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d59f4cd..2d4a114 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -47,5 +47,7 @@ jobs: - uses: actions/setup-python@v5 - name: Install ruff run: python -m pip install ruff - - name: Run ruff + - name: Run ruff check run: ruff check --select F,E,W,I,UP --target-version py310 . + - name: Run ruff format + - run: ruff format --diff . diff --git a/docs/conf.py b/docs/conf.py index 301b4d9..b31a6e8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -6,7 +6,7 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath("..")) # -- General configuration ------------------------------------------------ @@ -14,21 +14,21 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', + "sphinx.ext.autodoc", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'Python-Markups' -copyright = '2023, Dmitry Shachnev' +project = "Python-Markups" +copyright = "2023, Dmitry Shachnev" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -36,15 +36,15 @@ from markups import __version__, __version_tuple__ # noqa: E402 # The short X.Y version. -version = '{}.{}'.format(*__version_tuple__) +version = "{}.{}".format(*__version_tuple__) # The full version, including alpha/beta/rc tags. release = __version__ # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'nature' +html_theme = "nature" diff --git a/markup2html.py b/markup2html.py index 275e094..6f030a7 100755 --- a/markup2html.py +++ b/markup2html.py @@ -11,26 +11,33 @@ def export_file(args: argparse.Namespace) -> None: with open(args.input_file) as input: text = input.read() if not markup: - sys.exit('Markup not available.') + sys.exit("Markup not available.") converted = markup.convert(text) - html = converted.get_whole_html(include_stylesheet=args.include_stylesheet, - fallback_title=args.fallback_title, - webenv=args.web_environment) + html = converted.get_whole_html( + include_stylesheet=args.include_stylesheet, + fallback_title=args.fallback_title, + webenv=args.web_environment, + ) - with open(args.output_file, 'w') as output: + with open(args.output_file, "w") as output: output.write(html) -if __name__ == '__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument('--web-environment', help='export for web environment', - action='store_true') - parser.add_argument('--include-stylesheet', help='embed the stylesheet into html', - action='store_true') - parser.add_argument('--fallback-title', help='fallback title of the HTML document', - metavar='TITLE') - parser.add_argument('input_file', help='input file') - parser.add_argument('output_file', help='output file') + parser.add_argument( + "--web-environment", help="export for web environment", action="store_true" + ) + parser.add_argument( + "--include-stylesheet", + help="embed the stylesheet into html", + action="store_true", + ) + parser.add_argument( + "--fallback-title", help="fallback title of the HTML document", metavar="TITLE" + ) + parser.add_argument("input_file", help="input file") + parser.add_argument("output_file", help="output file") args = parser.parse_args() export_file(args) diff --git a/markups/__init__.py b/markups/__init__.py index 306f840..7e5f87e 100644 --- a/markups/__init__.py +++ b/markups/__init__.py @@ -12,7 +12,7 @@ from markups.textile import TextileMarkup __version_tuple__ = (4, 0, 0) -__version__ = '.'.join(map(str, __version_tuple__)) +__version__ = ".".join(map(str, __version_tuple__)) __all__ = [ "AbstractMarkup", @@ -63,15 +63,13 @@ def get_available_markups() -> list[type[AbstractMarkup]]: @overload def get_markup_for_file_name( filename: str, return_class: Literal[False] = False -) -> AbstractMarkup | None: - ... +) -> AbstractMarkup | None: ... @overload def get_markup_for_file_name( filename: str, return_class: Literal[True] -) -> type[AbstractMarkup] | None: - ... +) -> type[AbstractMarkup] | None: ... def get_markup_for_file_name( diff --git a/markups/abstract.py b/markups/abstract.py index 3854410..1763c81 100644 --- a/markups/abstract.py +++ b/markups/abstract.py @@ -63,8 +63,9 @@ class ConvertedMarkup: method, usually it should not be instantiated directly. """ - def __init__(self, body: str, title: str = '', - stylesheet: str = '', javascript: str = ''): + def __init__( + self, body: str, title: str = "", stylesheet: str = "", javascript: str = "" + ): self.title = title self.stylesheet = stylesheet self.javascript = javascript @@ -99,8 +100,13 @@ def get_javascript(self, webenv: bool = False) -> str: """ return self.javascript - def get_whole_html(self, custom_headers: str = '', include_stylesheet: bool = True, - fallback_title: str = '', webenv: bool = False) -> str: + def get_whole_html( + self, + custom_headers: str = "", + include_stylesheet: bool = True, + fallback_title: str = "", + webenv: bool = False, + ) -> str: """ :returns: the full contents of the HTML document (unless overridden this is a combination of the previous methods) @@ -114,8 +120,11 @@ def get_whole_html(self, custom_headers: str = '', include_stylesheet: bool = Tr :param webenv: like in :meth:`~.ConvertedMarkup.get_javascript` above """ - stylesheet = ('\n' if include_stylesheet else '') + stylesheet = ( + '\n" + if include_stylesheet + else "" + ) context = { "body": self.get_document_body(), diff --git a/markups/common.py b/markups/common.py index 1bfc8cd..129d2ce 100644 --- a/markups/common.py +++ b/markups/common.py @@ -6,31 +6,32 @@ # Some common constants and functions (LANGUAGE_HOME_PAGE, MODULE_HOME_PAGE, SYNTAX_DOCUMENTATION) = range(3) -CONFIGURATION_DIR = (os.getenv('XDG_CONFIG_HOME') or os.getenv('APPDATA') or - os.path.expanduser('~/.config')) +CONFIGURATION_DIR = ( + os.getenv("XDG_CONFIG_HOME") + or os.getenv("APPDATA") + or os.path.expanduser("~/.config") +) MATHJAX2_LOCAL_URLS = ( - 'file:///usr/share/javascript/mathjax/MathJax.js', # Debian libjs-mathjax - 'file:///usr/share/mathjax2/MathJax.js', # Arch Linux mathjax2 + "file:///usr/share/javascript/mathjax/MathJax.js", # Debian libjs-mathjax + "file:///usr/share/mathjax2/MathJax.js", # Arch Linux mathjax2 ) MATHJAX3_LOCAL_URLS = ( - 'file:///usr/share/mathjax/tex-chtml.js', # Arch Linux mathjax + "file:///usr/share/mathjax/tex-chtml.js", # Arch Linux mathjax ) -MATHJAX_WEB_URL = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js' +MATHJAX_WEB_URL = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js" -PYGMENTS_STYLE = 'default' +PYGMENTS_STYLE = "default" -def get_pygments_stylesheet( - selector: str | None, style: str | None = None -) -> str: +def get_pygments_stylesheet(selector: str | None, style: str | None = None) -> str: if style is None: style = PYGMENTS_STYLE - if style == '': - return '' + if style == "": + return "" try: from pygments.formatters import HtmlFormatter except ImportError: - return '' + return "" else: defs = HtmlFormatter(style=style).get_style_defs(selector) assert isinstance(defs, str) diff --git a/markups/markdown.py b/markups/markdown.py index 3da3590..c948ef7 100644 --- a/markups/markdown.py +++ b/markups/markdown.py @@ -16,12 +16,12 @@ try: import yaml + HAVE_YAML = True except ImportError: HAVE_YAML = False -MATHJAX2_CONFIG = \ - ''' -''' +""" # Taken from: # https://docs.mathjax.org/en/latest/upgrading/v2.html?highlight=upgrading#changes-in-the-mathjax-api -MATHJAX3_CONFIG = \ - ''' +MATHJAX3_CONFIG = """ -''' # noqa: E501 +""" # noqa: E501 -extensions_re = re.compile(r'required.extensions: (.+)', flags=re.IGNORECASE) -extension_name_re = re.compile( - r'[a-z0-9_.]+(?:\([^)]+\))?', flags=re.IGNORECASE) +extensions_re = re.compile(r"required.extensions: (.+)", flags=re.IGNORECASE) +extension_name_re = re.compile(r"[a-z0-9_.]+(?:\([^)]+\))?", flags=re.IGNORECASE) _canonicalized_ext_names: dict[str, str] = {} @@ -75,32 +73,33 @@ class MarkdownMarkup(AbstractMarkup): :param extensions: list of extension names :type extensions: list """ - name = 'Markdown' + + name = "Markdown" attributes = { - common.LANGUAGE_HOME_PAGE: 'https://daringfireball.net/projects/markdown/', - common.MODULE_HOME_PAGE: 'https://github.com/Python-Markdown/markdown', - common.SYNTAX_DOCUMENTATION: - 'https://daringfireball.net/projects/markdown/syntax' + common.LANGUAGE_HOME_PAGE: "https://daringfireball.net/projects/markdown/", + common.MODULE_HOME_PAGE: "https://github.com/Python-Markdown/markdown", + common.SYNTAX_DOCUMENTATION: "https://daringfireball.net/projects/markdown/syntax", } - file_extensions = ('.md', '.mkd', '.mkdn', '.mdwn', '.mdown', '.markdown') - default_extension = '.mkd' + file_extensions = (".md", ".mkd", ".mkdn", ".mdwn", ".mdown", ".markdown") + default_extension = ".mkd" @staticmethod def available() -> bool: try: import markdown - importlib.import_module('mdx_math') + + importlib.import_module("mdx_math") except ImportError: return False - return getattr(markdown, '__version_info__', (2,)) >= (3,) + return getattr(markdown, "__version_info__", (2,)) >= (3,) def _load_extensions_list_from_txt_file( self, filename: str ) -> Iterator[_name_and_config]: with open(filename) as extensions_file: for line in extensions_file: - if not line.startswith('#'): + if not line.startswith("#"): yield self._split_extension_config(line.rstrip()) def _load_extensions_list_from_yaml_file( @@ -110,8 +109,7 @@ def _load_extensions_list_from_yaml_file( try: data = yaml.safe_load(extensions_file) except yaml.YAMLError as ex: - warnings.warn( - f'Failed parsing {filename}: {ex}', SyntaxWarning) + warnings.warn(f"Failed parsing {filename}: {ex}", SyntaxWarning) raise OSError from ex if isinstance(data, list): for item in data: @@ -123,18 +121,18 @@ def _load_extensions_list_from_yaml_file( def _get_global_extensions( self, filename: str | None ) -> Iterator[_name_and_config]: - local_directory = os.path.dirname(filename) if filename else '' + local_directory = os.path.dirname(filename) if filename else "" choices = [ - os.path.join(local_directory, 'markdown-extensions.yaml'), - os.path.join(local_directory, 'markdown-extensions.txt'), - os.path.join(common.CONFIGURATION_DIR, 'markdown-extensions.yaml'), - os.path.join(common.CONFIGURATION_DIR, 'markdown-extensions.txt'), + os.path.join(local_directory, "markdown-extensions.yaml"), + os.path.join(local_directory, "markdown-extensions.txt"), + os.path.join(common.CONFIGURATION_DIR, "markdown-extensions.yaml"), + os.path.join(common.CONFIGURATION_DIR, "markdown-extensions.txt"), ] for choice in choices: - if choice.endswith('.yaml') and not HAVE_YAML: + if choice.endswith(".yaml") and not HAVE_YAML: continue try: - if choice.endswith('.txt'): + if choice.endswith(".txt"): yield from self._load_extensions_list_from_txt_file(choice) else: yield from self._load_extensions_list_from_yaml_file(choice) @@ -151,11 +149,11 @@ def _get_document_extensions(self, text: str) -> Iterator[_name_and_config]: yield from self._split_extensions_configs(extensions) def _canonicalize_extension_name(self, extension_name: str) -> str | None: - prefixes = ('markdown.extensions.', '', 'mdx_') + prefixes = ("markdown.extensions.", "", "mdx_") for prefix in prefixes: try: module = importlib.import_module(prefix + extension_name) - if not hasattr(module, 'makeExtension'): + if not hasattr(module, "makeExtension"): continue except (ImportError, ValueError, TypeError): pass @@ -165,10 +163,10 @@ def _canonicalize_extension_name(self, extension_name: str) -> str | None: def _split_extension_config(self, extension_name: str) -> _name_and_config: """Splits the configuration options from the extension name.""" - lb = extension_name.find('(') + lb = extension_name.find("(") if lb == -1: return extension_name, {} - extension_name, parameters = extension_name[:lb], extension_name[lb + 1:-1] + extension_name, parameters = extension_name[:lb], extension_name[lb + 1 : -1] pairs = [x.split("=") for x in parameters.split(",")] return extension_name, {x.strip(): y.strip() for (x, y) in pairs} @@ -186,8 +184,7 @@ def _apply_extensions( self, document_extensions: Iterable[_name_and_config] | None = None ) -> None: extensions = self.global_extensions.copy() - extensions.extend( - self._split_extensions_configs(self.requested_extensions)) + extensions.extend(self._split_extensions_configs(self.requested_extensions)) if document_extensions is not None: extensions.extend(document_extensions) @@ -195,10 +192,10 @@ def _apply_extensions( extension_configs = {} for name, config in extensions: - if name == 'mathjax': + if name == "mathjax": mathjax_config = {"enable_dollar_delimiter": True} extension_configs["mdx_math"] = mathjax_config - elif name == 'remove_extra': + elif name == "remove_extra": if "markdown.extensions.extra" in extension_names: extension_names.remove("markdown.extensions.extra") if "mdx_math" in extension_names: @@ -209,16 +206,19 @@ def _apply_extensions( else: candidate = self._canonicalize_extension_name(name) if candidate is None: - warnings.warn(f'Extension "{name}" does not exist.', - ImportWarning) + warnings.warn( + f'Extension "{name}" does not exist.', ImportWarning + ) continue canonical_name = candidate _canonicalized_ext_names[name] = canonical_name extension_names.add(canonical_name) extension_configs[canonical_name] = config - self.md = self.markdown.Markdown(extensions=list(extension_names), - extension_configs=extension_configs, - output_format='html5') + self.md = self.markdown.Markdown( + extensions=list(extension_names), + extension_configs=extension_configs, + output_format="html5", + ) self.extensions = extension_names self.extension_configs = extension_configs @@ -227,53 +227,49 @@ def __init__( ): AbstractMarkup.__init__(self, filename) import markdown + self.markdown = markdown self.requested_extensions = extensions or [] self.global_extensions: list[_name_and_config] = [] if extensions is None: - self.global_extensions.extend( - self._get_global_extensions(filename)) + self.global_extensions.extend(self._get_global_extensions(filename)) self._apply_extensions() def convert(self, text: str) -> ConvertedMarkdown: - # Determine body self.md.reset() self._apply_extensions(self._get_document_extensions(text)) - body = self.md.convert(text) + '\n' + body = self.md.convert(text) + "\n" # Determine title - if hasattr(self.md, 'Meta') and 'title' in self.md.Meta: - title = str.join(' ', self.md.Meta['title']) + if hasattr(self.md, "Meta") and "title" in self.md.Meta: + title = str.join(" ", self.md.Meta["title"]) else: - title = '' + title = "" # Determine stylesheet css_class = None - if 'markdown.extensions.codehilite' in self.extensions: - config = self.extension_configs.get( - 'markdown.extensions.codehilite', {}) - css_class = config.get('css_class', 'codehilite') - stylesheet = common.get_pygments_stylesheet(f'.{css_class}') - elif 'pymdownx.highlight' in self.extensions: - config = self.extension_configs.get('pymdownx.highlight', {}) - css_class = config.get('css_class', 'highlight') - stylesheet = common.get_pygments_stylesheet(f'.{css_class}') + if "markdown.extensions.codehilite" in self.extensions: + config = self.extension_configs.get("markdown.extensions.codehilite", {}) + css_class = config.get("css_class", "codehilite") + stylesheet = common.get_pygments_stylesheet(f".{css_class}") + elif "pymdownx.highlight" in self.extensions: + config = self.extension_configs.get("pymdownx.highlight", {}) + css_class = config.get("css_class", "highlight") + stylesheet = common.get_pygments_stylesheet(f".{css_class}") else: - stylesheet = '' + stylesheet = "" return ConvertedMarkdown(body, title, stylesheet) class ConvertedMarkdown(ConvertedMarkup): - def get_javascript(self, webenv: bool = False) -> str: if '\n' return script_tag % (mathjax_url, async_attr) diff --git a/markups/textile.py b/markups/textile.py index 0871401..7d42597 100644 --- a/markups/textile.py +++ b/markups/textile.py @@ -12,21 +12,21 @@ class TextileMarkup(AbstractMarkup): """Markup class for Textile language. Inherits :class:`~markups.abstract.AbstractMarkup`. """ - name = 'Textile' + + name = "Textile" attributes = { - common.LANGUAGE_HOME_PAGE: 'https://textile-lang.com', - common.MODULE_HOME_PAGE: 'https://github.com/textile/python-textile', - common.SYNTAX_DOCUMENTATION: - 'https://movabletype.org/documentation/author/textile-2-syntax.html' + common.LANGUAGE_HOME_PAGE: "https://textile-lang.com", + common.MODULE_HOME_PAGE: "https://github.com/textile/python-textile", + common.SYNTAX_DOCUMENTATION: "https://movabletype.org/documentation/author/textile-2-syntax.html", } - file_extensions = ('.textile',) - default_extension = '.textile' + file_extensions = (".textile",) + default_extension = ".textile" @staticmethod def available() -> bool: try: - importlib.import_module('textile') + importlib.import_module("textile") except ImportError: return False return True @@ -34,6 +34,7 @@ def available() -> bool: def __init__(self, filename: str | None = None): AbstractMarkup.__init__(self, filename) from textile import textile + self.textile = textile def convert(self, text: str) -> ConvertedMarkup: diff --git a/tests/test_markdown.py b/tests/test_markdown.py index 0cb903d..47be742 100644 --- a/tests/test_markdown.py +++ b/tests/test_markdown.py @@ -21,14 +21,12 @@ except ImportError: HAVE_YAML = False -tables_source = \ - '''th1 | th2 +tables_source = """th1 | th2 --- | --- t11 | t21 -t12 | t22''' +t12 | t22""" -tables_output = \ - '''
th1 | @@ -46,31 +44,27 @@
---|
+mathjax_output = r"""
some text $escaped$
@@ -104,37 +97,33 @@ text
$$escaped$$ \[escaped]
-''' # noqa: E501 +""" # noqa: E501 -mathjax_multiline_source = \ - r''' +mathjax_multiline_source = r""" $$ \TeX \LaTeX $$ -''' +""" -mathjax_multiline_output = \ - r'''+mathjax_multiline_output = r"""
-''' +""" -mathjax_multilevel_source = \ - r''' +mathjax_multilevel_source = r""" \begin{equation*} \begin{pmatrix} 1 & 0\\ 0 & 1 \end{pmatrix} \end{equation*} -''' +""" -mathjax_multilevel_output = \ - r'''
+mathjax_multilevel_output = r"""
-''' +""" -@unittest.skipUnless(MarkdownMarkup.available(), 'Markdown not available') +@unittest.skipUnless(MarkdownMarkup.available(), "Markdown not available") class MarkdownTest(unittest.TestCase): maxDiff = None @@ -154,74 +143,85 @@ def setUp(self) -> None: def test_empty_file(self) -> None: markup = MarkdownMarkup() - self.assertEqual(markup.convert('').get_document_body(), '\n') + self.assertEqual(markup.convert("").get_document_body(), "\n") def test_extensions_loading(self) -> None: markup = MarkdownMarkup() - self.assertIsNone(markup._canonicalize_extension_name('nonexistent')) - self.assertIsNone(markup._canonicalize_extension_name( - 'nonexistent(someoption)')) - self.assertIsNone(markup._canonicalize_extension_name('.foobar')) - self.assertEqual(markup._canonicalize_extension_name( - 'meta'), 'markdown.extensions.meta') - name, parameters = markup._split_extension_config( - 'toc(anchorlink=1, foo=bar)') - self.assertEqual(name, 'toc') - self.assertEqual(parameters, {'anchorlink': '1', 'foo': 'bar'}) + self.assertIsNone(markup._canonicalize_extension_name("nonexistent")) + self.assertIsNone( + markup._canonicalize_extension_name("nonexistent(someoption)") + ) + self.assertIsNone(markup._canonicalize_extension_name(".foobar")) + self.assertEqual( + markup._canonicalize_extension_name("meta"), "markdown.extensions.meta" + ) + name, parameters = markup._split_extension_config("toc(anchorlink=1, foo=bar)") + self.assertEqual(name, "toc") + self.assertEqual(parameters, {"anchorlink": "1", "foo": "bar"}) def test_loading_extensions_by_module_name(self) -> None: - markup = MarkdownMarkup(extensions=['markdown.extensions.footnotes']) - source = ('Footnotes[^1] have a label and the content.\n\n' - '[^1]: This is a footnote content.') + markup = MarkdownMarkup(extensions=["markdown.extensions.footnotes"]) + source = ( + "Footnotes[^1] have a label and the content.\n\n" + "[^1]: This is a footnote content." + ) html = markup.convert(source).get_document_body() - self.assertIn(' None: markup = MarkdownMarkup( - extensions=['remove_extra', 'toc', 'markdown.extensions.toc']) + extensions=["remove_extra", "toc", "markdown.extensions.toc"] + ) self.assertEqual(len(markup.extensions), 1) - self.assertIn('markdown.extensions.toc', markup.extensions) + self.assertIn("markdown.extensions.toc", markup.extensions) def test_extensions_parameters(self) -> None: - markup = MarkdownMarkup(extensions=['toc(anchorlink=1)']) - html = markup.convert('## Header').get_document_body() - self.assertEqual(html, - '
[TOC]
', html) + self.assertNotIn("[TOC]
", html) html = markup.convert(content).get_document_body() - self.assertIn('[TOC]
', html) + self.assertIn("[TOC]
", html) html = markup.convert(toc_header + content).get_document_body() - self.assertNotIn('[TOC]
', html) + self.assertNotIn("[TOC]
", html) def test_extra(self) -> None: markup = MarkdownMarkup() @@ -231,52 +231,48 @@ def test_extra(self) -> None: self.assertEqual(deflists_output, html) def test_remove_extra(self) -> None: - markup = MarkdownMarkup(extensions=['remove_extra']) + markup = MarkdownMarkup(extensions=["remove_extra"]) html = markup.convert(tables_source).get_document_body() - self.assertNotIn('