Skip to content

Commit 7e9b247

Browse files
Merge pull request #309 from adamtheturtle/embed
Add support for sphinx-iframes
2 parents 2ad4a19 + d9d1516 commit 7e9b247

File tree

6 files changed

+232
-26
lines changed

6 files changed

+232
-26
lines changed

README.rst

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,29 @@ For video support, also add the `sphinxcontrib-video <https://sphinxcontrib-vide
4242
"""Configuration for Sphinx."""
4343
4444
extensions = [
45-
"sphinxcontrib.video", # Must be before sphinx_notion
45+
# ``sphinxcontrib.video`` must be before ``sphinx_notion``
46+
"sphinxcontrib.video",
4647
"sphinx_notion",
4748
]
4849
50+
If you are using ``sphinxcontrib.video`` with ``sphinx-iframes``, the warning ``app.add_directive`` will be raised.
51+
This is because ``sphinxcontrib.video`` and ``sphinx-iframes`` both implement a ``video`` directive.
52+
To suppress this warning, add the following to your ``conf.py``:
53+
54+
.. code-block:: python
55+
56+
"""Configuration for Sphinx."""
57+
58+
suppress_warnings = ["app.add_directive"]
59+
4960
For strikethrough text support, also add the `sphinxnotes-strike <https://github.com/sphinx-toolbox/sphinxnotes-strike>`_ extension:
5061

5162
.. code-block:: python
5263
5364
"""Configuration for Sphinx."""
5465
5566
extensions = [
56-
"sphinxnotes.strike", # Must be before sphinx_notion
67+
"sphinxnotes.strike", # Must be before ``sphinx_notion``
5768
"sphinx_notion",
5869
]
5970
@@ -115,12 +126,13 @@ The following syntax is supported:
115126
- Table of contents
116127
- Block quotes
117128
- All standard admonitions (note, warning, tip, attention, caution, danger, error, hint, important)
118-
- Collapsible sections (using sphinx-toolbox collapse directive)
119-
- Rest-example blocks (using sphinx-toolbox rest-example directive)
129+
- Collapsible sections (using the ``collapse`` directive from ``sphinx-toolbox``)
130+
- Rest-example blocks (using the ``rest-example`` directive from ``sphinx-toolbox``)
120131
- Images (with URLs or local paths)
121132
- Videos (with URLs or local paths)
122133
- Audio (with URLs or local paths)
123134
- PDFs (with URLs or local paths)
135+
- Embed blocks (using the ``iframe`` directive from ``sphinx-iframes``)
124136
- Tables
125137
- Strikethrough text
126138
- Colored text and text styles (bold, italic, monospace)
@@ -156,6 +168,43 @@ Both remote URLs and local file paths are supported.
156168
157169
The PDF will be rendered as an embedded PDF viewer in the generated Notion page.
158170

171+
Using Embed Blocks
172+
------------------
173+
174+
Embed blocks can be created using the `sphinx-iframes <https://pypi.org/project/sphinx-iframes/>`_ extension. First, install the extension:
175+
176+
.. code-block:: console
177+
178+
$ pip install sphinx-iframes
179+
180+
Then add it to your ``conf.py``:
181+
182+
.. code-block:: python
183+
184+
"""Configuration for Sphinx."""
185+
186+
extensions = [
187+
"sphinx_iframes", # Must be before ``sphinx_notion``
188+
"sphinx_notion",
189+
]
190+
191+
You can then use the ``iframe`` directive:
192+
193+
.. code-block:: rst
194+
195+
.. iframe:: https://www.youtube.com/embed/dQw4w9WgXcQ
196+
197+
The iframes will be rendered as embed blocks in the generated Notion page, allowing you to embed external content like videos, interactive demos, or other web content.
198+
However, if you are using ``sphinx-iframes`` with ``sphinxcontrib.video``, the warning ``app.add_directive`` will be raised.
199+
This is because ``sphinx-iframes`` and ``sphinxcontrib.video`` both implement a ``video`` directive.
200+
To suppress this warning, add the following to your ``conf.py``:
201+
202+
.. code-block:: python
203+
204+
"""Configuration for Sphinx."""
205+
206+
suppress_warnings = ["app.add_directive"]
207+
159208
Using Text Styles
160209
-----------------
161210

