diff --git a/docs/source/_ext/lexer.py b/docs/source/_ext/lexer.py index 9421ff3..4ade9f2 100644 --- a/docs/source/_ext/lexer.py +++ b/docs/source/_ext/lexer.py @@ -33,13 +33,14 @@ from collections.abc import Iterator from typing import ClassVar -from pygments.lexer import bygroups, include -from pygments.lexers.python import PythonLexer +from pygments.lexer import bygroups, combined, include, words +from pygments.lexers.python import CythonLexer, PythonLexer, RegexLexer from pygments.token import ( Comment, Keyword, Name, Number, + Operator, Punctuation, String, Text, @@ -63,8 +64,65 @@ def inner(a, b) -> bool: return inner -root: list = [ - (r"\n", Whitespace), +class MixinLexer(RegexLexer): + """Regex Mixin Lexer class. + + Notes: + 1. Supports primitive rainbow bracket coloring. + 2. Supports primitive constant declaration (uppercase variables) + + """ + + n_brackets: int + _stack: deque[int] + + def __init__(self, **options) -> None: + self.n_brackets = int(options.pop("n_brackets", 4)) + super().__init__(**options) + self._stack = deque[int]() + + def _enter(self) -> _TokenType: + """Retrieve next token in cycle.""" + idx = len(self._stack) % self.n_brackets + self._stack.append(idx) + + return get_bracket_level(idx) + + def _exit(self) -> _TokenType: + """Remove element from stack and return token.""" + try: + idx: int = self._stack.pop() + return get_bracket_level(idx) + + # NOTE: Only additional ending brackets trigger this (e.g. `{{ }}}` ). + # NOTE: We are not attempting to detect correct matching brackets (e.g. `(]` ) + except IndexError: + return Punctuation.Error + + def get_tokens_unprocessed( + self, + text, + stack=("root",), + ) -> Iterator[tuple[int, _TokenType, str]]: + _token: _TokenType + for idx, token, value in super().get_tokens_unprocessed(text, stack): + _token = token + if token is Name and value.isupper(): + _token = Name.Constant + + elif token is Punctuation: + match value: + case "(" | "[" | "{" | "<": + _token = self._enter() + case "}" | "]" | ")" | ">": + _token = self._exit() + case _: + ... + + yield idx, _token, value + + +docstrings: list = [ ( # single line docstrings (edge case) r'^(\s*)([rRuUbB]{,2})("""(?:.)*?""")', bygroups(Whitespace, String.Affix, String.Doc), @@ -72,20 +130,29 @@ def inner(a, b) -> bool: ( # Modfied triple double quote docstrings to highlight docstring titles r'^(\s*)([rRuUbB]{,2})(""")', bygroups(Whitespace, String.Affix, String.Doc), - "docstring-double", + "docstring-double-quotes", ), ( # Intentionally treat text encapsulated within single triple quotes as String r"^(\s*)([rRuUbB]{,2})('''(?:.|\n)*?''')", bygroups(Whitespace, String.Affix, String), ), +] + +comments: list = [ (r"\A#!.+$", Comment.Hashbang), + # Format Special Common Keywords in Comments ( - # Format Special Common Keyword Comments - # NOTE: Must come before Comment.Single token in order to be matched. - r"(#\s*)(TODO|FIXME|NOTE|BUG|HACK|XXX)(:?)(.*$)", - bygroups(Comment.Single, Comment.Special, Comment.Special, Comment.Single), + r"(#\s*)(TODO|FIXME|NOTE|BUG|HACK|XXX)(.*$)", + bygroups(Comment.Single, Comment.Special, Comment.Single), ), (r"#.*$", Comment.Single), +] + + +python_root: list = [ + (r"\n", Whitespace), + *docstrings, + *comments, (r"\\\n", Text), (r"\\", Text), include("keywords"), @@ -115,8 +182,8 @@ def inner(a, b) -> bool: python_tokens: dict[str, list] = PythonLexer.tokens.copy() -python_tokens["root"] = root -python_tokens["docstring-double"] = [ +python_tokens["root"] = python_root +python_tokens["docstring-double-quotes"] = [ ( r"(?<=\n)(\s*)(Args|Attributes|Returns|Raises|" r"Examples|Yields|References|Notes|Equations)(:)(\s*)", @@ -135,6 +202,7 @@ def inner(a, b) -> bool: (r"\b([a-zA-Z_]\w*)(?=\s*\()", Name.Function), ) +# Tokenize segment of number literals declared in different base (non base 10) python_tokens["numbers"] = [ ( r"(\d(?:_?\d)*\.(?:\d(?:_?\d)*)?|(?:\d(?:_?\d)*)?\.\d(?:_?\d)*)" @@ -149,61 +217,264 @@ def inner(a, b) -> bool: ] -class CustomPythonLexer(PythonLexer): - """Enhanced regex-based python Lexer. +class CustomPythonLexer(MixinLexer, PythonLexer): + """Custom enhanced regex-based python lexer. Notes: 1. Implemented a simple stack based rainbow bracket colorizer. - * limitation: Only detects errors that close more brackets than opens. + * limitation: Only detects errors that close more brackets than it opens. + * limitation: No attempt is made to confirm matching closing brackets. 2. Highlight Docstring titles (assumes google docstring format) 3. Improved highlighting function calls (with limitations) 4. Modify display of number components which indicate a different base number. """ - n_brackets: int - _stack: deque[int] tokens: ClassVar[dict[str, list]] = python_tokens - def __init__(self, **options) -> None: - super().__init__(**options) - self._stack = deque[int]() - self.n_brackets = int(options.get("n_brackets", 4)) - - def _enter(self) -> _TokenType: - """Retrieve next token in cycle.""" - idx = len(self._stack) % self.n_brackets - self._stack.append(idx) - - return get_bracket_level(idx) - def _exit(self) -> _TokenType: - """Remove element from stack and return token.""" - try: - idx: int = self._stack.pop() - return get_bracket_level(idx) +cython_root = [ + (r"\n", Whitespace), + *docstrings, + (r"[^\S\n]+", Text), + *comments, + (r"[]{}:(),;[]", Punctuation), + (r"\\\n", Whitespace), + (r"\\", Text), + (r"(in|is|and|or|not)\b", Operator.Word), + (r"(<)([a-zA-Z0-9.?]+)(>)", bygroups(Punctuation, Keyword.Type, Punctuation)), + (r"!=|==|<<|>>|[-~+/*%=<>&^|.?]", Operator), + ( + r"(from)(\d+)(<=)(\s+)(<)(\d+)(:)", + bygroups( + Keyword, Number.Integer, Operator, Whitespace, Operator, Name, Punctuation + ), + ), + include("keywords"), + (r"(def)(\s+)", bygroups(Keyword.Declare, Whitespace), "funcname"), + (r"(property)(\s+)", bygroups(Keyword.Type, Whitespace), "funcname"), + (r"(cp?def)(\s+)", bygroups(Keyword.Declare, Whitespace), "cdef"), + (r"(ctypedef)(\s+)", bygroups(Keyword.Declare, Whitespace), "ctypedef"), + (r"(cdef)(:)", bygroups(Keyword.Declare, Punctuation)), + ( + r"(class|cppclass|struct)(\s+)", + bygroups(Keyword.Declare, Whitespace), + "classname", + ), + (r"(from)(\s+)", bygroups(Keyword.Namespace, Whitespace), "fromimport"), + (r"(c?import)(\s+)", bygroups(Keyword.Namespace, Whitespace), "import"), + include("builtins"), + include("backtick"), + ('(?:[rR]|[uU][rR]|[rR][uU])"""', String, "tdqs"), + ("(?:[rR]|[uU][rR]|[rR][uU])'''", String, "tsqs"), + ('(?:[rR]|[uU][rR]|[rR][uU])"', String, "dqs"), + ("(?:[rR]|[uU][rR]|[rR][uU])'", String, "sqs"), + ('[uU]?"""', String, combined("stringescape", "tdqs")), + ("[uU]?'''", String, combined("stringescape", "tsqs")), + ('[uU]?"', String, combined("stringescape", "dqs")), + ("[uU]?'", String, combined("stringescape", "sqs")), + include("name"), + include("numbers"), +] - except IndexError: - return Punctuation.Error +cython_tokens: dict[str, list] = CythonLexer.tokens.copy() +cython_tokens["root"] = cython_root +cython_tokens["numbers"] = python_tokens["numbers"] +cython_tokens["docstring-double-quotes"] = python_tokens["docstring-double-quotes"] +cython_tokens["name"].insert( + _find(cython_tokens["name"], Name, _get_index(1)), + (r"\b([a-zA-Z_]\w*)(?=\s*\()", Name.Function), +) +cython_tokens["cdef"] = [ + # include packed keyword + (r"(public|readonly|extern|api|inline|packed|fused)\b", Keyword), + # Specialize Name.Class vs Name.Function vs Name.Variable tokens + ( + # include cppclass keyword + r"(struct|enum|union|class|cppclass)\b(\s+)([a-zA-Z_]\w*)", + bygroups(Keyword.Declare, Whitespace, Name.Class), + "#pop", + ), + (r"([a-zA-Z_]\w*)(\s*)(?=\()", bygroups(Name.Function, Whitespace), "#pop"), + (r"([a-zA-Z_]\w*)(\s*)(?=[:,=#\n]|$)", bygroups(Name.Variable, Whitespace), "#pop"), + (r"([a-zA-Z_]\w*)(\s*)(,)", bygroups(Name.Variable, Whitespace, Punctuation)), + (r"from\b", Keyword, "#pop"), + (r"as\b", Keyword), + (r":", Punctuation, "#pop"), + (r'(?=["\'])', Text, "#pop"), + (r"[a-zA-Z_]\w*", Keyword.Type), + (r".", Text), +] +# Define new ctypedef context +cython_tokens["ctypedef"] = [ + (r"(public|readonly|extern|api|inline|packed|fused)\b", Keyword), + ( + r"(\s*)([a-zA-Z_]\w*)(\s*)(:)", + bygroups(Whitespace, Name.Class, Whitespace, Punctuation), + "#pop", + ), + ( + r"(struct|enum|union|class|cppclass)(\s+)([a-zA-Z_]\w*)", + bygroups(Keyword.Declare, Whitespace, Name.Class), + "#pop", + ), + ( + r"([a-zA-Z_]\w*)(\s+)([a-zA-Z_]\w*)", + bygroups(Keyword.Type, Whitespace, Name.Class), + "#pop", + ), + (r"([a-zA-Z_]\w*)", Name.Class, "#pop"), +] +# Define Keyword.Constant token +cython_tokens["keywords"].append( + (words(("True", "False", "None", "NULL"), suffix=r"\b"), Keyword.Constant) +) +cython_tokens["keywords"][_find(cython_tokens["keywords"], Keyword, _get_index(1))] = ( + words( + ( + "assert", + "async", + "await", + "break", + "by", + "continue", + # "ctypedef", + "del", + "elif", + "else", + "except", + "except?", + "exec", + "finally", + "for", + # "fused", + "gil", + "global", + "if", + "include", + "lambda", + "namespace", # added + "new", # added - relevant for c++ syntax + "noexcept", # added + "nogil", + "pass", + "print", + "raise", + "return", + "try", + "while", + "yield", + "as", + "with", + ), + suffix=r"\b", + ), + Keyword, +) +# Redefine Name.Builtin.Pseudo token (to not include Keyword.Constant values) +cython_tokens["builtins"][ + _find(cython_tokens["builtins"], Name.Builtin.Pseudo, _get_index(1)) +] = (r"(? Iterator[tuple[int, _TokenType, str]]: - _token: _TokenType - for idx, token, value in super().get_tokens_unprocessed(text, stack): - _token = token - if token is Name and value.isupper(): - _token = Name.Constant - elif token is Punctuation: - match value: - case "(" | "[" | "{": - _token = self._enter() - case "}" | "]" | ")": - _token = self._exit() - case _: - ... +class CustomCythonLexer(MixinLexer, CythonLexer): + """Custom enhanced regex-based cython lexer.""" - yield idx, _token, value + tokens: ClassVar[dict[str, list]] = cython_tokens diff --git a/docs/source/_ext/styles.py b/docs/source/_ext/styles.py index 1b66028..1b9c7f0 100644 --- a/docs/source/_ext/styles.py +++ b/docs/source/_ext/styles.py @@ -97,12 +97,12 @@ class VSCodeDarkPlus(Style): Keyword.Type: Colors.datatype, Keyword.Declare: bold(Colors.declare), Keyword.Constant: bold(Colors.declare), - Keyword.Reserved: bold(Colors.reserved), + Keyword.Reserved: Colors.declare, Keyword.Namespace: Colors.control, # Variable Names Name: Colors.variable, Name.Type: Colors.builtin, - Name.Class: bold(Colors.datatype), + Name.Class: Colors.datatype, Name.Builtin: Colors.builtin, Name.Builtin.Pseudo: italic(Colors.variable), Name.Constant: "#4FC1FF", diff --git a/docs/source/_templates/sidebar/brand.html b/docs/source/_templates/sidebar/brand.html new file mode 100644 index 0000000..939fa3b --- /dev/null +++ b/docs/source/_templates/sidebar/brand.html @@ -0,0 +1,18 @@ + + {% block brand_content %} + {%- if logo_url %} + + {%- endif %} + {%- if theme_light_logo and theme_dark_logo %} + + {%- endif %} + {% if not theme_sidebar_hide_name %} + + {%- endif %} + {% endblock brand_content %} + diff --git a/docs/source/_templates/sidebar/versions.html b/docs/source/_templates/sidebar/versions.html new file mode 100644 index 0000000..690749c --- /dev/null +++ b/docs/source/_templates/sidebar/versions.html @@ -0,0 +1,17 @@ + +{% if versions %} +
+{% endif %} diff --git a/docs/source/conf.py b/docs/source/conf.py index 0c788d7..96c9613 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -33,7 +33,7 @@ import sys from sphinx.application import Sphinx -from sphinx.highlighting import lexer_classes +from sphinx.highlighting import lexer_classes as _lexer_registry sys.path.insert(0, os.path.abspath("../src/")) @@ -46,36 +46,88 @@ # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration - extensions = [ "sphinx.ext.duration", "sphinx.ext.viewcode", "sphinx.ext.autodoc", "sphinx.ext.autosummary", "sphinx.ext.napoleon", + "sphinx_multiversion", ] + +napoleon_google_docstring = True # Use google docstring format (sphinx.ext.napoleon) autosummary_generate = True # Turn on sphinx.ext.autosummary -napoleon_google_docstring = True +smv_branch_whitelist = "^(main|dev)$" +smv_tag_whitelist = ( + r"^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)" # standard semvar version + r"(?:-(" + r"(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)" + r"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*" + r"))?" + r"(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" +) templates_path = ["_templates"] exclude_patterns = [] +# Use custom syntax highlighting (style) +pygments_style = "styles.VSCodeDarkPlus" +pygments_dark_style = "styles.VSCodeDarkPlus" # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output - -html_theme = "sphinx_rtd_theme" +# https://pradyunsg.me/furo/customisation/ +html_theme = "furo" html_static_path = ["_static"] html_css_files = ["custom.css"] -pygments_style = "styles.VSCodeDarkPlus" +# html_logo = "_static/logo.svg" +github_url = "https://github.com/Spill-Tea/DesignerDNA" + +# Theme options +html_theme_options = { + # "light_logo": "_static/logo.svg", + # "dark_logo": "_static/logo-dark.svg", + "footer_icons": [ + { + "name": "GitHub", + "url": github_url, + "html": """ + + """, + "class": "fa-brands fa-solid fa-github fa-2x", + "target": "_blank", + }, + ], +} + +html_sidebars = { + "**": [ + "sidebar/brand.html", # overwritten + "sidebar/search.html", + "sidebar/scroll-start.html", + "sidebar/navigation.html", + "sidebar/versions.html", # added + "sidebar/scroll-end.html", + ], +} +html_additional_pages = {"page": "page.html"} def setup(app: Sphinx) -> None: """Custom sphinx application startup setup.""" - from lexer import CustomPythonLexer + from lexer import CustomCythonLexer, CustomPythonLexer # type: ignore + # NOTE: overwrite default python and cython lexers app.add_lexer("python", CustomPythonLexer) - assert "python" in lexer_classes, "python language not found in registry" - assert lexer_classes["python"] == CustomPythonLexer, ( - "custom Lexer not found in registry." + assert "python" in _lexer_registry, "python language not found in registry" + assert _lexer_registry["python"] == CustomPythonLexer, ( + "custom Python Lexer not found in registry." + ) + + app.add_lexer("cython", CustomCythonLexer) + assert "cython" in _lexer_registry, "cython language not found in registry" + assert _lexer_registry["cython"] == CustomCythonLexer, ( + "custom Cython Lexer not found in registry." ) diff --git a/pyproject.toml b/pyproject.toml index 3e82dcb..aed0315 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ exclude = ["benchmarks", "build", "docs", "tests", "scripts"] [project.optional-dependencies] dev = ["designer_dna[doc,test,lint,type,commit]", "tox"] commit = ["pre-commit"] -doc = ["sphinx", "sphinx-rtd-theme"] +doc = ["sphinx", "furo", "sphinx_multiversion"] test = ["pytest", "coverage", "pytest-xdist"] lint = ["pylint", "ruff"] type = ["mypy"]