slides-xp 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. slides_xp-0.0.1/PKG-INFO +65 -0
  2. slides_xp-0.0.1/README.md +46 -0
  3. slides_xp-0.0.1/pyproject.toml +37 -0
  4. slides_xp-0.0.1/slides_xp/__init__.py +4 -0
  5. slides_xp-0.0.1/slides_xp/__main__.py +5 -0
  6. slides_xp-0.0.1/slides_xp/cli.py +70 -0
  7. slides_xp-0.0.1/slides_xp/css/root.css +48 -0
  8. slides_xp-0.0.1/slides_xp/javascript/navigator.js +36 -0
  9. slides_xp-0.0.1/slides_xp/markdown.py +34 -0
  10. slides_xp-0.0.1/slides_xp/picker.py +36 -0
  11. slides_xp-0.0.1/slides_xp/server.py +124 -0
  12. slides_xp-0.0.1/slides_xp/slide.py +37 -0
  13. slides_xp-0.0.1/slides_xp/themes/default/main.css +20 -0
  14. slides_xp-0.0.1/slides_xp/themes/default/picker.css +22 -0
  15. slides_xp-0.0.1/slides_xp/themes/default/slide.css +43 -0
  16. slides_xp-0.0.1/slides_xp/themes/xp/bliss.jpg +0 -0
  17. slides_xp-0.0.1/slides_xp/themes/xp/main.css +19 -0
  18. slides_xp-0.0.1/slides_xp/themes/xp/slide.css +31 -0
  19. slides_xp-0.0.1/slides_xp/themes/xp/window-bottom-left.png +0 -0
  20. slides_xp-0.0.1/slides_xp/themes/xp/window-bottom-right.png +0 -0
  21. slides_xp-0.0.1/slides_xp/themes/xp/window-bottom.png +0 -0
  22. slides_xp-0.0.1/slides_xp/themes/xp/window-center.png +0 -0
  23. slides_xp-0.0.1/slides_xp/themes/xp/window-left.png +0 -0
  24. slides_xp-0.0.1/slides_xp/themes/xp/window-right.png +0 -0
  25. slides_xp-0.0.1/slides_xp/themes/xp/window-top-left.png +0 -0
  26. slides_xp-0.0.1/slides_xp/themes/xp/window-top-right.png +0 -0
  27. slides_xp-0.0.1/slides_xp/themes/xp/window-top.png +0 -0
  28. slides_xp-0.0.1/slides_xp/themes/xp/window.png +0 -0
  29. slides_xp-0.0.1/slides_xp/util.py +29 -0
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.3
2
+ Name: slides-xp
3
+ Version: 0.0.1
4
+ Summary:
5
+ Author: Maddy Guthridge
6
+ Author-email: hello@maddyguthridge.com
7
+ Requires-Python: >=3.11
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Requires-Dist: click (>=8.1.8,<9.0.0)
13
+ Requires-Dist: flask (>=3.1.0,<4.0.0)
14
+ Requires-Dist: mistune (>=3.1.1,<4.0.0)
15
+ Requires-Dist: pygments (>=2.19.1,<3.0.0)
16
+ Requires-Dist: pyhtml-enhanced (>=2.2.0,<3.0.0)
17
+ Description-Content-Type: text/markdown
18
+
19
+ # Slides XP
20
+
21
+ A simple but flexible markdown slide-show viewer.
22
+
23
+ ## Running
24
+
25
+ ```sh
26
+ sxp <directories to serve>
27
+ ```
28
+
29
+ ## Theming
30
+
31
+ You can use the `--theme` option to specify the a built-in theme, or a path to
32
+ a directory containing CSS theme files.
33
+
34
+ The built-in themes are:
35
+
36
+ * `default`
37
+ * `xp`
38
+
39
+ ### Custom CSS
40
+
41
+ A theme directory should contain (at least) these files:
42
+
43
+ * `main.css`: main stylesheet. Always loaded.
44
+ * `slide.css`: stylesheet for slide pages.
45
+ * `picker.css`: stylesheet for slide picker page.
46
+
47
+ These stylesheets are mounted at the `/theme` endpoint.
48
+
49
+ Within these stylesheets, the following classes can be selected.
50
+
51
+ * `.highlight`: code blocks
52
+ * `.slide-content`: slide content
53
+ * `.picker-box`: slide picker
54
+ * `.picker-item`: slide within slide picker
55
+
56
+ And the following variables are available:
57
+
58
+ * `--hl-comment`: code block highlighting, comment
59
+ * `--hl-doc`: code block highlighting, documentation
60
+ * `--hl-keyword`: code block highlighting, keyword
61
+ * `--hl-var`: code block highlighting, variable
62
+ * `--hl-func`: code block highlighting, function
63
+ * `--hl-type`: code block highlighting, type
64
+ * `--hl-string`: code block highlighting, string
65
+
@@ -0,0 +1,46 @@
1
+ # Slides XP
2
+
3
+ A simple but flexible markdown slide-show viewer.
4
+
5
+ ## Running
6
+
7
+ ```sh
8
+ sxp <directories to serve>
9
+ ```
10
+
11
+ ## Theming
12
+
13
+ You can use the `--theme` option to specify the a built-in theme, or a path to
14
+ a directory containing CSS theme files.
15
+
16
+ The built-in themes are:
17
+
18
+ * `default`
19
+ * `xp`
20
+
21
+ ### Custom CSS
22
+
23
+ A theme directory should contain (at least) these files:
24
+
25
+ * `main.css`: main stylesheet. Always loaded.
26
+ * `slide.css`: stylesheet for slide pages.
27
+ * `picker.css`: stylesheet for slide picker page.
28
+
29
+ These stylesheets are mounted at the `/theme` endpoint.
30
+
31
+ Within these stylesheets, the following classes can be selected.
32
+
33
+ * `.highlight`: code blocks
34
+ * `.slide-content`: slide content
35
+ * `.picker-box`: slide picker
36
+ * `.picker-item`: slide within slide picker
37
+
38
+ And the following variables are available:
39
+
40
+ * `--hl-comment`: code block highlighting, comment
41
+ * `--hl-doc`: code block highlighting, documentation
42
+ * `--hl-keyword`: code block highlighting, keyword
43
+ * `--hl-var`: code block highlighting, variable
44
+ * `--hl-func`: code block highlighting, function
45
+ * `--hl-type`: code block highlighting, type
46
+ * `--hl-string`: code block highlighting, string
@@ -0,0 +1,37 @@
1
+ [project]
2
+ name = "slides-xp"
3
+ version = "0.0.1"
4
+ description = ""
5
+ authors = [{ name = "Maddy Guthridge", email = "hello@maddyguthridge.com" }]
6
+ readme = "README.md"
7
+ requires-python = ">=3.11"
8
+ dependencies = [
9
+ "flask (>=3.1.0,<4.0.0)",
10
+ "pyhtml-enhanced (>=2.2.0,<3.0.0)",
11
+ "click (>=8.1.8,<9.0.0)",
12
+ "mistune (>=3.1.1,<4.0.0)",
13
+ "pygments (>=2.19.1,<3.0.0)",
14
+ ]
15
+
16
+ [project.scripts]
17
+ sxp = "slides_xp.__main__:cli"
18
+
19
+ [tool.poetry]
20
+ include = ["py.typed", "css/*", "javascript/*", "themes/*"]
21
+
22
+ [tool.poetry.group.dev.dependencies]
23
+ ruff = "^0.9.6"
24
+ mypy = "^1.15.0"
25
+ pytest = "^8.3.4"
26
+ types-pygments = "^2.19.0.20250107"
27
+
28
+ [tool.ruff]
29
+ line-length = 79
30
+
31
+ [tool.mypy]
32
+ check_untyped_defs = true
33
+ files = ["slides_xp", "tests"]
34
+
35
+ [build-system]
36
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
37
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,4 @@
1
+ from .server import make_app
2
+
3
+
4
+ __all__ = ["make_app"]
@@ -0,0 +1,5 @@
1
+ from slides_xp.cli import cli
2
+
3
+
4
+ if __name__ == '__main__':
5
+ cli()
@@ -0,0 +1,70 @@
1
+ """
2
+ CLI
3
+
4
+ Main entrypoint to the program.
5
+ """
6
+
7
+ import click
8
+ from pathlib import Path
9
+ from slides_xp import make_app
10
+
11
+
12
+ HELP_TEXT = """
13
+ sxp
14
+
15
+ Slides XP CLI
16
+ """
17
+
18
+
19
+ @click.command("sxp", help=HELP_TEXT, options_metavar="<options>")
20
+ @click.argument(
21
+ "paths",
22
+ type=click.Path(
23
+ exists=True,
24
+ dir_okay=True,
25
+ file_okay=False,
26
+ readable=True,
27
+ path_type=Path,
28
+ ),
29
+ nargs=-1,
30
+ required=True,
31
+ )
32
+ @click.option(
33
+ "--theme",
34
+ envvar="SXP_THEME",
35
+ help="Theme directory to serve CSS stylesheets from",
36
+ )
37
+ @click.option(
38
+ "-h",
39
+ "--host",
40
+ default="localhost",
41
+ envvar="HOST",
42
+ help="Port to run server on",
43
+ )
44
+ @click.option(
45
+ "-p",
46
+ "--port",
47
+ type=int,
48
+ default=3000,
49
+ envvar="PORT",
50
+ help="Port to run server on",
51
+ )
52
+ @click.option(
53
+ "--debug",
54
+ is_flag=True,
55
+ help="Whether to enable Flask debug mode",
56
+ )
57
+ def cli(
58
+ paths: tuple[Path, ...],
59
+ *,
60
+ theme: str | None,
61
+ host: str,
62
+ port: int,
63
+ debug: bool,
64
+ ):
65
+ """
66
+ Entrypoint to CLI.
67
+ """
68
+ app = make_app(list(paths), theme)
69
+
70
+ app.run(host, port, debug)
@@ -0,0 +1,48 @@
1
+ .highlight {
2
+
3
+ /* Comment */
4
+ & .c1,
5
+ & .cm {
6
+ color: var(--hl-comment, black);
7
+ }
8
+
9
+ /* Documentation */
10
+ & .sd {
11
+ color: var(--hl-doc, black);
12
+ }
13
+
14
+ /* Keyword */
15
+ & .k,
16
+ & .kd {
17
+ color: var(--hl-keyword, black);
18
+ }
19
+
20
+ /* Variable */
21
+ & .n {
22
+ color: var(--hl-var, black);
23
+ }
24
+
25
+ /* Function name */
26
+ & .nf {
27
+ color: var(--hl-func, black);
28
+ }
29
+
30
+ /* Class name */
31
+ & .nb,
32
+ & .kt {
33
+ color: var(--hl-type, black);
34
+ }
35
+
36
+ /* String */
37
+ & .s2,
38
+ & .sb {
39
+ color: var(--hl-string, black);
40
+ }
41
+
42
+ }
43
+
44
+ /* Prevent images from exceeding the bounds of the slide */
45
+ .slide-content img {
46
+ max-width: 100%;
47
+ max-height: 50vh;
48
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Navigator.js
3
+ *
4
+ * Navigate between slides
5
+ */
6
+
7
+
8
+ window.addEventListener("keydown", e => {
9
+ const root = `${window.location.protocol}//${window.location.host}/`;
10
+
11
+ let target = null;
12
+ switch (e.key) {
13
+ case "0":
14
+ if (window.sxp.prev) {
15
+ target = `${root}${window.sxp.first}`;
16
+ }
17
+ break;
18
+ case "ArrowLeft":
19
+ if (window.sxp.prev) {
20
+ target = `${root}${window.sxp.prev}`;
21
+ }
22
+ break;
23
+ case "ArrowRight":
24
+ if (window.sxp.next) {
25
+ target = `${root}${window.sxp.next}`;
26
+ }
27
+ break;
28
+ case "Escape":
29
+ target = root;
30
+ break;
31
+ }
32
+ if (target !== null) {
33
+ window.location.href = target;
34
+ e.preventDefault();
35
+ }
36
+ });
@@ -0,0 +1,34 @@
1
+ """
2
+ Render markdown as HTML
3
+ """
4
+
5
+ from typing import cast
6
+ import mistune
7
+ from pygments import highlight
8
+ from pygments.lexers import get_lexer_by_name
9
+ from pygments.formatters import html
10
+
11
+
12
+ # https://mistune.lepture.com/en/latest/guide.html#customize-renderer
13
+ class HighlightRenderer(mistune.HTMLRenderer):
14
+ """
15
+ Markdown render that adds code block highlights
16
+ """
17
+
18
+ def block_code(self, code, info=None):
19
+ if info:
20
+ lexer = get_lexer_by_name(info, stripall=True)
21
+ formatter = html.HtmlFormatter()
22
+ return highlight(code, lexer, formatter)
23
+ return "<pre><code>" + mistune.escape(code) + "</code></pre>"
24
+
25
+
26
+ markdown = mistune.create_markdown(renderer=HighlightRenderer())
27
+
28
+
29
+ def render_markdown(text: str) -> str:
30
+ """
31
+ Render markdown as HTML.
32
+ """
33
+ # We know it's a string, since we haven't set the renderer to None
34
+ return cast(str, markdown(text))
@@ -0,0 +1,36 @@
1
+ from dataclasses import dataclass
2
+ import pyhtml as p
3
+
4
+
5
+ @dataclass
6
+ class Choice:
7
+ name: str
8
+ url: str
9
+
10
+
11
+ def picker(root: str, choices: list[Choice], parent: str | None = None):
12
+ choices_html = [
13
+ p.div(class_="picker-item")(
14
+ p.a(href=f"{choice.url}")(choice.name),
15
+ )
16
+ for choice in choices
17
+ ]
18
+
19
+ if parent is not None:
20
+ choices_html.insert(
21
+ 0, p.div(class_="picker-item")(p.a(href=parent)(".."))
22
+ )
23
+
24
+ return p.html(
25
+ p.head(
26
+ p.title("Slides XP"),
27
+ p.script(src="/javascript/navigator.js", defer=True),
28
+ p.link(href="/css/root.css", rel="stylesheet"),
29
+ p.link(href="/theme/main.css", rel="stylesheet"),
30
+ p.link(href="/theme/picker.css", rel="stylesheet"),
31
+ ),
32
+ p.body(
33
+ p.h1(root),
34
+ p.div(class_="picker-box")(choices_html),
35
+ ),
36
+ )
@@ -0,0 +1,124 @@
1
+ from flask import Flask, Blueprint, redirect, send_file, send_from_directory
2
+ from pathlib import Path
3
+ import pyhtml as p
4
+
5
+ from slides_xp.picker import Choice, picker
6
+ from slides_xp.slide import slide
7
+ from slides_xp.util import dir_contains_md, list_subdirs, slides_list
8
+
9
+
10
+ lib_dir = Path(__file__).parent
11
+
12
+
13
+ def error(code: int, message: str):
14
+ return str(
15
+ p.html(
16
+ p.body(
17
+ p.h1(f"Error {code}"),
18
+ p.p(message),
19
+ )
20
+ )
21
+ ), code
22
+
23
+
24
+ def make_blueprint(name: str, root_dir: Path):
25
+ bp = Blueprint(name, __name__, url_prefix=f"/{name}")
26
+
27
+ @bp.get("/", defaults={"path": ""})
28
+ @bp.get("/<path:path>")
29
+ def endpoint(path: str):
30
+ file_path = root_dir / path
31
+ url = f"/{name}/{path}".removesuffix("/")
32
+ # Root path must be a parent of `full_path` to prevent escaping the
33
+ # specified directories
34
+ if root_dir not in [file_path, *file_path.parents]:
35
+ return error(403, "Illegal path")
36
+ # 404 if file/dir does not exist
37
+ if not file_path.exists():
38
+ return error(404, "File not found")
39
+ # If file, send it
40
+ if file_path.is_file():
41
+ # Render markdown files as HTML
42
+ if file_path.suffix == ".md":
43
+ return str(slide(file_path))
44
+ else:
45
+ return send_file(file_path.absolute())
46
+ elif dir_contains_md(file_path):
47
+ # Dir with markdown, render a list of slides
48
+ return str(
49
+ picker(
50
+ str(root_dir),
51
+ [
52
+ Choice(p.name, f"{url}/{p.name}")
53
+ for p in slides_list(file_path)
54
+ ],
55
+ parent=str(file_path.parent),
56
+ )
57
+ )
58
+ else:
59
+ # Otherwise, dir with no markdown, so render a list of subdirs
60
+ return str(
61
+ picker(
62
+ str(root_dir),
63
+ [
64
+ Choice(p.name, f"{url}/{p.name}")
65
+ for p in list_subdirs(file_path)
66
+ ],
67
+ parent=str(file_path.parent),
68
+ )
69
+ )
70
+
71
+ return bp
72
+
73
+
74
+ def make_app(paths: list[Path], theme: str | None = None):
75
+ if theme is None:
76
+ theme_dir = lib_dir / "themes" / "default"
77
+ elif Path(theme).is_dir():
78
+ theme_dir = Path(theme)
79
+ elif (lib_dir / "themes" / theme).is_dir():
80
+ theme_dir = lib_dir / "themes" / theme
81
+ else:
82
+ # Invalid theme
83
+ theme_dir = lib_dir / "themes" / "default"
84
+
85
+ app = Flask(__name__)
86
+
87
+ for path in paths:
88
+ app.register_blueprint(make_blueprint(path.name, path))
89
+
90
+ @app.get("/")
91
+ def root():
92
+ """
93
+ Root path.
94
+
95
+ If using one path, redirect to it. Otherwise give a choice.
96
+ """
97
+ if len(paths) == 1:
98
+ return redirect(f"/{paths[0].name}/")
99
+ else:
100
+ return str(
101
+ picker(
102
+ "Slides XP",
103
+ [Choice(p.name, f"/{p.name}") for p in paths],
104
+ )
105
+ )
106
+
107
+ @app.get("/javascript/<path>")
108
+ def scripts(path):
109
+ return send_from_directory(lib_dir / "javascript", path)
110
+
111
+ @app.get("/css/<path>")
112
+ def css(path):
113
+ return send_from_directory(lib_dir / "css", path)
114
+
115
+ @app.get("/theme/<path>")
116
+ def theme_css(path):
117
+ return send_from_directory(theme_dir, path)
118
+
119
+ return app
120
+
121
+
122
+ if __name__ == "__main__":
123
+ app = make_app([Path("temp")])
124
+ app.run(port=3000)
@@ -0,0 +1,37 @@
1
+ import pyhtml as p
2
+ from pathlib import Path
3
+
4
+ from slides_xp.markdown import render_markdown
5
+ from slides_xp.util import slides_list
6
+
7
+
8
+ def slide(file: Path) -> p.html:
9
+ slides = list(slides_list(file.parent))
10
+
11
+ curr_index = slides.index(file)
12
+
13
+ first = f"'{slides[0]}'" if len(slides) else "null"
14
+ prev = f"'{slides[curr_index - 1]}'" if curr_index > 0 else "null"
15
+ next = f"'{slides[curr_index + 1]}'" if curr_index < len(slides) - 1 else "null"
16
+
17
+ return p.html(
18
+ p.head(
19
+ p.title("Slides XP"),
20
+ p.script(src="/javascript/navigator.js", defer=True),
21
+ p.link(href="/css/root.css", rel="stylesheet"),
22
+ p.link(href="/theme/main.css", rel="stylesheet"),
23
+ p.link(href="/theme/slide.css", rel="stylesheet"),
24
+ ),
25
+ p.body(
26
+ p.script(f"""
27
+ window.sxp = {{
28
+ first: {first},
29
+ prev: {prev},
30
+ next: {next},
31
+ }};
32
+ """),
33
+ p.div(class_="slide-content")(
34
+ p.DangerousRawHtml(render_markdown(file.read_text())),
35
+ ),
36
+ ),
37
+ )
@@ -0,0 +1,20 @@
1
+ body {
2
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
3
+ display: flex;
4
+ flex-direction: column;
5
+ justify-content: center;
6
+ align-items: center;
7
+ font-size: 2rem;
8
+ }
9
+
10
+ h1 {
11
+ font-size: 5rem;
12
+ text-align: center;
13
+ }
14
+
15
+ @media (prefers-color-scheme: dark) {
16
+ html {
17
+ background-color: rgb(27, 27, 27);
18
+ color: white;
19
+ }
20
+ }
@@ -0,0 +1,22 @@
1
+ .picker-box {
2
+ min-width: 70%;
3
+ }
4
+
5
+ .picker-item a {
6
+ text-decoration: none;
7
+ }
8
+ .picker-item a:hover {
9
+ text-decoration: underline;
10
+ }
11
+
12
+ @media (prefers-color-scheme: light) {
13
+ .picker-item a {
14
+ color: black;
15
+ }
16
+ }
17
+
18
+ @media (prefers-color-scheme: dark) {
19
+ .picker-item a {
20
+ color: white;
21
+ }
22
+ }
@@ -0,0 +1,43 @@
1
+ .slide-content {
2
+ min-width: 70%;
3
+ }
4
+
5
+ .highlight {
6
+ font-family: monospace;
7
+ font-weight: bold;
8
+ border: 2px solid rgb(138, 138, 138);
9
+ border-radius: 10px;
10
+ padding: 20px;
11
+ }
12
+
13
+ .highlight>pre {
14
+ margin: 0px;
15
+ }
16
+
17
+ @media (prefers-color-scheme: light) {
18
+
19
+ /* Syntax highlighting, derived from VS Code "Quiet Light" theme */
20
+ :root {
21
+ --hl-comment: rgb(116, 116, 116);
22
+ --hl-doc: rgb(87, 151, 61);
23
+ --hl-keyword: rgb(122, 62, 157);
24
+ --hl-var: rgb(122, 62, 157);
25
+ --hl-func: rgb(170, 55, 49);
26
+ --hl-type: rgb(122, 62, 157);
27
+ --hl-string: rgb(87, 151, 61);
28
+ }
29
+ }
30
+
31
+ @media (prefers-color-scheme: dark) {
32
+
33
+ /* Syntax highlighting, derived from VS Code "Synthwave '84" theme */
34
+ :root {
35
+ --hl-comment: rgb(128, 135, 184);
36
+ --hl-doc: rgb(128, 135, 184);
37
+ --hl-keyword: rgb(254, 222, 93);
38
+ --hl-var: rgb(237, 119, 205);
39
+ --hl-func: rgb(3, 237, 249);
40
+ --hl-type: rgb(214, 62, 75);
41
+ --hl-string: rgb(255, 139, 57);
42
+ }
43
+ }
@@ -0,0 +1,19 @@
1
+ html {
2
+ background-image: url('/theme/bliss.jpg');
3
+ background-size: cover;
4
+ min-height: 100vh;
5
+ }
6
+
7
+ body {
8
+ font-family: Tahoma, Verdana, Segoe, sans-serif;
9
+ font-size: 2rem;
10
+ }
11
+
12
+ h1 {
13
+ font-size: 5rem;
14
+ text-align: center;
15
+ }
16
+
17
+ .highlight {
18
+ font-family: Consolas, Hack, monospace;
19
+ }
@@ -0,0 +1,31 @@
1
+ .slide-content {
2
+ background:
3
+ /* Corners */
4
+ url('/theme/window-top-left.png') top left no-repeat,
5
+ url('/theme/window-top-right.png') top right no-repeat,
6
+ url('/theme/window-bottom-left.png') bottom left no-repeat,
7
+ url('/theme/window-bottom-right.png') bottom right no-repeat,
8
+ /* Edges */
9
+ url('/theme/window-top.png') top repeat-x,
10
+ url('/theme/window-bottom.png') bottom repeat-x,
11
+ url('/theme/window-left.png') left repeat-y,
12
+ url('/theme/window-right.png') right repeat-y,
13
+ /* Center */
14
+ url('/theme/window-center.png') center repeat;
15
+
16
+ clip-path: inset(7px 9px 7px 7px round 3px);
17
+
18
+ padding: 30px;
19
+
20
+ margin: 100px;
21
+ }
22
+
23
+ :root {
24
+ --hl-comment: rgb(125, 131, 136);
25
+ --hl-doc: rgb(125, 131, 136);
26
+ --hl-keyword: rgb(223, 58, 72);
27
+ --hl-var: rgb(35, 35, 35);
28
+ --hl-func: rgb(129, 98, 199);
29
+ --hl-type: rgb(129, 98, 199);
30
+ --hl-string: rgb(19, 42, 108);
31
+ }
@@ -0,0 +1,29 @@
1
+ from pathlib import Path
2
+ from typing import Generator
3
+
4
+
5
+ def list_subdirs(path: Path) -> list[Path]:
6
+ return [subdir for subdir in path.iterdir() if subdir.is_dir()]
7
+
8
+
9
+ def dir_contains_md(path: Path) -> bool:
10
+ """
11
+ Returns whether the given directory contains Markdown files.
12
+ """
13
+ return next(slides_list(path), None) is not None
14
+
15
+
16
+ def first_slide(path: Path) -> Path:
17
+ """
18
+ Returns the path of the first slide in the given dir
19
+ """
20
+ return next(slides_list(path))
21
+
22
+
23
+ def slides_list(path: Path) -> Generator[Path]:
24
+ """
25
+ Returns a slides list for the given directory.
26
+ """
27
+ return (
28
+ child for child in path.iterdir() if child.is_file() and child.suffix == ".md"
29
+ )