From f832fd7c2518191d6e1a7bf12e0c47fa5da838f1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 13 May 2026 10:13:26 +0000
Subject: [PATCH 1/4] Initial plan
From 23ad5406c1883409f15bbb1c469c774f5599ccfd Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 13 May 2026 10:22:14 +0000
Subject: [PATCH 2/4] fix: preserve toc state in shared urls
Agent-Logs-Url: https://github.com/ether/ep_table_of_contents/sessions/5f0f5716-b682-49ff-a18e-534e67b3de76
Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com>
---
static/js/postAceInit.js | 8 +++++-
static/js/toc.js | 55 ++++++++++++++++++++++++++++++++++++-
tests/toc-numbering.test.js | 45 +++++++++++++++++++++++++++---
3 files changed, 102 insertions(+), 6 deletions(-)
diff --git a/static/js/postAceInit.js b/static/js/postAceInit.js
index 62e896b..f8c3640 100644
--- a/static/js/postAceInit.js
+++ b/static/js/postAceInit.js
@@ -41,9 +41,14 @@ exports.postAceInit = () => {
}
const toc = getToc();
if (!toc) return;
+ toc.bindShareUrlSync();
const state = tocToggle.init({
- onChange: (enabled) => { enabled ? toc.enable() : toc.disable(); },
+ onChange: (enabled) => {
+ enabled ? toc.enable() : toc.disable();
+ toc.syncLocationUrl(enabled);
+ toc.syncShareUrls(enabled);
+ },
});
// ?toc=true / ?toc=false URL parameter still overrides the resolved
@@ -52,6 +57,7 @@ exports.postAceInit = () => {
if (tocParam === true || tocParam === false) {
tocParam ? toc.enable() : toc.disable();
$('#options-toc').prop('checked', tocParam);
+ toc.syncShareUrls(tocParam);
} else {
// No URL override — make sure the editor matches the helper's effective
// value. (init() already fired onChange once, so this is belt-and-braces.)
diff --git a/static/js/toc.js b/static/js/toc.js
index c453ede..d85b625 100644
--- a/static/js/toc.js
+++ b/static/js/toc.js
@@ -57,6 +57,20 @@ const getOutlineEntries = (toc) => {
});
};
+const setBooleanUrlParam = (rawUrl, name, enabled, baseUrl = window.location.href) => {
+ const url = new URL(rawUrl, baseUrl);
+ url.searchParams.set(name, String(enabled));
+ return url.toString();
+};
+
+const setEmbedCodeUrlParam = (embedCode, name, enabled, baseUrl = window.location.href) => {
+ return embedCode.replace(
+ /\bsrc=(['"])([^'"]+)\1/,
+ (match, quote, rawUrl) =>
+ `src=${quote}${setBooleanUrlParam(rawUrl, name, enabled, baseUrl)}${quote}`,
+ );
+};
+
if (typeof $ !== 'undefined') {
$('#tocButton').click(() => {
$('#toc').toggle();
@@ -81,6 +95,43 @@ const tableOfContents = globalThis.tableOfContents = {
$('#toc').hide();
},
+ syncShareUrls: (enabled) => {
+ const $linkInput = $('#linkinput');
+ if ($linkInput.length > 0) {
+ const linkValue = $linkInput.val();
+ if (typeof linkValue === 'string' && linkValue !== '') {
+ $linkInput.val(setBooleanUrlParam(linkValue, 'toc', enabled));
+ }
+ }
+
+ const $embedInput = $('#embedinput');
+ if ($embedInput.length > 0) {
+ const embedValue = $embedInput.val();
+ if (typeof embedValue === 'string' && embedValue.includes('src=')) {
+ $embedInput.val(setEmbedCodeUrlParam(embedValue, 'toc', enabled));
+ }
+ }
+ },
+
+ syncLocationUrl: (enabled) => {
+ if (!window.history || typeof window.history.replaceState !== 'function') return;
+ const nextUrl = setBooleanUrlParam(window.location.href, 'toc', enabled);
+ window.history.replaceState(window.history.state, document.title, nextUrl);
+ },
+
+ bindShareUrlSync: () => {
+ if (tableOfContents._shareUrlSyncBound) return;
+ tableOfContents._shareUrlSyncBound = true;
+
+ $(document).on('focusin.ep_table_of_contents', '#linkinput, #embedinput', () => {
+ tableOfContents.syncShareUrls($('#options-toc').is(':checked'));
+ });
+
+ $(document).on('change.ep_table_of_contents', '#readonlyinput', () => {
+ setTimeout(() => tableOfContents.syncShareUrls($('#options-toc').is(':checked')));
+ });
+ },
+
// Find Tags
findTags: () => {
const toc = [];
@@ -162,7 +213,7 @@ const tableOfContents = globalThis.tableOfContents = {
});
$('.tocItem').removeClass('activeTOC');
- if (activeTocIndex === null) return;
+ if (activeTocIndex == null) return;
$(`.tocItem[data-toc-index="${activeTocIndex}"]`).addClass('activeTOC');
},
@@ -213,4 +264,6 @@ const tableOfContents = globalThis.tableOfContents = {
return true;
},
+ _shareUrlSyncBound: false,
+
};
diff --git a/tests/toc-numbering.test.js b/tests/toc-numbering.test.js
index e7b62ae..a8a30a4 100644
--- a/tests/toc-numbering.test.js
+++ b/tests/toc-numbering.test.js
@@ -12,18 +12,31 @@ const loadTocHelpers = () => {
const sandbox = {
console,
URLSearchParams,
+ URL,
globalThis: {},
+ window: {
+ location: {href: 'https://example.test/p/test-pad?showChat=false#section'},
+ },
$: () => ({
- click() {},
+ click: () => {},
}),
};
sandbox.globalThis = sandbox;
- vm.runInNewContext(`${source}
-globalThis.__tocTestExports = {getHeadingLevel, getOutlineEntries};`, sandbox, {filename: tocPath});
+ vm.runInNewContext(
+ `${source}
+globalThis.__tocTestExports = {
+ getHeadingLevel,
+ getOutlineEntries,
+ setBooleanUrlParam,
+ setEmbedCodeUrlParam,
+};`,
+ sandbox,
+ {filename: tocPath},
+ );
return sandbox.__tocTestExports;
};
-const {getOutlineEntries} = loadTocHelpers();
+const {getOutlineEntries, setBooleanUrlParam, setEmbedCodeUrlParam} = loadTocHelpers();
const makeEntries = (tags) => tags.map((tag, index) => ({
tag,
@@ -88,3 +101,27 @@ test('keeps numbering stable across many sibling headings', () => {
assert.deepEqual(summary[99], {depth: 1, numbering: '100'});
assert.deepEqual(summary[199], {depth: 1, numbering: '200'});
});
+
+test('adds the toc state to shared pad links without dropping existing params', () => {
+ assert.equal(
+ setBooleanUrlParam('https://example.test/p/test-pad?showChat=false#section', 'toc', true),
+ 'https://example.test/p/test-pad?showChat=false&toc=true#section',
+ );
+ assert.equal(
+ setBooleanUrlParam('https://example.test/p/test-pad?showChat=false#section', 'toc', false),
+ 'https://example.test/p/test-pad?showChat=false&toc=false#section',
+ );
+});
+
+test('adds the toc state to embed iframe src urls', () => {
+ const embedCode =
+ '';
+ assert.equal(
+ setEmbedCodeUrlParam(embedCode, 'toc', true),
+ '',
+ );
+ assert.equal(
+ setEmbedCodeUrlParam(embedCode, 'toc', false),
+ '',
+ );
+});
From 7a894508d3c26a95392c1f2d04fc9075eb10e02b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 13 May 2026 10:24:39 +0000
Subject: [PATCH 3/4] test: cover toc url parameter rewrites
Agent-Logs-Url: https://github.com/ether/ep_table_of_contents/sessions/5f0f5716-b682-49ff-a18e-534e67b3de76
Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com>
---
static/js/toc.js | 21 +++++++++++++--------
1 file changed, 13 insertions(+), 8 deletions(-)
diff --git a/static/js/toc.js b/static/js/toc.js
index d85b625..f500fb2 100644
--- a/static/js/toc.js
+++ b/static/js/toc.js
@@ -57,19 +57,22 @@ const getOutlineEntries = (toc) => {
});
};
-const setBooleanUrlParam = (rawUrl, name, enabled, baseUrl = window.location.href) => {
+const getBaseUrl = () => (typeof window !== 'undefined' ? window.location.href : undefined);
+
+const setBooleanUrlParam = (rawUrl, name, enabled, baseUrl = getBaseUrl()) => {
const url = new URL(rawUrl, baseUrl);
url.searchParams.set(name, String(enabled));
return url.toString();
};
-const setEmbedCodeUrlParam = (embedCode, name, enabled, baseUrl = window.location.href) => {
- return embedCode.replace(
- /\bsrc=(['"])([^'"]+)\1/,
- (match, quote, rawUrl) =>
- `src=${quote}${setBooleanUrlParam(rawUrl, name, enabled, baseUrl)}${quote}`,
- );
-};
+const setEmbedCodeUrlParam = (
+ embedCode, name, enabled, baseUrl = getBaseUrl()
+) => embedCode.replace(
+ /\bsrc=(['"])([^'"]+)\1/,
+ (match, quote, rawUrl) => {
+ const urlWithParam = setBooleanUrlParam(rawUrl, name, enabled, baseUrl);
+ return `src=${quote}${urlWithParam}${quote}`;
+ });
if (typeof $ !== 'undefined') {
$('#tocButton').click(() => {
@@ -128,6 +131,8 @@ const tableOfContents = globalThis.tableOfContents = {
});
$(document).on('change.ep_table_of_contents', '#readonlyinput', () => {
+ // Etherpad rewrites the share/embed fields after the readonly checkbox
+ // change handler runs, so wait a tick before patching the generated URLs.
setTimeout(() => tableOfContents.syncShareUrls($('#options-toc').is(':checked')));
});
},
From d1a375fd86c5cbb3a95fc7c7fbea1afd1c4c15d1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 13 May 2026 10:29:02 +0000
Subject: [PATCH 4/4] test: verify toc url syncing behavior
Agent-Logs-Url: https://github.com/ether/ep_table_of_contents/sessions/5f0f5716-b682-49ff-a18e-534e67b3de76
Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com>
---
static/js/toc.js | 29 ++++++----
tests/toc-numbering.test.js | 109 +++++++++++++++++++++++++++++++++---
2 files changed, 120 insertions(+), 18 deletions(-)
diff --git a/static/js/toc.js b/static/js/toc.js
index f500fb2..75255b9 100644
--- a/static/js/toc.js
+++ b/static/js/toc.js
@@ -57,20 +57,26 @@ const getOutlineEntries = (toc) => {
});
};
-const getBaseUrl = () => (typeof window !== 'undefined' ? window.location.href : undefined);
-
-const setBooleanUrlParam = (rawUrl, name, enabled, baseUrl = getBaseUrl()) => {
- const url = new URL(rawUrl, baseUrl);
+const ETHERPAD_FIELD_REWRITE_DELAY_MS = 0;
+const getWindowHref = () => (typeof window !== 'undefined' ? window.location.href : undefined);
+
+const setBooleanUrlParam = (rawUrl, name, enabled, contextUrl = getWindowHref()) => {
+ let url;
+ try {
+ url = contextUrl == null ? new URL(rawUrl) : new URL(rawUrl, contextUrl);
+ } catch (err) {
+ return rawUrl;
+ }
url.searchParams.set(name, String(enabled));
return url.toString();
};
const setEmbedCodeUrlParam = (
- embedCode, name, enabled, baseUrl = getBaseUrl()
+ embedCode, name, enabled, contextUrl = getWindowHref()
) => embedCode.replace(
- /\bsrc=(['"])([^'"]+)\1/,
+ /\bsrc=(['"])([^'"]+)\1/g,
(match, quote, rawUrl) => {
- const urlWithParam = setBooleanUrlParam(rawUrl, name, enabled, baseUrl);
+ const urlWithParam = setBooleanUrlParam(rawUrl, name, enabled, contextUrl);
return `src=${quote}${urlWithParam}${quote}`;
});
@@ -110,7 +116,7 @@ const tableOfContents = globalThis.tableOfContents = {
const $embedInput = $('#embedinput');
if ($embedInput.length > 0) {
const embedValue = $embedInput.val();
- if (typeof embedValue === 'string' && embedValue.includes('src=')) {
+ if (typeof embedValue === 'string' && embedValue.includes('