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.
- slides_xp-0.0.1/PKG-INFO +65 -0
- slides_xp-0.0.1/README.md +46 -0
- slides_xp-0.0.1/pyproject.toml +37 -0
- slides_xp-0.0.1/slides_xp/__init__.py +4 -0
- slides_xp-0.0.1/slides_xp/__main__.py +5 -0
- slides_xp-0.0.1/slides_xp/cli.py +70 -0
- slides_xp-0.0.1/slides_xp/css/root.css +48 -0
- slides_xp-0.0.1/slides_xp/javascript/navigator.js +36 -0
- slides_xp-0.0.1/slides_xp/markdown.py +34 -0
- slides_xp-0.0.1/slides_xp/picker.py +36 -0
- slides_xp-0.0.1/slides_xp/server.py +124 -0
- slides_xp-0.0.1/slides_xp/slide.py +37 -0
- slides_xp-0.0.1/slides_xp/themes/default/main.css +20 -0
- slides_xp-0.0.1/slides_xp/themes/default/picker.css +22 -0
- slides_xp-0.0.1/slides_xp/themes/default/slide.css +43 -0
- slides_xp-0.0.1/slides_xp/themes/xp/bliss.jpg +0 -0
- slides_xp-0.0.1/slides_xp/themes/xp/main.css +19 -0
- slides_xp-0.0.1/slides_xp/themes/xp/slide.css +31 -0
- slides_xp-0.0.1/slides_xp/themes/xp/window-bottom-left.png +0 -0
- slides_xp-0.0.1/slides_xp/themes/xp/window-bottom-right.png +0 -0
- slides_xp-0.0.1/slides_xp/themes/xp/window-bottom.png +0 -0
- slides_xp-0.0.1/slides_xp/themes/xp/window-center.png +0 -0
- slides_xp-0.0.1/slides_xp/themes/xp/window-left.png +0 -0
- slides_xp-0.0.1/slides_xp/themes/xp/window-right.png +0 -0
- slides_xp-0.0.1/slides_xp/themes/xp/window-top-left.png +0 -0
- slides_xp-0.0.1/slides_xp/themes/xp/window-top-right.png +0 -0
- slides_xp-0.0.1/slides_xp/themes/xp/window-top.png +0 -0
- slides_xp-0.0.1/slides_xp/themes/xp/window.png +0 -0
- slides_xp-0.0.1/slides_xp/util.py +29 -0
slides_xp-0.0.1/PKG-INFO
ADDED
|
@@ -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,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
|
+
}
|
|
Binary file
|
|
@@ -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
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
)
|