Skip to content

Commit 9c75cdf

Browse files
authored
Merge pull request #6 from Spill-Tea/docs
Improve Documentation
2 parents c105512 + 6223a9a commit 9c75cdf

File tree

10 files changed

+417
-62
lines changed

10 files changed

+417
-62
lines changed

docs/source/_ext/lexer.py

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
# BSD 3-Clause License
2+
#
3+
# Copyright (c) 2025, Spill-Tea
4+
#
5+
# Redistribution and use in source and binary forms, with or without
6+
# modification, are permitted provided that the following conditions are met:
7+
#
8+
# 1. Redistributions of source code must retain the above copyright notice, this
9+
# list of conditions and the following disclaimer.
10+
#
11+
# 2. Redistributions in binary form must reproduce the above copyright notice,
12+
# this list of conditions and the following disclaimer in the documentation
13+
# and/or other materials provided with the distribution.
14+
#
15+
# 3. Neither the name of the copyright holder nor the names of its
16+
# contributors may be used to endorse or promote products derived from
17+
# this software without specific prior written permission.
18+
#
19+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
30+
"""Customized python lexer."""
31+
32+
from collections import deque
33+
from collections.abc import Iterator
34+
from typing import ClassVar
35+
36+
from pygments.lexer import bygroups, include
37+
from pygments.lexers.python import PythonLexer
38+
from pygments.token import (
39+
Comment,
40+
Keyword,
41+
Name,
42+
Number,
43+
Punctuation,
44+
String,
45+
Text,
46+
Whitespace,
47+
_TokenType,
48+
)
49+
from utils import get_bracket_level
50+
51+
52+
def _find(it, obj, key=lambda a, b: a == b) -> int:
53+
for n, j in enumerate(it):
54+
if key(j, obj):
55+
return n
56+
raise IndexError("Unable to find object.")
57+
58+
59+
def _get_index(n: int):
60+
def inner(a, b) -> bool:
61+
return a[n] == b
62+
63+
return inner
64+
65+
66+
root: list = [
67+
(r"\n", Whitespace),
68+
( # single line docstrings (edge case)
69+
r'^(\s*)([rRuUbB]{,2})("""(?:.)*?""")',
70+
bygroups(Whitespace, String.Affix, String.Doc),
71+
),
72+
( # Modfied triple double quote docstrings to highlight docstring titles
73+
r'^(\s*)([rRuUbB]{,2})(""")',
74+
bygroups(Whitespace, String.Affix, String.Doc),
75+
"docstring-double",
76+
),
77+
( # Intentionally treat text encapsulated within single triple quotes as String
78+
r"^(\s*)([rRuUbB]{,2})('''(?:.|\n)*?''')",
79+
bygroups(Whitespace, String.Affix, String),
80+
),
81+
(r"\A#!.+$", Comment.Hashbang),
82+
(
83+
# Format Special Common Keyword Comments
84+
# NOTE: Must come before Comment.Single token in order to be matched.
85+
r"(#\s*)(TODO|FIXME|NOTE|BUG|HACK|XXX)(:?)(.*$)",
86+
bygroups(Comment.Single, Comment.Special, Comment.Special, Comment.Single),
87+
),
88+
(r"#.*$", Comment.Single),
89+
(r"\\\n", Text),
90+
(r"\\", Text),
91+
include("keywords"),
92+
include("soft-keywords"),
93+
(
94+
r"(def)((?:\s|\\\s)+)",
95+
bygroups(Keyword.Declare, Whitespace),
96+
"funcname",
97+
),
98+
(
99+
r"(class)((?:\s|\\\s)+)",
100+
bygroups(Keyword.Declare, Whitespace),
101+
"classname",
102+
),
103+
(
104+
r"(from)((?:\s|\\\s)+)",
105+
bygroups(Keyword.Namespace, Whitespace),
106+
"fromimport",
107+
),
108+
(
109+
r"(import)((?:\s|\\\s)+)",
110+
bygroups(Keyword.Namespace, Whitespace),
111+
"import",
112+
),
113+
include("expr"),
114+
]
115+
116+
117+
python_tokens: dict[str, list] = PythonLexer.tokens.copy()
118+
python_tokens["root"] = root
119+
python_tokens["docstring-double"] = [
120+
(
121+
r"(?<=\n)(\s*)(Args|Attributes|Returns|Raises|"
122+
r"Examples|Yields|References|Notes|Equations)(:)(\s*)",
123+
bygroups(Whitespace, String.Doc.Title, String.Doc, Whitespace),
124+
),
125+
(r'^\s*(?:""")', String.Doc, "#pop"),
126+
(r".+[\r\n]*", String.Doc),
127+
]
128+
129+
# Tokenize function names when used (i.e. function calls)
130+
# NOTE: Must be inserted before general `Name` token but after `Name.Builtins` token
131+
# NOTE: Implementation limitations -> we cannot distinguish between class and function
132+
# calls using regex based parsing alone (i.e without semantic analysis).
133+
python_tokens["name"].insert(
134+
_find(python_tokens["name"], Name, _get_index(1)),
135+
(r"\b([a-zA-Z_]\w*)(?=\s*\()", Name.Function),
136+
)
137+
138+
python_tokens["numbers"] = [
139+
(
140+
r"(\d(?:_?\d)*\.(?:\d(?:_?\d)*)?|(?:\d(?:_?\d)*)?\.\d(?:_?\d)*)"
141+
r"([eE][+-]?\d(?:_?\d)*)?([jJ]?)",
142+
bygroups(Number.Float, Number.Float, Number.Other),
143+
),
144+
(r"(\d(?:_?\d)*[eE][+-]?\d(?:_?\d)*)([jJ]?)", bygroups(Number.Float, Number.Other)),
145+
(r"(0[oO])((?:_?[0-7])+)", bygroups(Number.Other, Number.Oct)),
146+
(r"(0[bB])((?:_?[01])+)", bygroups(Number.Other, Number.Bin)),
147+
(r"(0[xX])((?:_?[a-fA-F0-9])+)", bygroups(Number.Other, Number.Hex)),
148+
(r"(\d(?:_?\d)*)([jJ]?)", bygroups(Number.Integer, Number.Other)),
149+
]
150+
151+
152+
class CustomPythonLexer(PythonLexer):
153+
"""Enhanced regex-based python Lexer.
154+
155+
Notes:
156+
1. Implemented a simple stack based rainbow bracket colorizer.
157+
* limitation: Only detects errors that close more brackets than opens.
158+
2. Highlight Docstring titles (assumes google docstring format)
159+
3. Improved highlighting function calls (with limitations)
160+
4. Modify display of number components which indicate a different base number.
161+
162+
"""
163+
164+
n_brackets: int
165+
_stack: deque[int]
166+
tokens: ClassVar[dict[str, list]] = python_tokens
167+
168+
def __init__(self, **options) -> None:
169+
super().__init__(**options)
170+
self._stack = deque[int]()
171+
self.n_brackets = int(options.get("n_brackets", 4))
172+
173+
def _enter(self) -> _TokenType:
174+
"""Retrieve next token in cycle."""
175+
idx = len(self._stack) % self.n_brackets
176+
self._stack.append(idx)
177+
178+
return get_bracket_level(idx)
179+
180+
def _exit(self) -> _TokenType:
181+
"""Remove element from stack and return token."""
182+
try:
183+
idx: int = self._stack.pop()
184+
return get_bracket_level(idx)
185+
186+
except IndexError:
187+
return Punctuation.Error
188+
189+
def get_tokens_unprocessed(
190+
self,
191+
text,
192+
stack=("root",),
193+
) -> Iterator[tuple[int, _TokenType, str]]:
194+
_token: _TokenType
195+
for idx, token, value in super().get_tokens_unprocessed(text, stack):
196+
_token = token
197+
if token is Name and value.isupper():
198+
_token = Name.Constant
199+
200+
elif token is Punctuation:
201+
match value:
202+
case "(" | "[" | "{":
203+
_token = self._enter()
204+
case "}" | "]" | ")":
205+
_token = self._exit()
206+
case _:
207+
...
208+
209+
yield idx, _token, value

