diff --git a/docs/changelog.md b/docs/changelog.md
index 11d60f5c..c6b18438 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,6 +1,13 @@
ITables ChangeLog
=================
+2.5.0-dev (unreleased)
+------------------
+
+**Fixed**
+- The offline mode now allows the init cell to be rendered after the table cells. It should work more reliably in VS Code ([#424](https://github.com/mwouts/itables/issues/424))
+
+
2.4.5 (2025-08-23)
------------------
diff --git a/src/itables/html/datatables_template_offline.html b/src/itables/html/datatables_template_offline.html
new file mode 100644
index 00000000..77229047
--- /dev/null
+++ b/src/itables/html/datatables_template_offline.html
@@ -0,0 +1,25 @@
+
+
+
diff --git a/src/itables/html/init_notebook_offline.html b/src/itables/html/init_notebook_offline.html
new file mode 100644
index 00000000..bc5024d2
--- /dev/null
+++ b/src/itables/html/init_notebook_offline.html
@@ -0,0 +1,25 @@
+
diff --git a/src/itables/javascript.py b/src/itables/javascript.py
index c98008af..6679929b 100644
--- a/src/itables/javascript.py
+++ b/src/itables/javascript.py
@@ -50,9 +50,10 @@
from .downsample import downsample
from .utils import read_package_file
-DATATABLES_SRC_FOR_ITABLES = (
- f"_datatables_src_for_itables_{itables_version.replace('.','_').replace('-','_')}"
+_ITABLES_UNDERSCORE_VERSION = (
+ f"_itables_{itables_version.replace('.','_').replace('-','_')}"
)
+_ITABLES_READY_EVENT = f"itables-{itables_version}-ready"
_OPTIONS_NOT_AVAILABLE_IN_APP_MODE = {
"connected",
"dt_url",
@@ -179,7 +180,7 @@ def init_notebook_mode(
)
local_import = (
"const { set_or_remove_dark_class } = await import(window."
- + DATATABLES_SRC_FOR_ITABLES
+ + _ITABLES_UNDERSCORE_VERSION
+ ");"
)
init_datatables = replace_value(init_datatables, connected_import, local_import)
@@ -201,13 +202,26 @@ def generate_init_offline_itables_html(dt_bundle: Union[Path, str]) -> str:
assert dt_bundle.suffix == ".js"
dt_src = dt_bundle.read_text(encoding="utf-8")
dt_css = dt_bundle.with_suffix(".css").read_text(encoding="utf-8")
- dt64 = b64encode(dt_src.encode("utf-8")).decode("ascii")
+ dt_src_b64 = b64encode(dt_src.encode("utf-8")).decode("ascii")
+ dt_css_b64 = b64encode(dt_css.encode("utf-8")).decode("ascii")
+
+ init_notebook_mode = read_package_file("html/init_notebook_offline.html")
+ init_notebook_mode = replace_value(
+ init_notebook_mode,
+ "_itables_underscore_version",
+ _ITABLES_UNDERSCORE_VERSION,
+ expected_count=3,
+ )
+ init_notebook_mode = replace_value(
+ init_notebook_mode, "itables-version-ready", _ITABLES_READY_EVENT
+ )
+ init_notebook_mode = replace_value(init_notebook_mode, "dt_src_b64", dt_src_b64)
+ init_notebook_mode = replace_value(init_notebook_mode, "dt_css_b64", dt_css_b64)
- return f"""
+ return (
+ init_notebook_mode
+ + f"""
-
{get_animated_logo(opt.display_logo_when_loading)}
This is the init_notebook_mode cell from ITables v{itables_version}
@@ -215,6 +229,7 @@ def generate_init_offline_itables_html(dt_bundle: Union[Path, str]) -> str:
"""
+ )
def _table_header(
@@ -297,17 +312,15 @@ def get_keys_to_be_evaluated(data: Any) -> list[list[Union[int, str]]]:
return keys_to_be_evaluated
-def replace_value(template: str, pattern: str, value: str) -> str:
+def replace_value(
+ template: str, pattern: str, value: str, expected_count: int = 1
+) -> str:
"""Set the given pattern to the desired value in the template,
after making sure that the pattern is found exactly once."""
count = template.count(pattern)
- if not count:
- raise ValueError("pattern={} was not found in template".format(pattern))
- elif count > 1:
+ if count != expected_count:
raise ValueError(
- "pattern={} was found multiple times ({}) in template".format(
- pattern, count
- )
+ f"{pattern=} was found {count} times in template, expected {expected_count}."
)
return template.replace(pattern, value)
@@ -702,8 +715,8 @@ def html_table_from_template(
)
# Load the HTML template
- output = read_package_file("html/datatables_template.html")
if connected:
+ output = read_package_file("html/datatables_template.html")
assert dt_url.endswith(".js")
output = replace_value(output, UNPKG_DT_BUNDLE_URL_NO_VERSION, dt_url)
output = replace_value(
@@ -712,21 +725,14 @@ def html_table_from_template(
dt_url[:-3] + ".css",
)
else:
- connected_style = (
- f' \n'
- )
- output = replace_value(output, connected_style, "")
- connected_import = (
- "import { ITable, jQuery as $ } from '"
- + UNPKG_DT_BUNDLE_URL_NO_VERSION
- + "';"
- )
- local_import = (
- "const { ITable, jQuery: $ } = await import(window."
- + DATATABLES_SRC_FOR_ITABLES
- + ");"
+ output = read_package_file("html/datatables_template_offline.html")
+ output = replace_value(
+ output,
+ "_itables_underscore_version",
+ _ITABLES_UNDERSCORE_VERSION,
+ expected_count=2,
)
- output = replace_value(output, connected_import, local_import)
+ output = replace_value(output, "itables-version-ready", _ITABLES_READY_EVENT)
itables_source = (
"the internet" if connected else "the init_notebook_mode cell"
diff --git a/src/itables/version.py b/src/itables/version.py
index 5689dd5c..bc0001e7 100644
--- a/src/itables/version.py
+++ b/src/itables/version.py
@@ -1,3 +1,3 @@
"""ITables' version number"""
-__version__ = "2.4.5"
+__version__ = "2.5.0-dev"
diff --git a/tests/test_connected_notebook_is_small.py b/tests/test_connected_notebook_is_small.py
index bfab24a2..993ddad9 100644
--- a/tests/test_connected_notebook_is_small.py
+++ b/tests/test_connected_notebook_is_small.py
@@ -40,4 +40,4 @@ def test_offline_notebook_is_not_too_large(tmp_path):
nb_py.write_text(text_notebook(connected=False))
jupytext([str(nb_py), "--to", "ipynb", "--set-kernel", "itables", "--execute"])
assert nb_ipynb.exists()
- assert 750000 < nb_ipynb.stat().st_size < 850000
+ assert 825000 < nb_ipynb.stat().st_size < 875000
diff --git a/tests/test_javascript.py b/tests/test_javascript.py
index f0e587de..88bcbd1e 100644
--- a/tests/test_javascript.py
+++ b/tests/test_javascript.py
@@ -73,14 +73,23 @@ def test_replace_value(
def test_replace_value_not_found(
template="line1\nline2\nline3\n", pattern="line4", value="new line4"
):
- with pytest.raises(ValueError, match="not found"):
+ with pytest.raises(ValueError, match="was found 0 times in template"):
assert replace_value(template, pattern, value)
+def test_replace_value_multiple_expected(
+ template="line1\nline2\nline2\n", pattern="line2", value="new line2"
+):
+ assert (
+ replace_value(template, pattern, value, expected_count=2)
+ == "line1\nnew line2\nnew line2\n"
+ )
+
+
def test_replace_value_multiple(
template="line1\nline2\nline2\n", pattern="line2", value="new line2"
):
- with pytest.raises(ValueError, match="found multiple times"):
+ with pytest.raises(ValueError, match="was found 2 times in template, expected 1."):
assert replace_value(template, pattern, value)