diff --git a/.tool-versions b/.tool-versions index 2ab8199..e6ea852 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -python 3.11.2 +python 3.11.3 diff --git a/README.md b/README.md index e69de29..e112276 100644 --- a/README.md +++ b/README.md @@ -0,0 +1 @@ +# sphinx-revealjs \ No newline at end of file diff --git a/example/conf.py b/example/conf.py index 6bedda2..ad3efb6 100644 --- a/example/conf.py +++ b/example/conf.py @@ -1,4 +1,5 @@ -extensions = ["sei.sphinxext.revealjs"] +extensions = ["sphinx_revealjs"] html_sidebars = {"**": []} exclude_patterns = ["_build"] html_theme = "revealjs" +revealjs_theme = "night.css" \ No newline at end of file diff --git a/example/index.rst b/example/index.rst index d523dbc..ce1d103 100644 --- a/example/index.rst +++ b/example/index.rst @@ -15,6 +15,30 @@ Another Slide Hi +More content + +.. newslide:: Override title + +Yet another slide + +.. incr:: item + + - One + + - Two + + - Three + +Another Section +=============== + +Wow this is another section + +Subsection title +---------------- + +Whee + ===== Index ===== diff --git a/justfile b/justfile new file mode 100644 index 0000000..91b384e --- /dev/null +++ b/justfile @@ -0,0 +1,13 @@ +build: (_sphinx "sphinx-build" "revealjs" "example" "example" join("example", "_build")) + +_sphinx cmd builder config source output *opts: + @echo "Using {{cmd}} to build {{source}}" + poetry run {{cmd}} \ + -b {{builder}} \ + -d {{output}}/doctrees \ + -n \ + -c {{config}} \ + {{opts}} \ + {{source}} {{join(output, builder)}} + @echo "Opening in browser..." + open {{join(output, builder, "index.html")}} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index a57c0a3..7e1a8c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,22 +1,20 @@ [tool.poetry] -name = "sei-sphinxext-revealjs" +name = "sphinx-revealjs" version = "0.1.0" description = "" authors = ["Ashley Trinh "] readme = "README.md" -packages = [{include = "sei", from = "src"}] +packages = [{ include = "sphinx_revealjs", from = "src" }] include = ["lib/reveal.js/dist", "lib/reveal.js/plugin"] [tool.poetry.dependencies] python = "^3.11" sphinx = "^6.1.3" - [tool.poetry.group.dev.dependencies] mypy = "^1.1.1" black = "^23.1.0" - [tool.poetry.group.test.dependencies] pytest = "^7.2.2" diff --git a/src/sei/sphinxext/revealjs/builder.py b/src/sei/sphinxext/revealjs/builder.py deleted file mode 100644 index 62402d6..0000000 --- a/src/sei/sphinxext/revealjs/builder.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import TYPE_CHECKING - -from sphinx.builders.html import StandaloneHTMLBuilder - -if TYPE_CHECKING: - from sphinx.application import Sphinx - - -class RevealjsBuilder(StandaloneHTMLBuilder): - name = "revealjs" - search = False - - -def setup(app: "Sphinx") -> None: - app.add_builder(RevealjsBuilder) diff --git a/src/sei/sphinxext/revealjs/__init__.py b/src/sphinx_revealjs/__init__.py similarity index 63% rename from src/sei/sphinxext/revealjs/__init__.py rename to src/sphinx_revealjs/__init__.py index c571ae8..c36cef6 100644 --- a/src/sei/sphinxext/revealjs/__init__.py +++ b/src/sphinx_revealjs/__init__.py @@ -1,36 +1,37 @@ -from typing import TYPE_CHECKING, Any, List +from typing import TYPE_CHECKING, Any import importlib.metadata from pathlib import Path -from glob import glob from sphinx.util.fileutil import copy_asset_file -from sei.sphinxext.revealjs import builder, overridenodes +from . import builder, overridenodes, directives if TYPE_CHECKING: from sphinx.application import Sphinx - from sphinx.config import Config -__name__ = "sei.sphinxext.revealjs" +__name__ = "sphinx_revealjs" __version__ = importlib.metadata.version(__name__) THEMES_DIRECTORY = (Path(__file__).parent / "themes").resolve() -LIB_DIRECTORY = (Path(__file__).parent / ".." / ".." / ".." / ".." / "lib").resolve() +LIB_DIRECTORY = (Path(__file__).parent / ".." / "lib").resolve() REVEALJS_DIST = LIB_DIRECTORY / "reveal.js" / "dist" -def exclude_unused_theme_files(theme_name: str) -> List[str]: - """Exclude theme files that don't match the configured theme.""" +def init_builder(app: "Sphinx") -> None: + if app.builder.name == "revealjs": + add_revealjs_static_files(app) + override_nodes(app) - return [ - str(p) - for p in glob("theme/*.css", root_dir=REVEALJS_DIST) - if Path(p).name != theme_name - ] +def add_revealjs_static_files(app: "Sphinx") -> None: + app.add_css_file("reset.css", priority=500) + app.add_css_file("reveal.css", priority=500) + app.add_js_file("reveal.js", priority=500) + app.add_js_file("reveal.js.map", priority=500) + app.add_css_file(app.config.revealjs_theme, priority=600) -def add_theme(app: "Sphinx") -> None: - if app.builder.name == "revealjs": - app.add_css_file(app.config.revealjs_theme, priority=600) + +def override_nodes(app: "Sphinx") -> None: + overridenodes.setup(app) def copy_revealjs_files(app: "Sphinx", exc) -> None: @@ -49,19 +50,16 @@ def copy_revealjs_files(app: "Sphinx", exc) -> None: def setup(app: "Sphinx") -> dict[str, Any]: - builder.setup(app) - overridenodes.setup(app) - - app.add_config_value("revealjs_theme", "white.css", "revealjs") - - app.connect("builder-inited", add_theme) + app.add_config_value("revealjs_theme", "white.css", "html") + app.add_html_theme("revealjs", str(THEMES_DIRECTORY / "revealjs")) + app.add_builder(builder.RevealjsBuilder) + app.connect("builder-inited", init_builder) app.connect("build-finished", copy_revealjs_files) - app.add_html_theme("revealjs", str(THEMES_DIRECTORY / "revealjs")) - app.add_css_file("reset.css", priority=500) - app.add_css_file("reveal.css", priority=500) - app.add_js_file("reveal.js", priority=500) - app.add_js_file("reveal.js.map", priority=500) + directives.incremental.setup(app) + directives.speakernote.setup(app) + directives.newslide.setup(app) + return { "version": __version__, "parallel_read_safe": True, diff --git a/src/sphinx_revealjs/builder.py b/src/sphinx_revealjs/builder.py new file mode 100644 index 0000000..5d1f428 --- /dev/null +++ b/src/sphinx_revealjs/builder.py @@ -0,0 +1,8 @@ +"""sphinx_revealjs.builder""" + +from sphinx.builders.html import StandaloneHTMLBuilder + + +class RevealjsBuilder(StandaloneHTMLBuilder): + name = "revealjs" + search = False diff --git a/src/sphinx_revealjs/directives/__init__.py b/src/sphinx_revealjs/directives/__init__.py new file mode 100644 index 0000000..e3ef384 --- /dev/null +++ b/src/sphinx_revealjs/directives/__init__.py @@ -0,0 +1,3 @@ +"""sphinx_revealjs.directives""" + +from . import incremental, speakernote, newslide \ No newline at end of file diff --git a/src/sphinx_revealjs/directives/_base_slide.py b/src/sphinx_revealjs/directives/_base_slide.py new file mode 100644 index 0000000..09bd5b1 --- /dev/null +++ b/src/sphinx_revealjs/directives/_base_slide.py @@ -0,0 +1,31 @@ +"""sphinxext.revealjs.directives._base_slide + +Common, slides-related stuff. +""" + +from docutils.nodes import Element +from docutils.parsers.rst import Directive, directives + + +class BaseSlide(Directive): + """Base for slide-related directives.""" + + option_spec = { + "class": directives.class_option, + "background": directives.unchanged, + # The choices below are all from Revealjs. + # See https://revealjs.com/transitions/ + "transition": lambda arg: directives.choice( + arg, ("none", "fade", "slide", "convex", "concave", "zoom") + ), + "transition-speed": lambda arg: directives.choice( + arg, ("default", "fast", "slow") + ), + } + + def attach_options(self, node: Element) -> None: + node["data-background"] = self.options.get("background") + node["data-transition"] = self.options.get("transition") + node["data-transition-speed"] = self.options.get("transition-speed") + node["data-state"] = self.options.get("state") + node["classes"] += self.options.get("class", []) diff --git a/src/sphinx_revealjs/directives/incremental.py b/src/sphinx_revealjs/directives/incremental.py new file mode 100644 index 0000000..4cc1257 --- /dev/null +++ b/src/sphinx_revealjs/directives/incremental.py @@ -0,0 +1,120 @@ +"""sphinxext.revealjs.directives.incremental + +The `.. incremental::` directive. This will add a transition to its children so +they appear one at a time using Reveal.js's `fragment` class. +""" + +from typing import TYPE_CHECKING, List + +from docutils import nodes +from docutils.parsers.rst import Directive, directives + +from sphinx.util import logging + +if TYPE_CHECKING: + from sphinx.application import Sphinx + +logger = logging.getLogger(__name__) + + +class Incremental(Directive): + """Incremental directive.""" + + required_arguments = 1 + has_content = True + option_spec = {"class": directives.class_option} + + _valid_arguments = ("one", "item", "nest") + + def run(self) -> List[nodes.Node]: + self.validate_args() + self.assert_has_content() + + text = "\n".join(self.content) + node = nodes.container(text) + self.state.nested_parse(self.content, self.content_offset, node) + + if self.arguments[0] == "one": + node["classes"] += self.options.get("class", []) + node["classes"].append("fragment") + return [node] + else: + # Now we're handling the 'item' and 'nest' cases, where we need to + # apply the 'fragment' class to nodes in node.children + + self.assert_is_incrementable(node.children[0]) + + # Since we're gonna discard the parent node, copy + # classes set by the user onto the first child node + node.children[0]["classes"] += self.options.get("class", []) + + if isinstance(node.children[0], nodes.definition_list): + self.contain_definition_list_items(node.children[0]) + + if self.arguments[0] == "item": + self.increment_list_items(node) + elif self.arguments[0] == "nest": + self.increment_nested_list_items(node) + + return node.children + + def validate_args(self) -> None: + """Warn user if argument is invalid.""" + + location = self.state_machine.get_source_and_line(self.lineno) + if self.arguments[0] not in self._valid_arguments: + logger.warning( + f"Invalid argument: '{self.arguments[0]}' must be one of {', '.join(self._valid_arguments)}", + location=location, + ) + + def assert_is_incrementable(self, node: nodes.Element) -> None: + """Warn user if we can't apply transitions to this node.""" + + location = self.state_machine.get_source_and_line(self.lineno) + if not isinstance(node, nodes.Sequential): + logger.warning( + "contents of directive 'incremental' must be a list or sequence", + location=location, + ) + + def increment_list_items(self, node: nodes.Sequential) -> None: + """Add class 'fragment' to Sequential node's children.""" + + for list_item in node.children[0].children: + try: + list_item["classes"] += ["fragment"] + except TypeError: + continue + + def increment_nested_list_items(self, node: nodes.Sequential) -> None: + """Add class 'fragment' to a Sequential node's descendants.""" + + def traverse_condition(node: nodes.Node) -> bool: + return ( + isinstance(node, nodes.list_item) + or isinstance(node, nodes.term) + or isinstance(node, nodes.definition) + ) + + for list_item in node.traverse(traverse_condition): + list_item["classes"] += ["fragment"] + + @staticmethod + def contain_definition_list_items(dl_node: nodes.definition_list) -> None: + """Group definitions and terms in containers.""" + + dl_children = [] + for def_list_item in dl_node.children: + container = nodes.container() + container.children.append(def_list_item) + dl_children.append(container) + + dl_node.children = dl_children + + +def setup(app: "Sphinx") -> None: + """Setup the extension.""" + + app.add_directive("incremental", Incremental) + app.add_directive("incr", Incremental) diff --git a/src/sphinx_revealjs/directives/newslide.py b/src/sphinx_revealjs/directives/newslide.py new file mode 100644 index 0000000..5791243 --- /dev/null +++ b/src/sphinx_revealjs/directives/newslide.py @@ -0,0 +1,71 @@ +"""sphinxext.revealjs.directives.newslide + +Use this to create a new slide. Content will resume after a slide break. + +Pass in an argument to give the new slide a title:: + + .. newslide:: Title + +With no arguments, the new slide will have the same title as its +parent slide. +""" + +from typing import TYPE_CHECKING, List +from docutils import nodes + +from ._base_slide import BaseSlide + +if TYPE_CHECKING: + from sphinx.application import Sphinx + + +class newslide(nodes.General, nodes.Element): + """Newslide node.""" + + +class Newslide(BaseSlide): + """Newslide directive.""" + + optional_arguments = 1 + final_argument_whitespace = True + + def run(self) -> List[nodes.Element]: + local_title = self.arguments[0] if self.arguments else "" + + slide_node = newslide("", localtitle=local_title) + self.attach_options(slide_node) + + return [slide_node] + + +def visit_newslide(self, node: newslide) -> None: + title = node["localtitle"] + if not title and node.parent: + title = node.parent.next_node(nodes.title).astext().strip() + + self.body.append("") + self.body.append( + self.starttag( + node, + "section", + **{att: val for att, val in node.attributes.items() if val is not None}, + ) + ) + self.body.append(f"{title}") + + +def depart_newslide(self, node: newslide) -> None: + pass + + +def ignore_newslide(self, node: newslide) -> None: + raise nodes.SkipNode + + +def setup(app: "Sphinx") -> None: + app.add_node( + newslide, + revealjs=(visit_newslide, depart_newslide), + html=(ignore_newslide, None), + ) + app.add_directive("newslide", Newslide) diff --git a/src/sphinx_revealjs/directives/speakernote.py b/src/sphinx_revealjs/directives/speakernote.py new file mode 100644 index 0000000..53c15ee --- /dev/null +++ b/src/sphinx_revealjs/directives/speakernote.py @@ -0,0 +1,50 @@ +"""sphinxext.revealjs.directives.speakernote + +The `.. speakernote::` directive. Add speaker notes to slide deck. +""" + +from typing import TYPE_CHECKING, List + +from docutils import nodes +from docutils.parsers.rst import Directive + +if TYPE_CHECKING: + from sphinx.application import Sphinx + + +class speakernote(nodes.General, nodes.Element): + pass + + +class Speakernote(Directive): + has_content = True + + def run(self) -> List[nodes.Node]: + self.assert_has_content() + node = speakernote("\n".join(self.content)) + node["classes"] += ["notes"] + self.add_name(node) + self.state.nested_parse(self.content, self.content_offset, node) + return [node] + + +def visit_speakernote(self, node: speakernote) -> None: + classes = " ".join(node["classes"]) + self.body.append(f'") + + +def ignore_speakernote(self, node: speakernote) -> None: + raise nodes.SkipNode + + +def setup(app: "Sphinx") -> None: + app.add_node( + speakernote, + html=(ignore_speakernote, None), # type: ignore + revealjs=(visit_speakernote, depart_speakernote), + ) + app.add_directive("speaker", Speakernote) diff --git a/src/sei/sphinxext/revealjs/overridenodes.py b/src/sphinx_revealjs/overridenodes.py similarity index 100% rename from src/sei/sphinxext/revealjs/overridenodes.py rename to src/sphinx_revealjs/overridenodes.py diff --git a/src/sei/sphinxext/revealjs/themes/revealjs/layout.html b/src/sphinx_revealjs/themes/revealjs/layout.html similarity index 64% rename from src/sei/sphinxext/revealjs/themes/revealjs/layout.html rename to src/sphinx_revealjs/themes/revealjs/layout.html index aa1ef51..677b267 100644 --- a/src/sei/sphinxext/revealjs/themes/revealjs/layout.html +++ b/src/sphinx_revealjs/themes/revealjs/layout.html @@ -19,14 +19,16 @@ {%- block sidebar2 %}{% endblock sidebar2 %} {%- block relbar2 %}{% endblock relbar2 %} {%- block footer %}{% endblock footer %} - + {%- block initRevealjs %} + + {%- endblock initRevealjs %} {%- endblock content %} \ No newline at end of file diff --git a/src/sei/sphinxext/revealjs/themes/revealjs/theme.conf b/src/sphinx_revealjs/themes/revealjs/theme.conf similarity index 100% rename from src/sei/sphinxext/revealjs/themes/revealjs/theme.conf rename to src/sphinx_revealjs/themes/revealjs/theme.conf