Skip to content

Commit e63f81e

Browse files
committed
Offline ITables is robust to rendering order
1 parent 1ad5284 commit e63f81e

File tree

7 files changed

+106
-34
lines changed

7 files changed

+106
-34
lines changed

docs/changelog.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
ITables ChangeLog
22
=================
33

4+
2.5.0-dev (unreleased)
5+
------------------
6+
7+
**Fixed**
8+
- 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))
9+
10+
411
2.4.5 (2025-08-23)
512
------------------
613

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<!--| quarto-html-table-processing: none -->
2+
<table id="table_id"></table>
3+
<script type="module">
4+
(async () => {
5+
async function init() {
6+
const { ITable, jQuery: $ } = await window._itables_underscore_version;
7+
8+
document.querySelectorAll("#table_id:not(.dataTable)").forEach(table => {
9+
if (!(table instanceof HTMLTableElement))
10+
return;
11+
12+
let dt_args = {};
13+
new ITable(table, dt_args);
14+
});
15+
}
16+
17+
if (window._itables_underscore_version) {
18+
init();
19+
} else {
20+
window.addEventListener("itables-version-ready", () => {
21+
init();
22+
});
23+
}
24+
})();
25+
</script>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<script>
2+
function injectCSS(base64CSS) {
3+
const cssText = atob(base64CSS);
4+
const style = document.createElement('style');
5+
style.textContent = cssText;
6+
document.head.appendChild(style);
7+
}
8+
9+
async function injectModule(base64JS) {
10+
const jsText = atob(base64JS);
11+
const blob = new Blob([jsText], { type: 'application/javascript' });
12+
const url = URL.createObjectURL(blob);
13+
const module = await import(url);
14+
URL.revokeObjectURL(url);
15+
return module;
16+
}
17+
18+
if (!window._itables_underscore_version) {
19+
injectCSS("dt_css_b64");
20+
window._itables_underscore_version = injectModule("dt_src_b64");
21+
window._itables_underscore_version.then(() => {
22+
window.dispatchEvent(new Event("itables-version-ready"));
23+
});
24+
}
25+
</script>

src/itables/javascript.py

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,10 @@
5050
from .downsample import downsample
5151
from .utils import read_package_file
5252

