diff --git a/__TEST__/hyperaudio-lite.test.js b/__TEST__/hyperaudio-lite.test.js
index 5b9ff0d..5633920 100644
--- a/__TEST__/hyperaudio-lite.test.js
+++ b/__TEST__/hyperaudio-lite.test.js
@@ -158,6 +158,22 @@ test("createWordArray", () => {
expect(ht.createWordArray(words)).toStrictEqual(expectedResult);
});
+test("createWordArray ignores malformed data-m values", () => {
+ const container = document.createElement("p");
+ container.innerHTML =
+ 'valid' +
+ 'invalid' +
+ 'valid';
+
+ const result = ht.createWordArray(container.querySelectorAll("[data-m]"));
+ const invalidWord = container.children[1];
+
+ expect(result.map(({ m }) => m)).toEqual([100, 300]);
+ expect(invalidWord.classList.contains("unread")).toBe(true);
+ expect(invalidWord.classList.contains("read")).toBe(false);
+ expect(invalidWord.classList.contains("active")).toBe(false);
+});
+
test("getSelectionMediaFragment", () => {
document
.getSelection()
@@ -414,6 +430,25 @@ test("setPlayHead updates currentTime and plays if playOnClick is true", () => {
expect(ht.myPlayer.play).toHaveBeenCalled();
});
+test("setPlayHead ignores targets without a numeric data-m", () => {
+ ht.playOnClick = true;
+ ht.highlightedText = true;
+ ht.myPlayer = { setTime: jest.fn(), play: jest.fn(), paused: true };
+ const updateSpy = jest.spyOn(ht, "updateTranscriptVisualState");
+ const activeWord = document.querySelector('span[data-m="3950"]');
+ activeWord.classList.add("active");
+
+ ht.setPlayHead({ target: activeWord.parentNode });
+
+ expect(updateSpy).not.toHaveBeenCalled();
+ expect(ht.myPlayer.setTime).not.toHaveBeenCalled();
+ expect(ht.myPlayer.play).not.toHaveBeenCalled();
+ expect(ht.highlightedText).toBe(true);
+ expect(activeWord.classList.contains("active")).toBe(true);
+
+ updateSpy.mockRestore();
+});
+
test("preparePlayHead sets paused to false and calls checkPlayHead", () => {
ht.checkPlayHead = jest.fn();
ht.preparePlayHead();
diff --git a/js/hyperaudio-lite.js b/js/hyperaudio-lite.js
index c86b2a3..fcc3a8d 100644
--- a/js/hyperaudio-lite.js
+++ b/js/hyperaudio-lite.js
@@ -631,8 +631,23 @@ class HyperaudioLite {
// Create an array of words with metadata from the transcript
createWordArray(words) {
- return Array.from(words).map(word => {
+ const wordArr = [];
+
+ Array.from(words).forEach(word => {
const m = parseInt(word.getAttribute('data-m'));
+
+ // Reset to a clean baseline — stale read/active classes from a previous
+ // instance on the same DOM would survive the delta updates in
+ // updateTranscriptVisualState (#251).
+ word.classList.remove('read', 'active');
+ word.classList.add('unread');
+
+ // A malformed timestamp breaks every binary-search comparison that
+ // probes it. Keep the DOM node inert instead of poisoning playback.
+ if (Number.isNaN(m)) {
+ return;
+ }
+
let p = word.parentNode;
while (p !== document) {
if (['p', 'figure', 'ul'].includes(p.tagName.toLowerCase())) {
@@ -640,13 +655,11 @@ class HyperaudioLite {
}
p = p.parentNode;
}
- // Reset to a clean baseline — stale read/active classes from a previous
- // instance on the same DOM would survive the delta updates in
- // updateTranscriptVisualState (#251).
- word.classList.remove('read', 'active');
- word.classList.add('unread');
- return { n: word, m, p };
+
+ wordArr.push({ n: word, m, p });
});
+
+ return wordArr;
}
getSelectionRange = () => {
@@ -721,10 +734,19 @@ class HyperaudioLite {
}
const target = e.target || e.srcElement;
+ const timeSecs = target && target.dataset
+ ? parseInt(target.dataset.m) / 1000
+ : NaN;
+
+ // Clicks on transcript whitespace or malformed timed nodes must not update
+ // the visual-state binary search with NaN.
+ if (Number.isNaN(timeSecs)) {
+ return;
+ }
+
this.highlightedText = false;
this.clearActiveClasses();
- const timeSecs = parseInt(target.dataset.m) / 1000;
this.updateTranscriptVisualState(timeSecs);
// Mark the clicked word as active AFTER updateTranscriptVisualState (which
@@ -736,12 +758,10 @@ class HyperaudioLite {
target.parentNode.classList.add('active');
}
- if (!isNaN(timeSecs)) {
- this.end = null;
- this.myPlayer.setTime(timeSecs);
- if (this.playOnClick) {
- this.myPlayer.play();
- }
+ this.end = null;
+ this.myPlayer.setTime(timeSecs);
+ if (this.playOnClick) {
+ this.myPlayer.play();
}
}
diff --git a/js/hyperaudio-lite.mjs b/js/hyperaudio-lite.mjs
index 82f9f7c..e24a79b 100644
--- a/js/hyperaudio-lite.mjs
+++ b/js/hyperaudio-lite.mjs
@@ -631,8 +631,23 @@ class HyperaudioLite {
// Create an array of words with metadata from the transcript
createWordArray(words) {
- return Array.from(words).map(word => {
+ const wordArr = [];
+
+ Array.from(words).forEach(word => {
const m = parseInt(word.getAttribute('data-m'));
+
+ // Reset to a clean baseline — stale read/active classes from a previous
+ // instance on the same DOM would survive the delta updates in
+ // updateTranscriptVisualState (#251).
+ word.classList.remove('read', 'active');
+ word.classList.add('unread');
+
+ // A malformed timestamp breaks every binary-search comparison that
+ // probes it. Keep the DOM node inert instead of poisoning playback.
+ if (Number.isNaN(m)) {
+ return;
+ }
+
let p = word.parentNode;
while (p !== document) {
if (['p', 'figure', 'ul'].includes(p.tagName.toLowerCase())) {
@@ -640,13 +655,11 @@ class HyperaudioLite {
}
p = p.parentNode;
}
- // Reset to a clean baseline — stale read/active classes from a previous
- // instance on the same DOM would survive the delta updates in
- // updateTranscriptVisualState (#251).
- word.classList.remove('read', 'active');
- word.classList.add('unread');
- return { n: word, m, p };
+
+ wordArr.push({ n: word, m, p });
});
+
+ return wordArr;
}
getSelectionRange = () => {
@@ -721,10 +734,19 @@ class HyperaudioLite {
}
const target = e.target || e.srcElement;
+ const timeSecs = target && target.dataset
+ ? parseInt(target.dataset.m) / 1000
+ : NaN;
+
+ // Clicks on transcript whitespace or malformed timed nodes must not update
+ // the visual-state binary search with NaN.
+ if (Number.isNaN(timeSecs)) {
+ return;
+ }
+
this.highlightedText = false;
this.clearActiveClasses();
- const timeSecs = parseInt(target.dataset.m) / 1000;
this.updateTranscriptVisualState(timeSecs);
// Mark the clicked word as active AFTER updateTranscriptVisualState (which
@@ -736,12 +758,10 @@ class HyperaudioLite {
target.parentNode.classList.add('active');
}
- if (!isNaN(timeSecs)) {
- this.end = null;
- this.myPlayer.setTime(timeSecs);
- if (this.playOnClick) {
- this.myPlayer.play();
- }
+ this.end = null;
+ this.myPlayer.setTime(timeSecs);
+ if (this.playOnClick) {
+ this.myPlayer.play();
}
}