diff --git a/solstone/apps/body/tests/test_body_app.py b/solstone/apps/body/tests/test_body_app.py
index 83a30100a..347323900 100644
--- a/solstone/apps/body/tests/test_body_app.py
+++ b/solstone/apps/body/tests/test_body_app.py
@@ -590,7 +590,8 @@ def test_drawer_render_emphasizes_digits_after_escaping_under_node():
node = _node_or_skip()
source = Path("solstone/convey/static/drawer.js").read_text(encoding="utf-8")
functions = "\n".join(
- _function_source(source, name) for name in ("escapeHtml", "render")
+ _function_source(source, name)
+ for name in ("escapeHtml", "formatLine", "render")
)
script = "\n".join(
[
diff --git a/solstone/convey/static/drawer.js b/solstone/convey/static/drawer.js
index 8a0957e32..c8b9b41b2 100644
--- a/solstone/convey/static/drawer.js
+++ b/solstone/convey/static/drawer.js
@@ -17,15 +17,20 @@
})[char]);
}
+ function formatLine(line) {
+ const value = String(line ?? '').trim();
+ return value
+ ? escapeHtml(value).replace(/?\w+;|\d[\d,.:]*(?:\s?(?:am|pm|s))?/g, (m) => (m.startsWith('&') ? m : `${m}`))
+ : '';
+ }
+
function render(options) {
const config = options || {};
const id = String(config.id ?? '').trim();
const idAttr = id ? ` data-drawer-id="${escapeHtml(id)}"` : '';
const openAttr = config.open ? ' open' : '';
- const line = String(config.line ?? '').trim();
- const lineHtml = line
- ? `${escapeHtml(line).replace(/?\w+;|\d[\d,.:]*(?:\s?(?:am|pm|s))?/g, (m) => (m.startsWith('&') ? m : `${m}`))}`
- : '';
+ const line = formatLine(config.line);
+ const lineHtml = line ? `${line}` : '';
const chipText = String(config.chipText ?? '').trim();
const chipTone = config.chipTone === 'warn' || config.chipTone === 'danger'
? ` drawer-chip--${config.chipTone}`
@@ -65,5 +70,5 @@
return result;
}
- window.Drawer = Object.freeze({ render, preserveOpen });
+ window.Drawer = Object.freeze({ render, preserveOpen, formatLine });
})();
diff --git a/tests/test_convey_drawer.py b/tests/test_convey_drawer.py
index 137da3613..d597806e3 100644
--- a/tests/test_convey_drawer.py
+++ b/tests/test_convey_drawer.py
@@ -59,7 +59,9 @@ def test_drawer_js_contract_and_constraints():
assert "(function () {" in source
assert "'use strict';" in source
- assert "window.Drawer = Object.freeze({ render, preserveOpen });" in source
+ assert (
+ "window.Drawer = Object.freeze({ render, preserveOpen, formatLine });" in source
+ )
assert "Storage" not in source
assert "localStorage" not in source
assert "sessionStorage" not in source
@@ -285,6 +287,8 @@ const directDrawer = window.Drawer.render({
line: "median statement length 1.5s — needs 2s",
bodyHtml: "",
});
+assert(Object.keys(window.Drawer).join("|") === "render|preserveOpen|formatLine", "drawer exports formatLine");
+assert(window.Drawer.formatLine("2 files created · <5>").includes("2 files created · <5>"), "formatLine escapes and emphasizes digits");
assert(lineHtml(directDrawer).includes("1.5s"), "drawer emphasizes compact seconds");
const tooFewPayload = payload();