@playpilot/tpi 3.10.1 → 4.0.0
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 +10 -10
- package/package.json +1 -1
- package/src/lib/api.ts +2 -0
- package/src/lib/linkInjection.ts +79 -22
- package/src/lib/scss/global.scss +18 -6
- 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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
-
import { cleanPhrase, findTextNodeContaining, isNodeInLink, replaceStartingFrom, truncateAroundPhrase } from '$lib/text'
|
|
2
|
+
import { cleanPhrase, findAllMatchesBetweenPhrases, findShortestMatchBetweenPhrases, findSurroundingPhrases, findTextNodeContaining, getFirstNumberOfWordsInString, getNumberOfLeadingAndTrailingSpaces, isNodeInLink, replaceStartingFrom, reverseString, truncateAroundPhrase } from '$lib/text'
|
|
3
3
|
|
|
4
4
|
describe('text.js', () => {
|
|
5
5
|
beforeEach(() => {
|
|
@@ -127,4 +127,223 @@ describe('text.js', () => {
|
|
|
127
127
|
expect(truncateAroundPhrase('Some sentence with a word', 'sentence with a', 10)).toBe('…tence with…')
|
|
128
128
|
})
|
|
129
129
|
})
|
|
130
|
+
|
|
131
|
+
describe('getNumberOfLeadingAndTrailingSpaces', () => {
|
|
132
|
+
it('Should return the number of leading spaces and trailing spaces', () => {
|
|
133
|
+
expect(getNumberOfLeadingAndTrailingSpaces(' Some text ')).toEqual({ leadingSpaces: 2, trailingSpaces: 1 })
|
|
134
|
+
expect(getNumberOfLeadingAndTrailingSpaces(' Some text ')).toEqual({ leadingSpaces: 1, trailingSpaces: 3 })
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('Should return 0 for no leading or trailing spaces were given', () => {
|
|
138
|
+
expect(getNumberOfLeadingAndTrailingSpaces('Some text ')).toEqual({ leadingSpaces: 0, trailingSpaces: 1 })
|
|
139
|
+
expect(getNumberOfLeadingAndTrailingSpaces(' Some text')).toEqual({ leadingSpaces: 1, trailingSpaces: 0 })
|
|
140
|
+
expect(getNumberOfLeadingAndTrailingSpaces('Some text')).toEqual({ leadingSpaces: 0, trailingSpaces: 0 })
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('Should return 0 when given text is empty', () => {
|
|
144
|
+
expect(getNumberOfLeadingAndTrailingSpaces('')).toEqual({ leadingSpaces: 0, trailingSpaces: 0 })
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe('findAllMatchesBetweenPhrases', () => {
|
|
149
|
+
it('Should find a match between the given delimiters', () => {
|
|
150
|
+
expect(findAllMatchesBetweenPhrases('Some text with a match', 'text', 'match')).toEqual([
|
|
151
|
+
{ match: ' with a ', index: 9 },
|
|
152
|
+
])
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('Should find multiple matches between the given delimiters when multiple are present', () => {
|
|
156
|
+
expect(findAllMatchesBetweenPhrases('text Some match text with a match', 'text', 'match')).toEqual([
|
|
157
|
+
{ match: ' Some ', index: 4 },
|
|
158
|
+
{ match: ' with a ', index: 20 },
|
|
159
|
+
])
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('Should match from start of the string when no start string is given', () => {
|
|
163
|
+
expect(findAllMatchesBetweenPhrases('Some text with a match', '', 'with')).toEqual([
|
|
164
|
+
{ match: 'Some text ', index: 0 },
|
|
165
|
+
])
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('Should match to end of the string when no end string is given', () => {
|
|
169
|
+
expect(findAllMatchesBetweenPhrases('Some text with a match', 'with', '')).toEqual([
|
|
170
|
+
{ match: ' a match', index: 14 },
|
|
171
|
+
])
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('Should match from start to end of the string when no start and end string is given', () => {
|
|
175
|
+
expect(findAllMatchesBetweenPhrases('Some text with a match', '', '')).toEqual([
|
|
176
|
+
{ match: 'Some text with a match', index: 0 },
|
|
177
|
+
])
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
describe('findShortestMatchBetweenPhrases', () => {
|
|
182
|
+
it('Should return the shortest match', () => {
|
|
183
|
+
expect(findShortestMatchBetweenPhrases('A start with end and another start with another end', 'with', 'start', 'end')).toEqual({
|
|
184
|
+
match: ' with ',
|
|
185
|
+
index: 7,
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('Should correctly account for html elements', () => {
|
|
190
|
+
expect(findShortestMatchBetweenPhrases('A start <strong>with</strong> end and another start and end', 'with', 'start', 'end')).toEqual({
|
|
191
|
+
match: ' <strong>with</strong> ',
|
|
192
|
+
index: 7,
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('Should match phrase even if broken up by html', () => {
|
|
197
|
+
expect(findShortestMatchBetweenPhrases('A start wi<strong>th</strong> end and another start and end', 'with', 'start', 'end')).toEqual({
|
|
198
|
+
match: ' wi<strong>th</strong> ',
|
|
199
|
+
index: 7,
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('Should match phrase even if text contains incomplete html', () => {
|
|
204
|
+
expect(findShortestMatchBetweenPhrases('<a>A start with</strong> end <strong>and another start and end', 'with', 'start', 'end')).toEqual({
|
|
205
|
+
match: ' with</strong> ',
|
|
206
|
+
index: 10,
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('Should return null when no matches are found', () => {
|
|
211
|
+
expect(findShortestMatchBetweenPhrases('A string without matches', 'with', 'start', 'end')).toEqual(null)
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
describe('getFirstNumberOfWordsInString', () => {
|
|
216
|
+
it('Should return the first word in a given phrase, separated by spaces', () => {
|
|
217
|
+
expect(getFirstNumberOfWordsInString('Some sentence with words')).toBe('Some')
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('Should return the given number of words in a given phrase', () => {
|
|
221
|
+
expect(getFirstNumberOfWordsInString('Some sentence with words', 3)).toBe('Some sentence with')
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('Should return nothing if given string is empty', () => {
|
|
225
|
+
expect(getFirstNumberOfWordsInString('')).toBe('')
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('Should return word including special characters', () => {
|
|
229
|
+
expect(getFirstNumberOfWordsInString('("Some") sentence with words')).toBe('("Some")')
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('Should include special characters as words where relevant', () => {
|
|
233
|
+
expect(getFirstNumberOfWordsInString('Some & sentence with words', 2)).toBe('Some &')
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
describe('findSurroundingPhrases', () => {
|
|
238
|
+
it('Should return the first 2 words around the given indexes inside of a text node', () => {
|
|
239
|
+
document.body.innerHTML = 'A text with a phrase that can be matched'
|
|
240
|
+
const node = /** @type {Element} */ (document.querySelector('body')).childNodes[0]
|
|
241
|
+
|
|
242
|
+
expect(findSurroundingPhrases(node, 11, 20)).toEqual({
|
|
243
|
+
before: 'text with',
|
|
244
|
+
after: 'that can',
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('Should return the first 2 words around the given indexes inside of a html element', () => {
|
|
249
|
+
document.body.innerHTML = '<span>A text with a phrase that can be matched</span>'
|
|
250
|
+
const node = /** @type {Node} */ (document.querySelector('span'))
|
|
251
|
+
|
|
252
|
+
expect(findSurroundingPhrases(node, 11, 20)).toEqual({
|
|
253
|
+
before: 'text with',
|
|
254
|
+
after: 'that can',
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('Should return ignore html elements', () => {
|
|
259
|
+
document.body.innerHTML = '<span>A text <strong>with</strong> a <em>phrase</em> that can be matched</span>'
|
|
260
|
+
const node = /** @type {Node} */ (document.querySelector('span'))
|
|
261
|
+
|
|
262
|
+
expect(findSurroundingPhrases(node, 11, 20)).toEqual({
|
|
263
|
+
before: 'text with',
|
|
264
|
+
after: 'that can',
|
|
265
|
+
})
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('Should return ignore html elements on partial words', () => {
|
|
269
|
+
document.body.innerHTML = '<span>A text <strong>wi</strong>th a <em>phr</em><em>ase</em> that can be matched</span>'
|
|
270
|
+
const node = /** @type {Node} */ (document.querySelector('span'))
|
|
271
|
+
|
|
272
|
+
expect(findSurroundingPhrases(node, 11, 20)).toEqual({
|
|
273
|
+
before: 'text with',
|
|
274
|
+
after: 'that can',
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('Should return 1 word for before if start only contains 1 word', () => {
|
|
279
|
+
document.body.innerHTML = 'Text with a phrase that can be matched'
|
|
280
|
+
const node = /** @type {Element} */ (document.querySelector('body')).childNodes[0]
|
|
281
|
+
|
|
282
|
+
expect(findSurroundingPhrases(node, 4, 9)).toEqual({
|
|
283
|
+
before: 'Text',
|
|
284
|
+
after: 'a phrase',
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('Should return no words for before when starting at 0', () => {
|
|
289
|
+
document.body.innerHTML = 'A text with a phrase that can be matched'
|
|
290
|
+
const node = /** @type {Element} */ (document.querySelector('body')).childNodes[0]
|
|
291
|
+
|
|
292
|
+
expect(findSurroundingPhrases(node, 0, 6)).toEqual({
|
|
293
|
+
before: '',
|
|
294
|
+
after: 'with a',
|
|
295
|
+
})
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('Should return no words for before when no starting text is found', () => {
|
|
299
|
+
document.body.innerHTML = ' A text with a phrase that can be matched'
|
|
300
|
+
const node = /** @type {Element} */ (document.querySelector('body')).childNodes[0]
|
|
301
|
+
|
|
302
|
+
expect(findSurroundingPhrases(node, 3, 9)).toEqual({
|
|
303
|
+
before: '',
|
|
304
|
+
after: 'with a',
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it('Should return no words for after when end is at the end of the string', () => {
|
|
309
|
+
document.body.innerHTML = 'A text with a phrase that can be matched'
|
|
310
|
+
const node = /** @type {Element} */ (document.querySelector('body')).childNodes[0]
|
|
311
|
+
|
|
312
|
+
expect(findSurroundingPhrases(node, 30, /** @type {string} */ (node.textContent).length)).toEqual({
|
|
313
|
+
before: 'that can',
|
|
314
|
+
after: '',
|
|
315
|
+
})
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it('Should return no words for after when no after text is found', () => {
|
|
319
|
+
document.body.innerHTML = 'A text with a phrase that can be matched '
|
|
320
|
+
const node = /** @type {Element} */ (document.querySelector('body')).childNodes[0]
|
|
321
|
+
|
|
322
|
+
expect(findSurroundingPhrases(node, 30, /** @type {string} */ (node.textContent).length - 3)).toEqual({
|
|
323
|
+
before: 'that can',
|
|
324
|
+
after: '',
|
|
325
|
+
})
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('Should return 1 word for after if start only contains 1 word', () => {
|
|
329
|
+
document.body.innerHTML = 'Text with a phrase that can be matched'
|
|
330
|
+
const node = /** @type {Element} */ (document.querySelector('body')).childNodes[0]
|
|
331
|
+
|
|
332
|
+
expect(findSurroundingPhrases(node, 19, 30)).toEqual({
|
|
333
|
+
before: 'a phrase',
|
|
334
|
+
after: 'matched',
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
describe('reverseString', () => {
|
|
340
|
+
it('Should reverse a given string', () => {
|
|
341
|
+
expect(reverseString('abc')).toBe('cba')
|
|
342
|
+
expect(reverseString('abc 123')).toBe('321 cba')
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('Should handle empty strings by returning an empty string', () => {
|
|
346
|
+
expect(reverseString('')).toBe('')
|
|
347
|
+
})
|
|
348
|
+
})
|
|
130
349
|
})
|
|
@@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
4
4
|
import ManualInjection from '../../../../routes/components/Editorial/ManualInjection.svelte'
|
|
5
5
|
import { searchTitles } from '$lib/search'
|
|
6
6
|
import { title } from '$lib/fakeData'
|
|
7
|
+
import { getIndexOfSelection } from '$lib/selection'
|
|
7
8
|
|
|
8
9
|
function createSelectionMock() {
|
|
9
10
|
const container = document.querySelector('div')
|
|
@@ -27,14 +28,20 @@ vi.mock('$lib/search', () => ({
|
|
|
27
28
|
searchTitles: vi.fn(),
|
|
28
29
|
}))
|
|
29
30
|
|
|
31
|
+
vi.mock('$lib/selection', () => ({
|
|
32
|
+
getIndexOfSelection: vi.fn(),
|
|
33
|
+
}))
|
|
34
|
+
|
|
30
35
|
describe('ManualInjection.svelte', () => {
|
|
31
36
|
beforeEach(() => {
|
|
32
37
|
vi.resetAllMocks()
|
|
38
|
+
vi.mocked(getIndexOfSelection).mockReturnValueOnce({ start: 0, end: 0 })
|
|
33
39
|
|
|
34
40
|
document.body.innerHTML = '<div>Some text in a sentence</div>'
|
|
35
41
|
})
|
|
36
42
|
|
|
37
43
|
it('Should input selected text in selected text input and search input', async () => {
|
|
44
|
+
vi.mocked(getIndexOfSelection).mockReturnValueOnce({ start: 0, end: 0 })
|
|
38
45
|
createSelectionMock()
|
|
39
46
|
|
|
40
47
|
const { getByLabelText } = render(ManualInjection, { pageText: document.body.innerText, onsave: () => null })
|
|
@@ -58,6 +65,7 @@ describe('ManualInjection.svelte', () => {
|
|
|
58
65
|
createSelectionMock()
|
|
59
66
|
|
|
60
67
|
vi.mocked(searchTitles).mockResolvedValueOnce([title])
|
|
68
|
+
vi.mocked(getIndexOfSelection).mockReturnValueOnce({ start: 0, end: 0 })
|
|
61
69
|
|
|
62
70
|
const { getByText } = render(ManualInjection, { pageText: document.body.innerText, onsave: () => null })
|
|
63
71
|
|
|
@@ -72,6 +80,7 @@ describe('ManualInjection.svelte', () => {
|
|
|
72
80
|
createSelectionMock()
|
|
73
81
|
|
|
74
82
|
vi.mocked(searchTitles).mockResolvedValueOnce([title])
|
|
83
|
+
vi.mocked(getIndexOfSelection).mockReturnValueOnce({ start: 5, end: 10 })
|
|
75
84
|
|
|
76
85
|
const onsave = vi.fn()
|
|
77
86
|
const { getByText } = render(ManualInjection, { pageText: document.body.innerText, onsave })
|
|
@@ -88,11 +97,14 @@ describe('ManualInjection.svelte', () => {
|
|
|
88
97
|
key: expect.any(String),
|
|
89
98
|
title_details: title,
|
|
90
99
|
manual: true,
|
|
100
|
+
phrase_before: 'Some',
|
|
101
|
+
phrase_after: 'in a',
|
|
91
102
|
})
|
|
92
103
|
})
|
|
93
104
|
|
|
94
105
|
it('Should select content outside of element if content is too short', async () => {
|
|
95
106
|
vi.mocked(searchTitles).mockResolvedValueOnce([title])
|
|
107
|
+
vi.mocked(getIndexOfSelection).mockReturnValueOnce({ start: 0, end: 0 })
|
|
96
108
|
|
|
97
109
|
document.body.innerHTML = '<div>Some other sentence. Some <strong><span>text</span></strong> in a sentence</div>'
|
|
98
110
|
|
|
@@ -127,11 +139,14 @@ describe('ManualInjection.svelte', () => {
|
|
|
127
139
|
key: expect.any(String),
|
|
128
140
|
title_details: title,
|
|
129
141
|
manual: true,
|
|
142
|
+
phrase_before: '',
|
|
143
|
+
phrase_after: '',
|
|
130
144
|
})
|
|
131
145
|
})
|
|
132
146
|
|
|
133
147
|
it('Should not select content if it is outside of given parent', async () => {
|
|
134
148
|
vi.mocked(searchTitles).mockResolvedValueOnce([title])
|
|
149
|
+
vi.mocked(getIndexOfSelection).mockReturnValueOnce({ start: 0, end: 0 })
|
|
135
150
|
|
|
136
151
|
document.body.innerHTML = '<div>Some text <main><p>in a sentence</p></main></div>'
|
|
137
152
|
|
|
@@ -182,34 +197,4 @@ describe('ManualInjection.svelte', () => {
|
|
|
182
197
|
|
|
183
198
|
await waitFor(() => expect(searchTitles).toHaveBeenCalled())
|
|
184
199
|
})
|
|
185
|
-
|
|
186
|
-
it('Should not select content if it contains multiple child nodes with content', async () => {
|
|
187
|
-
document.body.innerHTML = '<main><div>Some text in a sentence</div></main>'
|
|
188
|
-
|
|
189
|
-
const container = document.querySelector('div')
|
|
190
|
-
const onsave = vi.fn()
|
|
191
|
-
const { getByText } = render(ManualInjection, { pageText: document.body.innerText, onsave })
|
|
192
|
-
|
|
193
|
-
// @ts-ignore
|
|
194
|
-
window.getSelection = vi.fn(() => ({
|
|
195
|
-
toString: () => 'Some text',
|
|
196
|
-
getRangeAt: () => ({
|
|
197
|
-
commonAncestorContainer: container,
|
|
198
|
-
startContainer: container,
|
|
199
|
-
startOffset: 0,
|
|
200
|
-
endOffset: 0,
|
|
201
|
-
cloneContents: () => ({ childNodes: [{ textContent: 'a' }, { textContent: 'b' }] }),
|
|
202
|
-
}),
|
|
203
|
-
anchorNode: container,
|
|
204
|
-
focusNode: container,
|
|
205
|
-
}))
|
|
206
|
-
|
|
207
|
-
await fireEvent.mouseUp(window)
|
|
208
|
-
|
|
209
|
-
expect(searchTitles).not.toHaveBeenCalled()
|
|
210
|
-
|
|
211
|
-
await waitFor(() => {
|
|
212
|
-
expect(getByText('Selection contains multiple items. Selection may not contain a mix of styled and non styled text. Please select the text more directly.')).toBeTruthy()
|
|
213
|
-
})
|
|
214
|
-
})
|
|
215
200
|
})
|
|
@@ -39,15 +39,6 @@ describe('Title.svelte', () => {
|
|
|
39
39
|
expect(getByText(title.year)).toBeTruthy()
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
-
it('Should include data attributes', () => {
|
|
43
|
-
const { container } = render(Title, { title })
|
|
44
|
-
|
|
45
|
-
const element = /** @type {HTMLElement} */ (container.querySelector('[data-playpilot-link-injections-title]'))
|
|
46
|
-
|
|
47
|
-
expect(element).toBeTruthy()
|
|
48
|
-
expect(element?.dataset.playpilotOriginalTitle).toBe(title.original_title)
|
|
49
|
-
})
|
|
50
|
-
|
|
51
42
|
it('Should not have small class by default', () => {
|
|
52
43
|
const { container } = render(Title, { title })
|
|
53
44
|
|