docs/source/_ext/styles.py

Lines changed: 93 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
2828
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2929

30-
"""Custom Pygment styles."""
30+
"""Custom Pygment syntax highlighting style."""
3131

3232
from typing import ClassVar
3333

@@ -39,43 +39,107 @@
3939
Name,
4040
Number,
4141
Operator,
42+
Other,
4243
Punctuation,
4344
String,
45+
Text,
4446
_TokenType,
4547
)
48+
from utils import get_brackets
49+
50+
51+
def bold(color: str) -> str:
52+
"""Embolden color."""
53+
return f"bold {color}"
54+
55+
56+
def italic(color: str) -> str:
57+
"""Italicize color."""
58+
return f"italic {color}"
59+
60+
61+
def underline(color: str) -> str:
62+
"""Underline text with color."""
63+
return f"underline {color}"
64+
65+
66+
class Colors:
67+
"""Define colors used more than once."""
68+
69+
datatype: str = "#61C8B0"
70+
variable: str = "#9CDCFE"
71+
function: str = "#DCDCAA"
72+
reserved: str = "#639BD4"
73+
default: str = "#D4D4D4"
74+
control: str = "#C586C0"
75+
builtin: str = "#4EC9B0"
76+
declare: str = "#569CD6"
77+
problem: str = "#C3726A"
78+
comment: str = "#6A9955"
79+
bracket: str = "#F9C922"
4680

