Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -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)
------------------

Expand Down
25 changes: 25 additions & 0 deletions src/itables/html/datatables_template_offline.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!--| quarto-html-table-processing: none -->
<table id="table_id"></table>
<script type="module">
(async () => {
async function init() {
const { ITable, jQuery: $ } = await window._itables_underscore_version;

document.querySelectorAll("#table_id:not(.dataTable)").forEach(table => {
if (!(table instanceof HTMLTableElement))
return;

let dt_args = {};
new ITable(table, dt_args);
});
}

if (window._itables_underscore_version) {
init();
} else {
window.addEventListener("itables-version-ready", () => {
init();
});
}
})();
</script>
25 changes: 25 additions & 0 deletions src/itables/html/init_notebook_offline.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script>
function injectCSS(base64CSS) {
const cssText = atob(base64CSS);
const style = document.createElement('style');
style.textContent = cssText;
document.head.appendChild(style);
}

async function injectModule(base64JS) {
const jsText = atob(base64JS);
const blob = new Blob([jsText], { type: 'application/javascript' });
const url = URL.createObjectURL(blob);
const module = await import(url);
URL.revokeObjectURL(url);
return module;
}

if (!window._itables_underscore_version) {
injectCSS("dt_css_b64");
window._itables_underscore_version = injectModule("dt_src_b64");
window._itables_underscore_version.then(() => {
window.dispatchEvent(new Event("itables-version-ready"));
});
}
</script>
66 changes: 36 additions & 30 deletions src/itables/javascript.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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)
Expand All @@ -201,20 +202,34 @@ 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"""<style>{dt_css}</style>
return (
init_notebook_mode
+ f"""
<div style="vertical-align:middle; text-align:left">
<script>
window.{DATATABLES_SRC_FOR_ITABLES} = "data:text/javascript;base64,{dt64}";
</script>
<noscript>
{get_animated_logo(opt.display_logo_when_loading)}
This is the <code>init_notebook_mode</code> cell from ITables v{itables_version}<br>
(you should not see this message - is your notebook <it>trusted</it>?)
</noscript>
</div>
"""
)


def _table_header(
Expand Down Expand Up @@ -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}."
Comment on lines 320 to +323
Copy link

Copilot AI Aug 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message should use consistent formatting. Either use f-string format for both pattern and count, or use traditional string formatting for consistency with the rest of the codebase.

Copilot uses AI. Check for mistakes.
)
return template.replace(pattern, value)

Expand Down Expand Up @@ -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(
Expand All @@ -712,21 +725,14 @@ def html_table_from_template(
dt_url[:-3] + ".css",
)
else:
connected_style = (
f'<link href="{UNPKG_DT_BUNDLE_CSS_NO_VERSION}" rel="stylesheet">\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 <code>init_notebook_mode</code> cell"
Expand Down
2 changes: 1 addition & 1 deletion src/itables/version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""ITables' version number"""

__version__ = "2.4.5"
__version__ = "2.5.0-dev"
2 changes: 1 addition & 1 deletion tests/test_connected_notebook_is_small.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 11 additions & 2 deletions tests/test_javascript.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
Loading