@@ -282,7 +331,6 @@ Unsupported Notion Block Types
282331
- Child page
283332
- Column and column list
284333
- Divider
285-
- Embed
286334
- File
287335
- Link preview
288336
- Mention

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dynamic = [
3333
dependencies = [
3434
"atsphinx-audioplayer>=0.1.0",
3535
"beartype>=0.21.0",
36+
"beautifulsoup4>=4.13.3",
3637
"click>=8.0.0",
3738
"docutils>=0.21",
3839
"requests>=2.32.5",
@@ -74,6 +75,7 @@ optional-dependencies.dev = [
7475
# use it to lint shell commands in GitHub workflow files.
7576
"shellcheck-py==0.11.0.1",
7677
"shfmt-py==3.12.0.2",
78+
"sphinx-iframes==1.0.7",
7779
"sphinx-lint==1.0.0",
7880
"sphinx-pyproject==0.3.0",
7981
"sphinx-substitution-extensions==2025.6.6",
@@ -86,6 +88,7 @@ optional-dependencies.dev = [
8688
]
8789
optional-dependencies.release = [ "check-wheel-contents==0.6.3" ]
8890
optional-dependencies.sample = [
91+
"sphinx-iframes==1.0.7",
8992
"sphinxcontrib-text-styles==0.2.0",
9093
]
9194
urls.Source = "https://github.com/adamtheturtle/sphinx-notionbuilder"
@@ -415,6 +418,7 @@ ignore_names = [
415418
"rst_prolog",
416419
"source_suffix",
417420
"spelling_word_list_filename",
421+
"suppress_warnings",
418422
"templates_path",
419423
"warning_is_error",
420424
# Docutils translator

sample/conf.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@
44

55
extensions = [
66
"sphinxcontrib.video",
7+
"sphinx_iframes",
8+
"sphinx_notion",
79
"sphinxnotes.strike",
810
"sphinxcontrib_text_styles",
9-
"sphinx_notion",
1011
"sphinx_simplepdf",
1112
"sphinx_toolbox.collapse",
1213
"sphinx_toolbox.rest_example",
1314
"atsphinx.audioplayer",
1415
"sphinx_immaterial.task_lists",
1516
"sphinx.ext.mathjax",
1617
]
18+
19+
# This is necessary because ``sphinx-iframes`` and ``sphinxcontrib.video``
20+
# both implement a ``video`` directive.
21+
# This is explained in the README.
22+
suppress_warnings = ["app.add_directive"]

sample/index.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,3 +336,10 @@ This is useful for documentation that demonstrates how to write reStructuredText
336336
337337
338338
greet(name="World")
339+
340+
Embed Blocks
341+
~~~~~~~~~~~~
342+
343+
Embed blocks can be created using the `sphinx-iframes <https://pypi.org/project/sphinx-iframes/>`_ extension.
344+
345+
.. iframe:: https://www.youtube.com/embed/dQw4w9WgXcQ

src/sphinx_notion/__init__.py

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from pathlib import Path
1010
from typing import Any
1111

12+
import bs4
1213
import sphinxnotes.strike
1314
from atsphinx.audioplayer.nodes import ( # pyright: ignore[reportMissingTypeStubs]
1415
audio as audio_node,
@@ -27,6 +28,7 @@
2728
)
2829
from sphinx_toolbox.collapse import CollapseNode
2930
from sphinxcontrib.video import ( # pyright: ignore[reportMissingTypeStubs]
31+
Video,
3032
video_node,
3133
)
3234
from sphinxnotes.strike import strike_node
@@ -37,6 +39,7 @@
3739
from ultimate_notion.blocks import BulletedItem as UnoBulletedItem
3840
from ultimate_notion.blocks import Callout as UnoCallout
3941
from ultimate_notion.blocks import Code as UnoCode
42+
from ultimate_notion.blocks import Embed as UnoEmbed
4043
from ultimate_notion.blocks import Equation as UnoEquation
4144
from ultimate_notion.blocks import Heading as UnoHeading
4245
from ultimate_notion.blocks import (
@@ -614,6 +617,26 @@ def _map_pygments_to_notion_language(*, pygments_lang: str) -> CodeLang:
614617
return language_mapping[pygments_lang.lower()]
615618

616619

620+
def _get_unsupported_node_type_exception(
621+
*,
622+
node: nodes.Element,
623+
) -> NotImplementedError:
624+
"""
625+
Raise an ``NotImplementedError`` for an unsupported node type.
626+
"""
627+
line_number = node.line or node.parent.line
628+
source = node.source or node.parent.source
629+
630+
if line_number is not None and source is not None:
631+
unsupported_node_type_msg = (
632+
f"Unsupported node type: {node.tagname} on line "
633+
f"{line_number} in {source}."
634+
)
635+
else:
636+
unsupported_node_type_msg = f"Unsupported node type: {node.tagname}."
637+
return NotImplementedError(unsupported_node_type_msg)
638+
639+
617640
@singledispatch
618641
@beartype
619642
def _process_node_to_blocks(
@@ -625,12 +648,7 @@ def _process_node_to_blocks(
625648
Required function for ``singledispatch``.
626649
"""
627650
del section_level
628-
unsupported_node_type_msg = (
629-
f"Unsupported node type: {node.tagname} on line "
630-
f"{node.line or node.parent.line} in "
631-
f"{node.source or node.parent.source}."
632-
)
633-
raise NotImplementedError(unsupported_node_type_msg)
651+
raise _get_unsupported_node_type_exception(node=node)
634652

635653

636654
@beartype
@@ -1354,6 +1372,32 @@ def _(
13541372
return blocks
13551373

13561374

1375+
@beartype
1376+
@_process_node_to_blocks.register
1377+
def _(
1378+
node: nodes.raw,
1379+
*,
1380+
section_level: int,
1381+
) -> list[Block]:
1382+
"""
1383+
Process raw nodes, specifically those containing HTML from the extension
1384+
``sphinx-iframes``.
1385+
"""
1386+
del section_level
1387+
1388+
# Check if this is an ``iframe`` from ``sphinx-iframes``.
1389+
# See https://github.com/TeachBooks/sphinx-iframes/issues/9
1390+
# for making this more robust.
1391+
soup = bs4.BeautifulSoup(markup=node.rawsource, features="html.parser")
1392+
iframe = soup.find(name="iframe")
1393+
if iframe is not None:
1394+
url = iframe.get(key="src")
1395+
assert url is not None
1396+
return [UnoEmbed(url=str(object=url))]
1397+
1398+
raise _get_unsupported_node_type_exception(node=node)
1399+
1400+
13571401
@beartype
13581402
def _process_rest_example_container(
13591403
*,
@@ -1546,6 +1590,14 @@ def _filter_ulem(record: logging.LogRecord) -> bool:
15461590
return msg != "latex package 'ulem' already included"
15471591

15481592

1593+
@beartype
1594+
def _make_static_dir(app: Sphinx) -> None:
1595+
"""
1596+
We make the ``_static`` directory that ``sphinx-iframes`` expects.
1597+
"""
1598+
(app.outdir / "_static").mkdir(parents=True, exist_ok=True)
1599+
1600+
15491601
@beartype
15501602
def setup(app: Sphinx) -> ExtensionMetadata:
15511603
"""
@@ -1559,8 +1611,18 @@ def setup(app: Sphinx) -> ExtensionMetadata:
15591611
callback=_notion_register_pdf_include_directive,
15601612
)
15611613

1614+
app.connect(event="builder-inited", callback=_make_static_dir)
1615+
15621616
logger = logging.getLogger(name="sphinx.sphinx.registry")
15631617
logger.addFilter(filter=_filter_ulem)
15641618

15651619
sphinxnotes.strike.SUPPORTED_BUILDERS.append(NotionBuilder)
1620+
1621+
# that we use. The ``sphinx-iframes`` extension implements a ``video``
1622+
# directive that we don't use.
1623+
# Make sure that if they are both enabled, we use the
1624+
# ``sphinxcontrib.video`` extension.
1625+
if "sphinxcontrib.video" in app.extensions:
1626+
app.add_directive(name="video", cls=Video, override=True)
1627+
15661628
return {"parallel_read_safe": True}

0 commit comments

Comments
 (0)