@playpilot/tpi 3.8.1 → 4.0.0-beta.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 +8 -8
- package/package.json +1 -1
- package/src/lib/api.ts +2 -0
- package/src/lib/linkInjection.ts +78 -22
- package/src/lib/selection.ts +31 -0
- package/src/lib/text.ts +99 -0
- package/src/lib/types/injection.d.ts +2 -0
- package/src/routes/components/Editorial/ManualInjection.svelte +27 -7
- package/src/routes/components/Editorial/TextInput.svelte +1 -1
- package/src/routes/components/Title.svelte +1 -1
- package/src/tests/lib/linkInjection.test.js +54 -8
- package/src/tests/lib/text.test.js +220 -1
- package/src/tests/routes/components/Editorial/ManualInjection.test.js +15 -30
- package/src/tests/routes/components/Title.test.js +0 -9
package/package.json
CHANGED
package/src/lib/api.ts
CHANGED
|
@@ -105,6 +105,8 @@ export async function saveLinkInjections(linkInjections: LinkInjection[], pageTe
|
|
|
105
105
|
sid: linkInjection.sid,
|
|
106
106
|
title: linkInjection.title,
|
|
107
107
|
sentence: linkInjection.sentence,
|
|
108
|
+
phrase_before: linkInjection.phrase_before,
|
|
109
|
+
phrase_after: linkInjection.phrase_after,
|
|
108
110
|
playpilot_url: linkInjection.playpilot_url,
|
|
109
111
|
after_article: !!linkInjection.after_article,
|
|
110
112
|
after_article_style: linkInjection.after_article_style || null,
|
package/src/lib/linkInjection.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { mount, unmount } from 'svelte'
|
|
|
2
2
|
import TitleModal from '../routes/components/TitleModal.svelte'
|
|
3
3
|
import TitlePopover from '../routes/components/TitlePopover.svelte'
|
|
4
4
|
import AfterArticlePlaylinks from '../routes/components/AfterArticlePlaylinks.svelte'
|
|
5
|
-
import { cleanPhrase, findTextNodeContaining, isNodeInLink, replaceStartingFrom } from './text'
|
|
5
|
+
import { cleanPhrase, findNumberOfMatchesInString, findShortestMatchBetweenPhrases, findTextNodeContaining, getNumberOfLeadingAndTrailingSpaces, isNodeInLink, replaceBetween, replaceStartingFrom } from './text'
|
|
6
|
+
import { getLargestValueInArray } from './array'
|
|
6
7
|
import type { LinkInjection, LinkInjectionTypes } from './types/injection'
|
|
7
8
|
import { isHoldingSpecialKey } from './event'
|
|
8
9
|
import { playFallbackViewTransition } from './viewTransition'
|
|
@@ -138,32 +139,67 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
|
|
|
138
139
|
const linkNodeContainingText = findTextNodeContaining(injection.title, element)
|
|
139
140
|
if (linkNodeContainingText && isNodeInLink(linkNodeContainingText)) {
|
|
140
141
|
failedMessages[injection.key] = 'Given text is already inside of a link.'
|
|
142
|
+
continue
|
|
141
143
|
}
|
|
142
|
-
|
|
143
|
-
continue
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
const { injectionElement, linkElement } = createLinkInjectionElement(injection)
|
|
147
|
+
|
|
148
|
+
let replacementIndex = -1
|
|
149
|
+
let hasBeenReplaced = false
|
|
150
|
+
|
|
151
|
+
// !! Option 1 - Simple replacements
|
|
152
|
+
// Check if there is only one occurance in the element, in which case the replacement is simple
|
|
153
|
+
const numberOfMatches = findNumberOfMatchesInString(cleanPhrase(element.innerText), cleanPhrase(injection.title))
|
|
154
|
+
if (numberOfMatches === 1) replacementIndex = element.innerHTML.indexOf(injection.title)
|
|
155
|
+
|
|
156
|
+
// !! Option 2 - Replace by phrase_before and phrase_after
|
|
157
|
+
// If multiple or no occurances were found, we use phrase_before and phrase_after to refine the search
|
|
158
|
+
if (replacementIndex === -1 && (injection.phrase_before || injection.phrase_after)) {
|
|
159
|
+
console.log(injection.phrase_before)
|
|
160
|
+
// The before and after phrase are combined to see if the sentence contains the match exactly.
|
|
161
|
+
// This is a fairly simple comparison that will fail on special characters, html tags, or inconsistencies
|
|
162
|
+
const fullPhrase = [injection.phrase_before, injection.title, injection.phrase_after].filter(Boolean).join(' ')
|
|
163
|
+
|
|
164
|
+
replacementIndex = element.innerHTML.indexOf(fullPhrase)
|
|
165
|
+
|
|
166
|
+
// If we reach this point the match wasn't straight forward and we need to replace whatever is between phrase_before and phrase_after fully.
|
|
167
|
+
// We insert the html here separately from below, where it's done with replacementIndex because we need to capture all html
|
|
168
|
+
// that may have been inside of the match.
|
|
169
|
+
if (replacementIndex === -1) {
|
|
170
|
+
const match = findShortestMatchBetweenPhrases(element.innerHTML, injection.title, injection.phrase_before || '', injection.phrase_after || '')
|
|
171
|
+
|
|
172
|
+
if (match) {
|
|
173
|
+
const { leadingSpaces, trailingSpaces } = getNumberOfLeadingAndTrailingSpaces(match.match)
|
|
174
|
+
|
|
175
|
+
linkElement.innerHTML = match.match.trim()
|
|
176
|
+
|
|
177
|
+
const startIndex = match.index + leadingSpaces
|
|
178
|
+
const endIndex = match.index + match.match.length - trailingSpaces
|
|
179
|
+
element.innerHTML = replaceBetween(element.innerHTML, injectionElement.outerHTML, startIndex, endIndex)
|
|
150
180
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
linkElement.rel = 'noopener nofollow noreferrer'
|
|
181
|
+
replacementIndex = match.index
|
|
182
|
+
hasBeenReplaced = true
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
157
186
|
|
|
158
|
-
|
|
187
|
+
// !! Option 3 - Replace by title only, taking previous injections into account
|
|
188
|
+
// If no occurances were found previously, we check the element from start to finish only checking for the title,
|
|
189
|
+
// without considering phrase_before and phrase_after
|
|
190
|
+
if (replacementIndex === -1 && nodeContainingText?.nodeValue) {
|
|
191
|
+
// Start searching for injection from either the value or the sentence. This prevents injecting into
|
|
192
|
+
// text in an element earlier than the sentence started. A element might contain many sentences, after all.
|
|
193
|
+
const valueIndex = element.innerHTML.indexOf(nodeContainingText.nodeValue)
|
|
194
|
+
const sentenceIndex = element.innerHTML.indexOf(injection.sentence)
|
|
159
195
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const valueIndex = element.innerHTML.indexOf(nodeContainingText.nodeValue)
|
|
163
|
-
const sentenceIndex = element.innerHTML.indexOf(injection.sentence)
|
|
164
|
-
const highestIndex = Math.max(valueIndex, sentenceIndex, 0)
|
|
196
|
+
replacementIndex = Math.max(valueIndex, sentenceIndex, 0)
|
|
197
|
+
}
|
|
165
198
|
|
|
166
|
-
|
|
199
|
+
if (replacementIndex === -1) continue
|
|
200
|
+
if (hasBeenReplaced) continue
|
|
201
|
+
|
|
202
|
+
element.innerHTML = replaceStartingFrom(element.innerHTML, injection.title, injectionElement.outerHTML, replacementIndex)
|
|
167
203
|
}
|
|
168
204
|
|
|
169
205
|
addLinkInjectionEventListeners(validInjections)
|
|
@@ -194,6 +230,23 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
|
|
|
194
230
|
})
|
|
195
231
|
}
|
|
196
232
|
|
|
233
|
+
function createLinkInjectionElement(injection: LinkInjection): { injectionElement: HTMLSpanElement, linkElement: HTMLAnchorElement } {
|
|
234
|
+
// Create a wrapper in which the link will be placed. This wrapper exists as a parent for the popover
|
|
235
|
+
// so that it is not directly inside of the link.
|
|
236
|
+
const injectionElement = document.createElement('span')
|
|
237
|
+
injectionElement.dataset.playpilotInjectionKey = injection.key
|
|
238
|
+
|
|
239
|
+
const linkElement = document.createElement('a')
|
|
240
|
+
linkElement.innerText = injection.title
|
|
241
|
+
linkElement.href = injection.playpilot_url
|
|
242
|
+
linkElement.target = '_blank'
|
|
243
|
+
linkElement.rel = 'noopener nofollow noreferrer'
|
|
244
|
+
|
|
245
|
+
injectionElement.insertAdjacentElement('beforeend', linkElement)
|
|
246
|
+
|
|
247
|
+
return { injectionElement, linkElement }
|
|
248
|
+
}
|
|
249
|
+
|
|
197
250
|
/**
|
|
198
251
|
* Add all used CSS variables to a data attribute. This data attribute is then used for selectors that for each
|
|
199
252
|
* individual CSS variable. This is done this way so that CSS variables are only set when they are used.
|
|
@@ -363,7 +416,7 @@ function clearAfterArticlePlaylinks(): void {
|
|
|
363
416
|
*/
|
|
364
417
|
export function clearLinkInjections(): void {
|
|
365
418
|
const elements = document.querySelectorAll(keySelector)
|
|
366
|
-
elements.forEach((element) => element.
|
|
419
|
+
elements.forEach((element) => clearLinkInjection(element.getAttribute(keyDataAttribute) || ''))
|
|
367
420
|
|
|
368
421
|
clearAfterArticlePlaylinks()
|
|
369
422
|
destroyLinkModal(false)
|
|
@@ -376,7 +429,10 @@ export function clearLinkInjections(): void {
|
|
|
376
429
|
*/
|
|
377
430
|
export function clearLinkInjection(key: string): void {
|
|
378
431
|
const element: HTMLAnchorElement | null = document.querySelector(`[${keyDataAttribute}="${key}"]`)
|
|
379
|
-
if (element)
|
|
432
|
+
if (!element) return
|
|
433
|
+
|
|
434
|
+
const linkContent = element.querySelector('a')?.innerHTML
|
|
435
|
+
element.outerHTML = linkContent || ''
|
|
380
436
|
}
|
|
381
437
|
|
|
382
438
|
/**
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the index of the selection. The returned index will be exclusing any leading or trailing spaces. We contain it to actual phrases.
|
|
3
|
+
*/
|
|
4
|
+
export function getIndexOfSelection(selection: Selection, node: Node | null = selection.anchorNode): { start: number, end: number } {
|
|
5
|
+
if (!selection.anchorNode || !node) return { start: -1, end: -1 }
|
|
6
|
+
|
|
7
|
+
const trimmedSelection = selection.toString().trim()
|
|
8
|
+
const preceedingSpaces = selection.toString().search(/\S/)
|
|
9
|
+
|
|
10
|
+
// If node is a text node we can simply use it's anchorOffset and focusOffset
|
|
11
|
+
if (selection.anchorNode === node && node.nodeType === Node.TEXT_NODE) {
|
|
12
|
+
const start = Math.min(selection.anchorOffset, selection.focusOffset) + preceedingSpaces
|
|
13
|
+
|
|
14
|
+
return { start, end: start + trimmedSelection.length }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// If node is an element (meaning the text alone was not enough for text, and we grabbed a parent with enough text content)
|
|
18
|
+
// Here we use the range to get the text before the selection and use it's length as the index.
|
|
19
|
+
const range = selection.getRangeAt(0)
|
|
20
|
+
const clonedRange = range.cloneRange()
|
|
21
|
+
|
|
22
|
+
clonedRange.selectNodeContents(node)
|
|
23
|
+
clonedRange.setEnd(range.startContainer, range.startOffset)
|
|
24
|
+
|
|
25
|
+
const fullTextBefore = clonedRange.toString()
|
|
26
|
+
|
|
27
|
+
const start = fullTextBefore.length + preceedingSpaces
|
|
28
|
+
const end = start + trimmedSelection.length
|
|
29
|
+
|
|
30
|
+
return { start, end }
|
|
31
|
+
}
|
package/src/lib/text.ts
CHANGED
|
@@ -49,6 +49,10 @@ export function replaceStartingFrom(text: string, search: string, replacement: s
|
|
|
49
49
|
return before + updatedAfter
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
export function replaceBetween(text: string, replacement: string, startIndex: number, endIndex: number): string {
|
|
53
|
+
return text.substring(0, startIndex) + replacement + text.substring(endIndex)
|
|
54
|
+
}
|
|
55
|
+
|
|
52
56
|
/**
|
|
53
57
|
* Returns a string for most consistent string comparisons, decoding Html symbols and removing spaces.
|
|
54
58
|
*/
|
|
@@ -79,3 +83,98 @@ export function truncateAroundPhrase(sentence: string, phrase: string, maxLength
|
|
|
79
83
|
|
|
80
84
|
return result
|
|
81
85
|
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Find phrases surrounding in a parentNode's text content around a given start and end index.
|
|
89
|
+
* For instance in "Some example text", if the startIndex and endIndex match the start and end of "example",
|
|
90
|
+
* the phrases "Some" and "text" will be returned.
|
|
91
|
+
*/
|
|
92
|
+
export function findSurroundingPhrases(parentNode: Node, startIndex: number, endIndex: number): { before: string, after: string } {
|
|
93
|
+
const sentenceNode = parentNode.nodeType === Node.TEXT_NODE && parentNode.parentNode ? parentNode.parentElement : (parentNode as HTMLElement)
|
|
94
|
+
if (!sentenceNode || !parentNode.textContent) return { before: '', after: '' }
|
|
95
|
+
|
|
96
|
+
// Include the index of where the text is contained, as parentNode might start later within the sentenceNode
|
|
97
|
+
const parentNodeStartIndex = sentenceNode.innerText.indexOf(parentNode.textContent)
|
|
98
|
+
|
|
99
|
+
const stringBefore = reverseString(sentenceNode.innerText.slice(0, startIndex + parentNodeStartIndex))
|
|
100
|
+
const stringAfter = sentenceNode.innerText.slice(endIndex + parentNodeStartIndex)
|
|
101
|
+
|
|
102
|
+
const firstPhraseBefore = reverseString(getFirstNumberOfWordsInString(stringBefore, 2))
|
|
103
|
+
const firstPhraseAfter = getFirstNumberOfWordsInString(stringAfter, 2)
|
|
104
|
+
|
|
105
|
+
return { before: firstPhraseBefore, after: firstPhraseAfter }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function reverseString(string: string): string {
|
|
109
|
+
return string.split('').reverse().join('')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get the first x words in a string. Only returning full words, but can contain special characters like &, commas, period, etc.
|
|
114
|
+
*/
|
|
115
|
+
export function getFirstNumberOfWordsInString(sentence: string, numberOfWords = 1): string {
|
|
116
|
+
const match = sentence.match(/[^\s]+/g)
|
|
117
|
+
return match ? match.slice(0, numberOfWords).join(' ').replace(/[.,!?;:]+$/u, '') : ''
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function findNumberOfMatchesInString(text: string, phrase: string): number {
|
|
121
|
+
let count = 0
|
|
122
|
+
let index = 0
|
|
123
|
+
|
|
124
|
+
while ((index = text.indexOf(phrase, index)) !== -1) {
|
|
125
|
+
count++
|
|
126
|
+
index += phrase.length
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return count
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Finds all matches between two phrases. For example "A sentence with a start and end phrase". Is "start" and "end" are given
|
|
134
|
+
* as the `start` and `end` properties, this will match on ` and `. This does not currently account for multiple occurences
|
|
135
|
+
* of the end phrase. For example "A sentence with a start and end phrase and another end" will only return 1 value, despite
|
|
136
|
+
* there being 2 end phrases with content between. While that might make sense, we don't currently need that as we always
|
|
137
|
+
* filter by the shortest length anyway.
|
|
138
|
+
*/
|
|
139
|
+
export function findAllMatchesBetweenPhrases(text: string, start: string, end: string): { match: string, index: number }[] {
|
|
140
|
+
const matches: { match: string; index: number }[] = []
|
|
141
|
+
|
|
142
|
+
let currentIndex = 0
|
|
143
|
+
while (currentIndex <= text.length) {
|
|
144
|
+
const startIndex = start ? text.indexOf(start, currentIndex) : currentIndex
|
|
145
|
+
|
|
146
|
+
if (startIndex === -1 && start) break
|
|
147
|
+
|
|
148
|
+
const matchStart = start ? startIndex + start.length : startIndex
|
|
149
|
+
const endIndex = end ? text.indexOf(end, matchStart) : text.length
|
|
150
|
+
|
|
151
|
+
if (end && endIndex === -1) break
|
|
152
|
+
|
|
153
|
+
const match = text.slice(matchStart, endIndex)
|
|
154
|
+
matches.push({ match, index: matchStart })
|
|
155
|
+
|
|
156
|
+
currentIndex = end ? endIndex + end.length : text.length + 1
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return matches
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function findShortestMatchBetweenPhrases(text: string, phrase: string, start: string, end: string): { match: string, index: number } | null {
|
|
163
|
+
const matches = findAllMatchesBetweenPhrases(text, start, end)
|
|
164
|
+
const matchesContainingPhrase = matches.filter(({ match }) => decodeHtmlEntities(match).includes(phrase))
|
|
165
|
+
|
|
166
|
+
if (!matchesContainingPhrase.length) return null
|
|
167
|
+
|
|
168
|
+
const shortestMatch = matchesContainingPhrase.reduce((shortest, current) =>
|
|
169
|
+
!shortest || current.match.length < shortest.match.length ? current : shortest,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return shortestMatch
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function getNumberOfLeadingAndTrailingSpaces(text: string): { leadingSpaces: number, trailingSpaces: number } {
|
|
176
|
+
return {
|
|
177
|
+
leadingSpaces: text.match(/^\s*/)?.[0].length || 0,
|
|
178
|
+
trailingSpaces: text.match(/\s*$/)?.[0].length || 0
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
import type { LinkInjection } from '$lib/types/injection'
|
|
12
12
|
import type { TitleData } from '$lib/types/title'
|
|
13
13
|
import { heading } from '$lib/actions/heading'
|
|
14
|
-
import { cleanPhrase } from '$lib/text'
|
|
14
|
+
import { findSurroundingPhrases, cleanPhrase } from '$lib/text'
|
|
15
|
+
import { getIndexOfSelection } from '$lib/selection'
|
|
15
16
|
|
|
16
17
|
interface Props {
|
|
17
18
|
pageText: string
|
|
@@ -25,6 +26,8 @@
|
|
|
25
26
|
let currentSelection = $state('')
|
|
26
27
|
let selectionSentence = $state('')
|
|
27
28
|
let selectedTitle: TitleData | null = $state(null)
|
|
29
|
+
let phraseAfter = $state('')
|
|
30
|
+
let phraseBefore = $state('')
|
|
28
31
|
let error = $state('')
|
|
29
32
|
let query = $state('')
|
|
30
33
|
|
|
@@ -49,13 +52,27 @@
|
|
|
49
52
|
// Check if the selection contains more than 1 element with non-empty content.
|
|
50
53
|
const fragment = selection.getRangeAt(0).cloneContents()
|
|
51
54
|
const selectionNodesWithContent = Array.from(fragment.childNodes).filter(n => n.textContent?.trim())
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
|
|
56
|
+
if (selectionNodesWithContent.find(node => node.nodeName === 'A' || (node as HTMLElement).querySelector?.('a'))) {
|
|
57
|
+
error = 'Selection is or contains a link. Injections can not be created around already existing links.'
|
|
54
58
|
return
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
query = currentSelection
|
|
58
|
-
|
|
62
|
+
|
|
63
|
+
const { node: parentNode, sentence } = findSentenceForSelection(selection, selectionText)
|
|
64
|
+
if (!parentNode) return
|
|
65
|
+
|
|
66
|
+
selectionSentence = sentence
|
|
67
|
+
|
|
68
|
+
const { start, end } = getIndexOfSelection(selection, parentNode)
|
|
69
|
+
|
|
70
|
+
;({ before: phraseBefore, after: phraseAfter } = findSurroundingPhrases(parentNode, start, end))
|
|
71
|
+
|
|
72
|
+
// Remove phrases if they were outside of the sentence. This can happen when a node contains more than
|
|
73
|
+
// one sentence and match was found before or after the sentence the selection was in.
|
|
74
|
+
if (!sentence.includes(phraseBefore)) phraseBefore = ''
|
|
75
|
+
if (!sentence.includes(phraseAfter)) phraseAfter = ''
|
|
59
76
|
|
|
60
77
|
const nodeContent = selection.getRangeAt(0).commonAncestorContainer.textContent
|
|
61
78
|
const documentTextContent = cleanPhrase(pageText)
|
|
@@ -67,7 +84,7 @@
|
|
|
67
84
|
/**
|
|
68
85
|
* Find the sentence that the given selected phrase is in. This is limited by the node that the text is in.
|
|
69
86
|
*/
|
|
70
|
-
function findSentenceForSelection(selection: Selection, selectionText: string): string {
|
|
87
|
+
function findSentenceForSelection(selection: Selection, selectionText: string): { node: Node | null, sentence: string } {
|
|
71
88
|
const range = selection.getRangeAt(0)
|
|
72
89
|
|
|
73
90
|
// Get the node the text is in. If the content of the node is very short we use the parent node instead.
|
|
@@ -78,7 +95,7 @@
|
|
|
78
95
|
node = node.parentNode
|
|
79
96
|
}
|
|
80
97
|
|
|
81
|
-
if (!node || !node.textContent) return ''
|
|
98
|
+
if (!node || !node.textContent) return { node: null, sentence: '' }
|
|
82
99
|
|
|
83
100
|
const fullText = node.textContent
|
|
84
101
|
|
|
@@ -92,8 +109,9 @@
|
|
|
92
109
|
|
|
93
110
|
const sentenceStart = before === -1 ? 0 : before + 1
|
|
94
111
|
const sentenceEnd = after
|
|
112
|
+
const sentence = fullText.slice(sentenceStart, sentenceEnd + 1).trim()
|
|
95
113
|
|
|
96
|
-
return
|
|
114
|
+
return { node, sentence }
|
|
97
115
|
}
|
|
98
116
|
|
|
99
117
|
/**
|
|
@@ -144,6 +162,8 @@
|
|
|
144
162
|
sid: selectedTitle.sid,
|
|
145
163
|
title: currentSelection,
|
|
146
164
|
sentence: selectionSentence,
|
|
165
|
+
phrase_before: phraseBefore,
|
|
166
|
+
phrase_after: phraseAfter,
|
|
147
167
|
playpilot_url: url,
|
|
148
168
|
key: generateInjectionKey(selectedTitle.sid),
|
|
149
169
|
title_details: selectedTitle,
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
let { value = $bindable(), label = '', name = '', readonly = false, oninput = () => null }: Props = $props()
|
|
11
11
|
</script>
|
|
12
12
|
|
|
13
|
-
<input type="text" bind:value {name} aria-label={label} placeholder={label} {readonly} {oninput} />
|
|
13
|
+
<input type="text" bind:value {name} aria-label={label} placeholder={label} {readonly} {oninput} autocomplete="off" />
|
|
14
14
|
|
|
15
15
|
<style lang="scss">
|
|
16
16
|
input {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
let useBackgroundFallback = $state(false)
|
|
22
22
|
</script>
|
|
23
23
|
|
|
24
|
-
<div class="content" class:small class:compact data-playpilot-link-injections-title
|
|
24
|
+
<div class="content" class:small class:compact data-playpilot-link-injections-title>
|
|
25
25
|
<div class="header">
|
|
26
26
|
{#if !compact}
|
|
27
27
|
<div class="top">
|
|
@@ -71,13 +71,9 @@ describe('linkInjection.js', () => {
|
|
|
71
71
|
|
|
72
72
|
expect(links[0].innerText).toBe(linkInjections[0].title)
|
|
73
73
|
expect(links[0].href).toBe(linkInjections[0].playpilot_url)
|
|
74
|
-
/** @ts-ignore It is defined */
|
|
75
|
-
expect(links[0].dataset.playpilotOriginalTitle).toBe(linkInjections[0].title_details.original_title)
|
|
76
74
|
|
|
77
75
|
expect(links[1].innerText).toBe(linkInjections[1].title)
|
|
78
76
|
expect(links[1].href).toBe(linkInjections[1].playpilot_url)
|
|
79
|
-
/** @ts-ignore It is defined */
|
|
80
|
-
expect(links[1].dataset.playpilotOriginalTitle).toBe(linkInjections[1].title_details.original_title)
|
|
81
77
|
})
|
|
82
78
|
|
|
83
79
|
it('Should ignore injections that are marked as inactive', () => {
|
|
@@ -618,11 +614,61 @@ describe('linkInjection.js', () => {
|
|
|
618
614
|
|
|
619
615
|
expect(document.querySelectorAll('a')).toHaveLength(4)
|
|
620
616
|
})
|
|
617
|
+
|
|
618
|
+
it('Should properly inject into titles that are split up by multiple elements when phrase_before and phrase_after are given', () => {
|
|
619
|
+
document.body.innerHTML = '<section>Some text with a <strong>phra</strong><strong>se</strong> in it</section>'
|
|
620
|
+
|
|
621
|
+
const elements = getLinkInjectionElements(/** @type {HTMLElement} */ (document.querySelector('section')))
|
|
622
|
+
const injection = generateInjection('Some text with a phrase in it', 'phrase')
|
|
623
|
+
injection.phrase_before = 'with a'
|
|
624
|
+
injection.phrase_after = 'in it'
|
|
625
|
+
|
|
626
|
+
injectLinksInDocument(elements, { aiInjections: [], manualInjections: [injection] })
|
|
627
|
+
|
|
628
|
+
expect(document.querySelector('a')?.innerText).toBe(injection.title)
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
it('Should properly inject into titles that are split up by multiple elements when only phrase_before is given', () => {
|
|
632
|
+
document.body.innerHTML = '<section>Some text with a <strong>phra</strong><strong>se</strong></section>'
|
|
633
|
+
|
|
634
|
+
const elements = getLinkInjectionElements(/** @type {HTMLElement} */ (document.querySelector('section')))
|
|
635
|
+
const injection = generateInjection('Some text with a phrase', 'phrase')
|
|
636
|
+
injection.phrase_before = 'with a'
|
|
637
|
+
|
|
638
|
+
injectLinksInDocument(elements, { aiInjections: [], manualInjections: [injection] })
|
|
639
|
+
|
|
640
|
+
expect(document.querySelector('a')?.innerText).toBe(injection.title)
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
it('Should properly inject into titles that are split up by multiple elements when only phrase_after is given', () => {
|
|
644
|
+
document.body.innerHTML = '<section><strong>phra</strong><strong>se</strong> in it</section>'
|
|
645
|
+
|
|
646
|
+
const elements = getLinkInjectionElements(/** @type {HTMLElement} */ (document.querySelector('section')))
|
|
647
|
+
const injection = generateInjection('phrase in it', 'phrase')
|
|
648
|
+
injection.phrase_after = 'in it'
|
|
649
|
+
|
|
650
|
+
injectLinksInDocument(elements, { aiInjections: [], manualInjections: [injection] })
|
|
651
|
+
|
|
652
|
+
expect(document.querySelector('a')?.innerText).toBe(injection.title)
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
it('Should properly inject into titles if phrase_before or phrase_after are nonsense, but title can be matched simply', () => {
|
|
656
|
+
document.body.innerHTML = '<section>Some text with a phrase in it</section>'
|
|
657
|
+
|
|
658
|
+
const elements = getLinkInjectionElements(/** @type {HTMLElement} */ (document.querySelector('section')))
|
|
659
|
+
const injection = generateInjection('Some text with a phrase in it', 'phrase')
|
|
660
|
+
injection.phrase_before = 'random'
|
|
661
|
+
injection.phrase_after = 'crap'
|
|
662
|
+
|
|
663
|
+
injectLinksInDocument(elements, { aiInjections: [], manualInjections: [injection] })
|
|
664
|
+
|
|
665
|
+
expect(document.querySelector('a')?.innerText).toBe(injection.title)
|
|
666
|
+
})
|
|
621
667
|
})
|
|
622
668
|
|
|
623
669
|
describe('clearLinkInjections', () => {
|
|
624
670
|
it('Should remove injected links from the page', () => {
|
|
625
|
-
document.body.innerHTML = '<p><
|
|
671
|
+
document.body.innerHTML = '<p><span data-playpilot-injection-key><a>Some link</a></span></p>'
|
|
626
672
|
clearLinkInjections()
|
|
627
673
|
|
|
628
674
|
expect(document.body.innerHTML).toBe('<p>Some link</p>')
|
|
@@ -638,17 +684,17 @@ describe('linkInjection.js', () => {
|
|
|
638
684
|
|
|
639
685
|
describe('clearLinkInjection', () => {
|
|
640
686
|
it('Should remove injected link from the page', () => {
|
|
641
|
-
document.body.innerHTML = '<p><
|
|
687
|
+
document.body.innerHTML = '<p><span data-playpilot-injection-key="a"><a>Some link</a></span></p>'
|
|
642
688
|
clearLinkInjection('a')
|
|
643
689
|
|
|
644
690
|
expect(document.body.innerHTML).toBe('<p>Some link</p>')
|
|
645
691
|
})
|
|
646
692
|
|
|
647
693
|
it('Should keep other keys intact', () => {
|
|
648
|
-
document.body.innerHTML = '<p><
|
|
694
|
+
document.body.innerHTML = '<p><span data-playpilot-injection-key="b"><a>Some link</a></span></p>'
|
|
649
695
|
clearLinkInjection('a')
|
|
650
696
|
|
|
651
|
-
expect(document.body.innerHTML).toBe('<p><
|
|
697
|
+
expect(document.body.innerHTML).toBe('<p><span data-playpilot-injection-key="b"><a>Some link</a></span></p>')
|
|
652
698
|
})
|
|
653
699
|
})
|
|
654
700
|
|