53-
DATATABLES_SRC_FOR_ITABLES = (
54-
f"_datatables_src_for_itables_{itables_version.replace('.','_').replace('-','_')}"
53+
_ITABLES_UNDERSCORE_VERSION = (
54+
f"_itables_{itables_version.replace('.','_').replace('-','_')}"
5555
)
56+
_ITABLES_READY_EVENT = f"itables-{itables_version}-ready"
5657
_OPTIONS_NOT_AVAILABLE_IN_APP_MODE = {
5758
"connected",
5859
"dt_url",
@@ -179,7 +180,7 @@ def init_notebook_mode(
179180
)
180181
local_import = (
181182
"const { set_or_remove_dark_class } = await import(window."
182-
+ DATATABLES_SRC_FOR_ITABLES
183+
+ _ITABLES_UNDERSCORE_VERSION
183184
+ ");"
184185
)
185186
init_datatables = replace_value(init_datatables, connected_import, local_import)
@@ -201,20 +202,34 @@ def generate_init_offline_itables_html(dt_bundle: Union[Path, str]) -> str:
201202
assert dt_bundle.suffix == ".js"
202203
dt_src = dt_bundle.read_text(encoding="utf-8")
203204
dt_css = dt_bundle.with_suffix(".css").read_text(encoding="utf-8")
204-
dt64 = b64encode(dt_src.encode("utf-8")).decode("ascii")
205+
dt_src_b64 = b64encode(dt_src.encode("utf-8")).decode("ascii")
206+
dt_css_b64 = b64encode(dt_css.encode("utf-8")).decode("ascii")
207+
208+
init_notebook_mode = read_package_file("html/init_notebook_offline.html")
209+
init_notebook_mode = replace_value(
210+
init_notebook_mode,
211+
"_itables_underscore_version",
212+
_ITABLES_UNDERSCORE_VERSION,
213+
expected_count=3,
214+
)
215+
init_notebook_mode = replace_value(
216+
init_notebook_mode, "itables-version-ready", _ITABLES_READY_EVENT
217+
)
218+
init_notebook_mode = replace_value(init_notebook_mode, "dt_src_b64", dt_src_b64)
219+
init_notebook_mode = replace_value(init_notebook_mode, "dt_css_b64", dt_css_b64)
205220

206-
return f"""<style>{dt_css}</style>
221+
return (
222+
init_notebook_mode
223+
+ f"""
207224
<div style="vertical-align:middle; text-align:left">
208-
<script>
209-
window.{DATATABLES_SRC_FOR_ITABLES} = "data:text/javascript;base64,{dt64}";
210-
</script>
211225
<noscript>
212226
{get_animated_logo(opt.display_logo_when_loading)}
213227
This is the <code>init_notebook_mode</code> cell from ITables v{itables_version}<br>
214228
(you should not see this message - is your notebook <it>trusted</it>?)
215229
</noscript>
216230
</div>
217231
"""
232+
)
218233

219234

220235
def _table_header(
@@ -297,17 +312,15 @@ def get_keys_to_be_evaluated(data: Any) -> list[list[Union[int, str]]]:
297312
return keys_to_be_evaluated
298313

299314

300-
def replace_value(template: str, pattern: str, value: str) -> str:
315+
def replace_value(
316+
template: str, pattern: str, value: str, expected_count: int = 1
317+
) -> str:
301318
"""Set the given pattern to the desired value in the template,
302319
after making sure that the pattern is found exactly once."""
303320
count = template.count(pattern)
304-
if not count:
305-
raise ValueError("pattern={} was not found in template".format(pattern))
306-
elif count > 1:
321+
if count != expected_count:
307322
raise ValueError(
308-
"pattern={} was found multiple times ({}) in template".format(
309-
pattern, count
310-
)
323+
f"{pattern=} was found {count} times in template, expected {expected_count}."
311324
)
312325
return template.replace(pattern, value)
313326

@@ -702,8 +715,8 @@ def html_table_from_template(
702715
)
703716

704717
# Load the HTML template
705-
output = read_package_file("html/datatables_template.html")
706718
if connected:
719+
output = read_package_file("html/datatables_template.html")
707720
assert dt_url.endswith(".js")
708721
output = replace_value(output, UNPKG_DT_BUNDLE_URL_NO_VERSION, dt_url)
709722
output = replace_value(
@@ -712,21 +725,14 @@ def html_table_from_template(
712725
dt_url[:-3] + ".css",
713726
)
714727
else:
715-
connected_style = (
716-
f'<link href="{UNPKG_DT_BUNDLE_CSS_NO_VERSION}" rel="stylesheet">\n'
717-
)
718-
output = replace_value(output, connected_style, "")
719-
connected_import = (
720-
"import { ITable, jQuery as $ } from '"
721-
+ UNPKG_DT_BUNDLE_URL_NO_VERSION
722-
+ "';"
723-
)
724-
local_import = (
725-
"const { ITable, jQuery: $ } = await import(window."
726-
+ DATATABLES_SRC_FOR_ITABLES
727-
+ ");"
728+
output = read_package_file("html/datatables_template_offline.html")
729+
output = replace_value(
730+
output,
731+
"_itables_underscore_version",
732+
_ITABLES_UNDERSCORE_VERSION,
733+
expected_count=2,
728734
)
729-
output = replace_value(output, connected_import, local_import)
735+
output = replace_value(output, "itables-version-ready", _ITABLES_READY_EVENT)
730736

731737
itables_source = (
732738
"the internet" if connected else "the <code>init_notebook_mode</code> cell"

src/itables/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""ITables' version number"""
22

3-
__version__ = "2.4.5"
3+
__version__ = "2.5.0-dev"

tests/test_connected_notebook_is_small.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,4 @@ def test_offline_notebook_is_not_too_large(tmp_path):
4040
nb_py.write_text(text_notebook(connected=False))
4141
jupytext([str(nb_py), "--to", "ipynb", "--set-kernel", "itables", "--execute"])
4242
assert nb_ipynb.exists()
43-
assert 750000 < nb_ipynb.stat().st_size < 850000
43+
assert 825000 < nb_ipynb.stat().st_size < 875000

tests/test_javascript.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,23 @@ def test_replace_value(
7373
def test_replace_value_not_found(
7474
template="line1\nline2\nline3\n", pattern="line4", value="new line4"
7575
):
76-
with pytest.raises(ValueError, match="not found"):
76+
with pytest.raises(ValueError, match="was found 0 times in template"):
7777
assert replace_value(template, pattern, value)
7878

7979

80+
def test_replace_value_multiple_expected(
81+
template="line1\nline2\nline2\n", pattern="line2", value="new line2"
82+
):
83+
assert (
84+
replace_value(template, pattern, value, expected_count=2)
85+
== "line1\nnew line2\nnew line2\n"
86+
)
87+
88+
8089
def test_replace_value_multiple(
8190
template="line1\nline2\nline2\n", pattern="line2", value="new line2"
8291
):
83-
with pytest.raises(ValueError, match="found multiple times"):
92+
with pytest.raises(ValueError, match="was found 2 times in template, expected 1."):
8493
assert replace_value(template, pattern, value)
8594

8695

0 commit comments

Comments
 (0)