@playpilot/tpi 5.20.0 → 5.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/link-injections.js +9 -9
- package/package.json +1 -1
- package/src/lib/injection.ts +23 -4
- package/src/lib/text.ts +22 -0
- package/src/tests/lib/injections.test.js +31 -0
- package/src/tests/lib/text.test.js +21 -1
package/package.json
CHANGED
package/src/lib/injection.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mount, unmount } from 'svelte'
|
|
2
2
|
import TitlePopover from '../routes/components/TitlePopover.svelte'
|
|
3
3
|
import AfterArticlePlaylinks from '../routes/components/Playlinks/AfterArticlePlaylinks.svelte'
|
|
4
|
-
import { cleanPhrase, findNumberOfMatchesInString, findShortestMatchBetweenPhrases, findTextNodeContaining, getIndexOfPhraseInElement, getNumberOfLeadingAndTrailingSpaces, isNodeInLink, replaceBetween, replaceStartingFrom } from './text'
|
|
4
|
+
import { cleanPhrase, findNumberOfMatchesInString, findShortestMatchBetweenPhrases, findTextNodeContaining, getIndexOfPhraseInElement, getIndexOfPhraseInBoundary, getNumberOfLeadingAndTrailingSpaces, isNodeInLink, replaceBetween, replaceStartingFrom } from './text'
|
|
5
5
|
import type { LinkInjection, LinkInjectionTypes } from './types/injection'
|
|
6
6
|
import { isHoldingSpecialKey } from './event'
|
|
7
7
|
import { playFallbackViewTransition } from './viewTransition'
|
|
@@ -122,7 +122,7 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
|
|
|
122
122
|
removePlayPilotTitleLinks()
|
|
123
123
|
|
|
124
124
|
const mergedInjections = mergeInjectionTypes(injections)
|
|
125
|
-
if (!mergedInjections) return []
|
|
125
|
+
if (!mergedInjections.length) return []
|
|
126
126
|
|
|
127
127
|
// Find injection in text content of all elements together, ignore potential HTML elements.
|
|
128
128
|
// This is to filter out injections that can't be injected anyway.
|
|
@@ -216,6 +216,13 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
|
|
|
216
216
|
// But it's something to fall back on regardless.
|
|
217
217
|
const startOfSentenceIndex = sentenceIndex === -1 ? element.innerHTML.indexOf(injection.sentence.slice(0, 20)) : -1
|
|
218
218
|
|
|
219
|
+
// Similar to the start of the sentence, we look first just the first word of the sentence. This is only relevant if the start
|
|
220
|
+
// of the sentence is broken up by HTML is less characters than is required for startOfSentenceIndex.
|
|
221
|
+
// This only works if the first word occurs only once, as otherwise we might match on the incorrect part of the sentence.
|
|
222
|
+
const firstWordOfSentence = injection.sentence.split(' ')[0]
|
|
223
|
+
const firstWordOccurrences = findNumberOfMatchesInString(element.innerHTML, firstWordOfSentence)
|
|
224
|
+
const firstWordIndex = firstWordOccurrences === 1 ? element.innerHTML.indexOf(firstWordOfSentence) : -1
|
|
225
|
+
|
|
219
226
|
// Starting from occurence happens when a sentence might include multiple occurences of the same phrase.
|
|
220
227
|
// We might still prefer the valueIndex if that is higher than this value.
|
|
221
228
|
|
|
@@ -224,14 +231,26 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
|
|
|
224
231
|
const indexOfOccurrence = getNumberOfOccurrencesInArray<LinkInjection>(foundInjections.slice(0, injectionIndex + 1), injection, ['title', 'sentence']) - 1
|
|
225
232
|
const indexInSentence = getIndexOfPhraseInElement(nodeContainingText.nodeValue, element, indexOfOccurrence)
|
|
226
233
|
|
|
227
|
-
replacementIndex = Math.max(valueIndex, startOfSentenceIndex, indexInSentence, sentenceIndex, 0)
|
|
234
|
+
replacementIndex = Math.max(valueIndex, startOfSentenceIndex, indexInSentence, firstWordIndex, sentenceIndex, 0)
|
|
228
235
|
}
|
|
229
236
|
|
|
230
237
|
if (replacementIndex === -1) continue
|
|
231
238
|
if (hasBeenReplaced) continue
|
|
232
239
|
|
|
233
240
|
if (!replaceIfSafeInjection(element.innerHTML, injection.title, element, injectionElement, replacementIndex)) {
|
|
234
|
-
|
|
241
|
+
// If all else fails, we try one more time with a word boundary. If it has gotten to this point it likely means an
|
|
242
|
+
// injection tried to match into another that match partially. For example in the sentence:
|
|
243
|
+
// "Some Movie: The Sequel is a follow up to Some Movie", there are two matches for "Some Movie". But because the first
|
|
244
|
+
// match contains the second, the second will also try to inject itself into the first. In this case we try and find it
|
|
245
|
+
// again using a word boundary. This will fail in cases such as "Some Movie 2 is a follow up to Some Movie" because
|
|
246
|
+
// the word boundary still the first one.
|
|
247
|
+
const wordBoundaryMatchIndex = getIndexOfPhraseInBoundary(injection.title, element.innerHTML)
|
|
248
|
+
|
|
249
|
+
if (wordBoundaryMatchIndex > -1) {
|
|
250
|
+
replaceIfSafeInjection(element.innerHTML, injection.title, element, injectionElement, wordBoundaryMatchIndex)
|
|
251
|
+
} else {
|
|
252
|
+
failedMessages[injection.key] = 'Injection would have lead to broken HTML.'
|
|
253
|
+
}
|
|
235
254
|
}
|
|
236
255
|
}
|
|
237
256
|
|
package/src/lib/text.ts
CHANGED
|
@@ -263,3 +263,25 @@ export function getRangesOfTextNodesInElement(element: Element): Range[] {
|
|
|
263
263
|
|
|
264
264
|
return ranges
|
|
265
265
|
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Attempt to find a phrase inside a word boundary, only matching phrases between given characters.
|
|
269
|
+
*/
|
|
270
|
+
export function getIndexOfPhraseInBoundary(phrase: string, text: string): number {
|
|
271
|
+
const boundaryCharacters = [' ', '.', ',', '<', '>']
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
// This is a bit complicated looking, but this creates a regex for the given phrase
|
|
275
|
+
// between the word boundaries. ".phrase." " phrase.", ">phrase<", etc.
|
|
276
|
+
// In some cases the phrase might contain special characters, these are escaped first
|
|
277
|
+
// to make sure they don't interfere with the regex, or create totally invalid results.
|
|
278
|
+
const map = boundaryCharacters.map(c => '\\' + c).join('')
|
|
279
|
+
const escapedPhrase = phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
280
|
+
const pattern = new RegExp(`([${map}])${escapedPhrase}([${map}])`)
|
|
281
|
+
const match = text.match(pattern)
|
|
282
|
+
|
|
283
|
+
return match?.index ? match.index + 1 : -1
|
|
284
|
+
} catch {
|
|
285
|
+
return -1
|
|
286
|
+
}
|
|
287
|
+
}
|
|
@@ -568,6 +568,37 @@ describe('linkInjection.js', () => {
|
|
|
568
568
|
expect(links).toHaveLength(3)
|
|
569
569
|
})
|
|
570
570
|
|
|
571
|
+
it('Should inject link properly when sentence already contains match of the same phrase but the sentence is broken up by HTML very early in the sentence.', () => {
|
|
572
|
+
document.body.innerHTML = '<p>A <em><a>phrase</a></em> within a sentence. Another <strong>sentence</strong> with a <em>phrase</em> in it.</p>'
|
|
573
|
+
|
|
574
|
+
const elements = Array.from(document.querySelectorAll('p'))
|
|
575
|
+
|
|
576
|
+
const linkInjections = [generateInjection('Another sentence with a phrase in it.', 'phrase')]
|
|
577
|
+
|
|
578
|
+
injectLinksInDocument(elements, { aiInjections: linkInjections, manualInjections: [] })
|
|
579
|
+
|
|
580
|
+
const links = /** @type {HTMLAnchorElement[]} */ (Array.from(document.querySelectorAll('a')))
|
|
581
|
+
|
|
582
|
+
expect(links).toHaveLength(2)
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
it('Should inject link properly when sentence already contains partial match of the same phrase when the sentence is broken up by html.', () => {
|
|
586
|
+
document.body.innerHTML = '<p>Some sentence with a phrase: something, and another phrase.</p>'
|
|
587
|
+
|
|
588
|
+
const elements = Array.from(document.querySelectorAll('p'))
|
|
589
|
+
|
|
590
|
+
const linkInjections = [
|
|
591
|
+
generateInjection('Some sentence with a phrase: something, and another phrase.', 'phrase: something'),
|
|
592
|
+
generateInjection('Some sentence with a phrase: something, and another phrase.', 'phrase'),
|
|
593
|
+
]
|
|
594
|
+
|
|
595
|
+
injectLinksInDocument(elements, { aiInjections: linkInjections, manualInjections: [] })
|
|
596
|
+
|
|
597
|
+
const links = /** @type {HTMLAnchorElement[]} */ (Array.from(document.querySelectorAll('a')))
|
|
598
|
+
|
|
599
|
+
expect(links).toHaveLength(2)
|
|
600
|
+
})
|
|
601
|
+
|
|
571
602
|
it('Should not inject links into element attributes', () => {
|
|
572
603
|
document.body.innerHTML = '<p><span data-thing="phrase">Some</span> phrase</p>'
|
|
573
604
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
-
import { cleanPhrase, findAllMatchesBetweenPhrases, findShortestMatchBetweenPhrases, findSurroundingPhrases, findTextNodeContaining, getFirstNumberOfWordsInString, getIndexOfPhraseInElement, getNumberOfLeadingAndTrailingSpaces, getRangesOfTextNodesInElement, isNodeInLink, replaceStartingFrom, reverseString, truncateAroundPhrase } from '$lib/text'
|
|
2
|
+
import { cleanPhrase, findAllMatchesBetweenPhrases, findShortestMatchBetweenPhrases, findSurroundingPhrases, findTextNodeContaining, getFirstNumberOfWordsInString, getIndexOfPhraseInBoundary, getIndexOfPhraseInElement, getNumberOfLeadingAndTrailingSpaces, getRangesOfTextNodesInElement, isNodeInLink, replaceStartingFrom, reverseString, truncateAroundPhrase } from '$lib/text'
|
|
3
3
|
|
|
4
4
|
describe('text.js', () => {
|
|
5
5
|
beforeEach(() => {
|
|
@@ -428,4 +428,24 @@ describe('text.js', () => {
|
|
|
428
428
|
expect(ranges).toEqual([])
|
|
429
429
|
})
|
|
430
430
|
})
|
|
431
|
+
|
|
432
|
+
describe('getIndexOfPhraseInBoundary', () => {
|
|
433
|
+
const text = 'This is a sentence. <phrase> Another phrase.'
|
|
434
|
+
|
|
435
|
+
it('Should find a phrase surrounded by spaces', () => {
|
|
436
|
+
expect(getIndexOfPhraseInBoundary('is', text)).toBe(5)
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
it('Should find a phrase surrounded by brackets', () => {
|
|
440
|
+
expect(getIndexOfPhraseInBoundary('phrase', text)).toBe(21)
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
it('Should return -1 if phrase is not found', () => {
|
|
444
|
+
expect(getIndexOfPhraseInBoundary('nothing', text)).toBe(-1)
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
it('Should return the first match', () => {
|
|
448
|
+
expect(getIndexOfPhraseInBoundary('phrase', ' .phrase. <phrase> phrase ')).toBe(2)
|
|
449
|
+
})
|
|
450
|
+
})
|
|
431
451
|
})
|