4781

4882
class VSCodeDarkPlus(Style):
49-
"""VSCode Dark+ Style."""
83+
"""Custom theme deeply inspired by VSCode Dark+ as a pygments style."""
5084

5185
background_color: str = "#1E1E1E"
5286

53-
styles: ClassVar[dict[_TokenType, str]] = {
54-
Number: "#B6CEA9",
55-
Operator: "#D4D4D4",
56-
Operator.Word: "#C586C0",
57-
Comment: "#6D9957",
58-
Comment.Preproc: "#639BD4",
59-
Keyword.Namespace: "#C287A0",
60-
# Keyword.Reserved: "#C287A0",
61-
Keyword.Reserved: "#639BD4",
62-
Keyword.Type: "#61C8B0",
63-
Keyword.Constant: "#4FC1FF",
64-
# Keyword: "#639BD4",
65-
Keyword: "#C586C0",
66-
Name: "#7FD0FD",
67-
Name.Class: "#61C8B0",
68-
Name.Namespace: "#61C8B0",
69-
Name.Function: "#DCDCAA",
70-
# Name.Builtin: "#DCDCAA",
71-
Name.Builtin: "#4EC9B0",
72-
Name.Type: "#4EC9B0",
73-
Name.Builtin.Pseudo: "#9CDCFE",
74-
Name.Variable: "#9CDCFE",
75-
Name.Variable.Class: "#61C8B0",
76-
Name.Variable.Magic: "#DCDCAA",
77-
Name.Exception: "#61C8B0",
78-
Error: "#61C8B0",
87+
styles: ClassVar[dict[_TokenType, str]] = { # pyright: ignore
88+
# Comments
89+
Comment: Colors.comment,
90+
Comment.Single: Colors.comment,
91+
Comment.Preproc: Colors.reserved,
92+
Comment.Special: bold(Colors.declare),
93+
Comment.Hashbang: italic("#7C7046"),
94+
Comment.Multiline: italic("#525252"),
95+
# Keywords
96+
Keyword: Colors.control,
97+
Keyword.Type: Colors.datatype,
98+
Keyword.Declare: bold(Colors.declare),
99+
Keyword.Constant: bold(Colors.declare),
100+
Keyword.Reserved: bold(Colors.reserved),
101+
Keyword.Namespace: Colors.control,
102+
# Variable Names
103+
Name: Colors.variable,
104+
Name.Type: Colors.builtin,
105+
Name.Class: bold(Colors.datatype),
106+
Name.Builtin: Colors.builtin,
107+
Name.Builtin.Pseudo: italic(Colors.variable),
108+
Name.Constant: "#4FC1FF",
109+
Name.Function: Colors.function,
110+
Name.Function.Magic: italic(Colors.function),
111+
Name.Variable: Colors.variable,
112+
Name.Variable.Class: Colors.datatype,
113+
Name.Variable.Magic: Colors.function,
114+
Name.Namespace: Colors.datatype,
115+
Name.Exception: Colors.problem,
116+
# (Doc)Strings
117+
Text: Colors.default,
79118
String: "#C9937A",
80-
Punctuation: "#F9C922",
119+
String.Doc: italic(Colors.comment),
120+
String.Doc.Title: bold("#80AE6B"),
121+
String.Affix: Colors.declare,
122+
String.Regex: "#D16969",
123+
String.Escape: "#D7BA7D",
124+
String.Interpol: Colors.declare,
125+
# Numbers
126+
Number: "#B6CEA9",
127+
Number.Other: Colors.declare,
128+
# Operators
129+
Operator: Colors.default,
130+
Operator.Word: Colors.control,
131+
# Punctuation
132+
Punctuation: Colors.default,
133+
**get_brackets(
134+
[
135+
Colors.bracket,
136+
"#EA2EEA",
137+
"#5DCD4C",
138+
"#3B9ADE",
139+
]
140+
),
141+
Punctuation.Error: underline("#F92222"),
142+
# Miscellaneous
143+
Error: underline(bold(Colors.problem)),
144+
Other: Colors.default,
81145
}

0 commit comments

Comments
 (0)