@playpilot/tpi 3.7.2 → 3.8.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/README.md +32 -38
- package/dist/link-injections.js +9 -9
- package/index.html +1 -1
- package/package.json +1 -1
- package/src/lib/linkInjection.ts +34 -28
- package/src/lib/session.ts +7 -7
- package/src/lib/types/config.d.ts +25 -0
- package/src/lib/types/global.d.ts +6 -9
- package/src/lib/types/script.d.ts +10 -0
- package/src/{main.js → main.ts} +3 -5
- package/src/routes/+layout.svelte +99 -47
- package/src/routes/+page.svelte +11 -7
- package/src/routes/components/Editorial/Editor.svelte +1 -4
- package/src/routes/components/Editorial/ManualInjection.svelte +3 -3
- package/src/routes/components/Modal.svelte +0 -3
- package/src/routes/components/Popover.svelte +0 -3
- package/src/tests/lib/linkInjection.test.js +70 -3
- package/src/tests/routes/+page.test.js +2 -12
- package/static/favicon.png +0 -0
package/index.html
CHANGED
package/package.json
CHANGED
package/src/lib/linkInjection.ts
CHANGED
|
@@ -3,8 +3,7 @@ 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
5
|
import { cleanPhrase, findTextNodeContaining, isNodeInLink, replaceStartingFrom } from './text'
|
|
6
|
-
import {
|
|
7
|
-
import type { LinkInjection, LinkInjectionTypes, LinkInjectionRanges } from './types/injection'
|
|
6
|
+
import type { LinkInjection, LinkInjectionTypes } from './types/injection'
|
|
8
7
|
import { isHoldingSpecialKey } from './event'
|
|
9
8
|
import { playFallbackViewTransition } from './viewTransition'
|
|
10
9
|
|
|
@@ -19,9 +18,11 @@ let activeModalInsertedComponent: object | null = null
|
|
|
19
18
|
/**
|
|
20
19
|
* Return a list of all valid text containing elements that may get injected into.
|
|
21
20
|
* This excludes duplicates, empty elements, links, buttons, and header tags.
|
|
22
|
-
*
|
|
21
|
+
*
|
|
22
|
+
* Elements can additionally be excluded via the excludeElementsSelector attribute.
|
|
23
|
+
* This will exclude any element that matches or is in that selector.
|
|
23
24
|
*/
|
|
24
|
-
export function getLinkInjectionElements(parentElement: HTMLElement): HTMLElement[] {
|
|
25
|
+
export function getLinkInjectionElements(parentElement: HTMLElement, excludeElementsSelector: string = ''): HTMLElement[] {
|
|
25
26
|
const validElements: HTMLElement[] = []
|
|
26
27
|
const remainingChildren = [parentElement]
|
|
27
28
|
|
|
@@ -29,11 +30,19 @@ export function getLinkInjectionElements(parentElement: HTMLElement): HTMLElemen
|
|
|
29
30
|
const element = remainingChildren.pop() as HTMLElement
|
|
30
31
|
|
|
31
32
|
if (validElements.includes(element)) continue
|
|
33
|
+
if (excludeElementsSelector && element.matches(excludeElementsSelector)) continue
|
|
32
34
|
|
|
33
35
|
// Ignore links, buttons, and headers
|
|
34
36
|
if (/^(A|BUTTON|SCRIPT|NOSCRIPT|STYLE|IFRAME|FIGCAPTION|TIME|H1)$/.test(element.tagName)) continue
|
|
35
37
|
|
|
36
|
-
//
|
|
38
|
+
// Ignore elements that are visibly hidden as they are likely only for screen readers.
|
|
39
|
+
// These elements can be hidden via display: none or via a tiny width or perhaps even a clip path.
|
|
40
|
+
// Checking by their offsetWidth seems like the surest way to ignore these elements.
|
|
41
|
+
// We continue regardless of whether this is true or not, as we'd otherwise loop through children
|
|
42
|
+
// which are also hidden because of their parent.
|
|
43
|
+
// We always do it when running tests, as offsetWidth can't be reliably tested.
|
|
44
|
+
if (process.env.NODE_ENV !== 'test' && element.offsetWidth <= 1) continue
|
|
45
|
+
|
|
37
46
|
const hasTextNode = Array.from(element.childNodes).some(
|
|
38
47
|
node => node.nodeType === Node.TEXT_NODE && node.nodeValue?.trim() !== '',
|
|
39
48
|
)
|
|
@@ -53,13 +62,18 @@ export function getLinkInjectionElements(parentElement: HTMLElement): HTMLElemen
|
|
|
53
62
|
continue
|
|
54
63
|
}
|
|
55
64
|
|
|
65
|
+
// Some wysiwyg editors wrap contents of a paragraph in `span` elements when they contain styling.
|
|
66
|
+
// This leads to broken up sentences which don't match correctly against the given sentence.
|
|
67
|
+
// `<p><span>Some text</span><span> <strong>with broken up</strong></span><span>elements</span></p>`
|
|
68
|
+
const isParagraphWithText = element.tagName === 'P' && !!element.textContent
|
|
69
|
+
|
|
56
70
|
// If this element has a text node we add it to the valid elements and stop there
|
|
57
|
-
|
|
58
|
-
if (hasTextNode) {
|
|
71
|
+
if (hasTextNode || isParagraphWithText) {
|
|
59
72
|
validElements.push(element)
|
|
60
73
|
continue
|
|
61
74
|
}
|
|
62
75
|
|
|
76
|
+
// Add all children of the current element to be checked in this same loop.
|
|
63
77
|
const children = Array.from(element.children) as HTMLElement[]
|
|
64
78
|
for (let i = children.length - 1; i >= 0; i--) {
|
|
65
79
|
remainingChildren.push(children[i] as HTMLElement)
|
|
@@ -107,7 +121,6 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
|
|
|
107
121
|
return fullText.includes(cleanPhrase(i.sentence))
|
|
108
122
|
})
|
|
109
123
|
|
|
110
|
-
const ranges: LinkInjectionRanges = {}
|
|
111
124
|
const failedMessages: Record<string, string> = {}
|
|
112
125
|
|
|
113
126
|
for (const injection of foundInjections) {
|
|
@@ -144,25 +157,13 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
|
|
|
144
157
|
|
|
145
158
|
linkWrapperElement.insertAdjacentElement('beforeend', linkElement)
|
|
146
159
|
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
// the actual start if it was partially matched with manual injections. If that martial match contains a phrase that also
|
|
150
|
-
// occurs in the full sentence before it, it would incorrectly match against that. If that happens to be a link already,
|
|
151
|
-
// it would break that link with newly inserted HTML.
|
|
152
|
-
const startingIndex = getLargestValueInArray(Object.values(ranges).filter(r => r.elementIndex === elementIndex).map(r => r.to))
|
|
160
|
+
// Start searching for injection from either the value or the sentence. This prevents injecting into
|
|
161
|
+
// text in an element earlier than the sentence started. A element might contain many sentences, after all.
|
|
153
162
|
const valueIndex = element.innerHTML.indexOf(nodeContainingText.nodeValue)
|
|
154
163
|
const sentenceIndex = element.innerHTML.indexOf(injection.sentence)
|
|
155
|
-
const highestIndex = Math.max(
|
|
164
|
+
const highestIndex = Math.max(valueIndex, sentenceIndex, 0)
|
|
156
165
|
|
|
157
166
|
element.innerHTML = replaceStartingFrom(element.innerHTML, injection.title, linkWrapperElement.outerHTML, highestIndex)
|
|
158
|
-
|
|
159
|
-
const from = element.innerHTML.indexOf(linkWrapperElement.outerHTML)
|
|
160
|
-
|
|
161
|
-
ranges[injection.key] = {
|
|
162
|
-
elementIndex,
|
|
163
|
-
from,
|
|
164
|
-
to: from + linkWrapperElement.outerHTML.length,
|
|
165
|
-
}
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
addLinkInjectionEventListeners(validInjections)
|
|
@@ -177,10 +178,11 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
|
|
|
177
178
|
|
|
178
179
|
const matchingElement = document.querySelector(`[${keyDataAttribute}="${injection.key}"]`)
|
|
179
180
|
const failed = isValidPlaylinkType(injection) && !injection.inactive && !injection.after_article && !matchingElement
|
|
181
|
+
const containsSentence = !!elements.find(element => cleanPhrase(element.innerText).includes(cleanPhrase(injection.sentence)))
|
|
180
182
|
const failedMessage =
|
|
181
183
|
!failed ? '' :
|
|
182
184
|
failedMessages[injection.key] ||
|
|
183
|
-
(!
|
|
185
|
+
(!containsSentence ? 'Given sentence was not found in the article.' : 'The link failed to inject for unknown reasons.')
|
|
184
186
|
|
|
185
187
|
return {
|
|
186
188
|
...injection,
|
|
@@ -265,8 +267,9 @@ function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
|
|
|
265
267
|
|
|
266
268
|
if (!injection) return
|
|
267
269
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
+
injectionElement.addEventListener('mouseenter', (event) => {
|
|
271
|
+
if (!activePopoverInsertedComponent) openLinkPopover(event, injection)
|
|
272
|
+
})
|
|
270
273
|
injectionElement.addEventListener('mouseleave', () => currentlyHoveredInjection = null)
|
|
271
274
|
})
|
|
272
275
|
}
|
|
@@ -281,7 +284,7 @@ function openLinkModal(event: MouseEvent, injection: LinkInjection): void {
|
|
|
281
284
|
|
|
282
285
|
event.preventDefault()
|
|
283
286
|
|
|
284
|
-
activeModalInsertedComponent = mount(TitleModal, { target:
|
|
287
|
+
activeModalInsertedComponent = mount(TitleModal, { target: getPlayPilotWrapperElement(), props: { title: injection.title_details!, onclose: destroyLinkModal } })
|
|
285
288
|
}
|
|
286
289
|
|
|
287
290
|
/**
|
|
@@ -311,7 +314,7 @@ function openLinkPopover(event: MouseEvent, injection: LinkInjection): void {
|
|
|
311
314
|
setTimeout(() => {
|
|
312
315
|
if (currentlyHoveredInjection !== target) return // User is no longer hovering this link
|
|
313
316
|
|
|
314
|
-
activePopoverInsertedComponent = mount(TitlePopover, { target:
|
|
317
|
+
activePopoverInsertedComponent = mount(TitlePopover, { target: getPlayPilotWrapperElement(), props: { event, title: injection.title_details! } })
|
|
315
318
|
}, 100)
|
|
316
319
|
}
|
|
317
320
|
|
|
@@ -451,3 +454,6 @@ export function isEquivalentInjection(injection1: LinkInjection, injection2: Lin
|
|
|
451
454
|
return injection1.title === injection2.title && cleanPhrase(injection1.sentence) === cleanPhrase(injection2.sentence)
|
|
452
455
|
}
|
|
453
456
|
|
|
457
|
+
function getPlayPilotWrapperElement(): Element {
|
|
458
|
+
return document.querySelector('[data-playpilot-link-injections]') || document.body
|
|
459
|
+
}
|
package/src/lib/session.ts
CHANGED
|
@@ -11,10 +11,10 @@ export const sessionPeriodMilliseconds = sessionPollPeriodMilliseconds * 2
|
|
|
11
11
|
* where we save the session.
|
|
12
12
|
* Along with that, we also return automation_enabled and injections_enabled as they are used to update the state of
|
|
13
13
|
* the current editing session.
|
|
14
|
-
* @param
|
|
14
|
+
* @param pageText Despite not being used, the request still requires a valid pageText string
|
|
15
15
|
*/
|
|
16
|
-
export async function fetchAsSession(
|
|
17
|
-
const { automation_enabled, injections_enabled, session_id, session_last_ping } = await fetchLinkInjections(
|
|
16
|
+
export async function fetchAsSession(pageText: string): Promise<SessionResponse> {
|
|
17
|
+
const { automation_enabled, injections_enabled, session_id, session_last_ping } = await fetchLinkInjections(pageText)
|
|
18
18
|
|
|
19
19
|
const isCurrentlyAllowToEdit = isAllowedToEdit(session_id || null, session_last_ping || null)
|
|
20
20
|
|
|
@@ -27,15 +27,15 @@ export async function fetchAsSession(html: string): Promise<SessionResponse> {
|
|
|
27
27
|
|
|
28
28
|
if (!isCurrentlyAllowToEdit) return response
|
|
29
29
|
|
|
30
|
-
return await saveCurrentSession(
|
|
30
|
+
return await saveCurrentSession(pageText)
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Save the current users session id as the currently active session. This is always completed regardless of if the user
|
|
35
35
|
* is the current owner of the session, so check that first if necessary!
|
|
36
|
-
* @param
|
|
36
|
+
* @param pageText Despite not being used, the request still requires a valid pageText string
|
|
37
37
|
*/
|
|
38
|
-
export async function saveCurrentSession(
|
|
38
|
+
export async function saveCurrentSession(pageText: string): Promise<SessionResponse> {
|
|
39
39
|
const sessionId = getSessionId()
|
|
40
40
|
const now = new Date(Date.now()).toISOString()
|
|
41
41
|
|
|
@@ -44,7 +44,7 @@ export async function saveCurrentSession(html: string): Promise<SessionResponse>
|
|
|
44
44
|
session_last_ping: now.toString(),
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const { automation_enabled, injections_enabled } = await fetchLinkInjections(
|
|
47
|
+
const { automation_enabled, injections_enabled } = await fetchLinkInjections(pageText, { params })
|
|
48
48
|
|
|
49
49
|
return {
|
|
50
50
|
automation_enabled,
|
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
export type ConfigResponse = {
|
|
2
|
+
/**
|
|
3
|
+
* Used to exclude URL matching a regex pattern. If a url matches, nothing is executed beyond fetching the config.
|
|
4
|
+
* The url should never be indexed.
|
|
5
|
+
*/
|
|
2
6
|
exclude_urls_pattern?: string
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Used to inject custom styling for the links, modal, and popover. All styling is done via CSS variables. The passed
|
|
10
|
+
* string should not contain a selector.
|
|
11
|
+
* For example:
|
|
12
|
+
* `--some-variable: some-value;`
|
|
13
|
+
*
|
|
14
|
+
* rather than:
|
|
15
|
+
* `body { --some-variable: some-value; }`
|
|
16
|
+
*/
|
|
3
17
|
custom_style?: string
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The primary selector for the entire article from which text will be selected. Should contain as little content as
|
|
21
|
+
* possible. Should match all pages. May contain multiple selectors like `.article, .listicle`.
|
|
22
|
+
*/
|
|
4
23
|
html_selector?: string
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Some pages contain elements that can't easily be excluded via the `html_selector`. These elements can instead be
|
|
27
|
+
* excluded via this property. May contain multiple selectors like `.footer, .ad`.
|
|
28
|
+
*/
|
|
29
|
+
exclude_elements_selector?: string
|
|
5
30
|
}
|
|
@@ -1,14 +1,11 @@
|
|
|
1
|
+
import type { ScriptConfig } from "./script"
|
|
2
|
+
|
|
1
3
|
declare global {
|
|
2
4
|
interface Window {
|
|
3
|
-
PlayPilotLinkInjections: {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
after_article_selector: string
|
|
8
|
-
after_article_insert_position: InsertPosition | ''
|
|
9
|
-
language: string | null
|
|
10
|
-
organization_sid: string | null
|
|
11
|
-
domain_sid: string | null
|
|
5
|
+
PlayPilotLinkInjections: ScriptConfig & {
|
|
6
|
+
app: any | null
|
|
7
|
+
initialize(config: ScriptConfig): void
|
|
8
|
+
destroy(): void
|
|
12
9
|
}
|
|
13
10
|
}
|
|
14
11
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type ScriptConfig = {
|
|
2
|
+
token: string
|
|
3
|
+
editorial_token?: string
|
|
4
|
+
organization_sid?: string | null,
|
|
5
|
+
domain_sid?: string | null,
|
|
6
|
+
selector?: string
|
|
7
|
+
after_article_selector?: string
|
|
8
|
+
after_article_insert_position?: InsertPosition | ''
|
|
9
|
+
language?: string | null
|
|
10
|
+
}
|
package/src/{main.js → main.ts}
RENAMED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
|
|
3
1
|
import { mount } from 'svelte'
|
|
4
2
|
import App from './routes/+page.svelte'
|
|
5
3
|
import { clearLinkInjections } from '$lib/linkInjection'
|
|
@@ -15,7 +13,7 @@ window.PlayPilotLinkInjections = {
|
|
|
15
13
|
domain_sid: null,
|
|
16
14
|
app: null,
|
|
17
15
|
|
|
18
|
-
initialize(config = { token: '', selector: '', after_article_selector: '', language: null, organization_sid: null, domain_sid: null, editorial_token: '' }) {
|
|
16
|
+
initialize(config = { token: '', selector: '', after_article_selector: '', after_article_insert_position: '', language: null, organization_sid: null, domain_sid: null, editorial_token: '' }): void {
|
|
19
17
|
if (!config.token) {
|
|
20
18
|
console.error('An API token is required.')
|
|
21
19
|
return
|
|
@@ -25,7 +23,7 @@ window.PlayPilotLinkInjections = {
|
|
|
25
23
|
this.editorial_token = config.editorial_token
|
|
26
24
|
this.selector = config.selector
|
|
27
25
|
this.after_article_selector = config.after_article_selector
|
|
28
|
-
this.after_article_insert_position = config.after_article_insert_position
|
|
26
|
+
this.after_article_insert_position = config.after_article_insert_position as InsertPosition
|
|
29
27
|
this.language = config.language
|
|
30
28
|
this.organization_sid = config.organization_sid
|
|
31
29
|
this.domain_sid = config.domain_sid
|
|
@@ -40,7 +38,7 @@ window.PlayPilotLinkInjections = {
|
|
|
40
38
|
this.app = mount(App, { target })
|
|
41
39
|
},
|
|
42
40
|
|
|
43
|
-
destroy() {
|
|
41
|
+
destroy(): void {
|
|
44
42
|
if (!this.app) return
|
|
45
43
|
|
|
46
44
|
this.app = null
|
|
@@ -1,75 +1,127 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { browser } from '$app/environment'
|
|
3
|
+
import { page } from '$app/state'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* This layout file is for development purposes only and will not be compiled with the final script.
|
|
6
|
+
* !! NOTE: This layout file is for development purposes only and will not be compiled with the final script.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
const { children } = $props()
|
|
9
10
|
|
|
10
|
-
// This is used to remove the scoped classes in Svelte in order to prevent articles
|
|
11
|
-
// from having to be re-generated between different HMR builds.
|
|
12
|
-
// This is purely for development.
|
|
13
|
-
const noClass = (node: HTMLElement) => {
|
|
14
|
-
node.classList.forEach(e => {if (e.startsWith('s-')) node.classList.remove(e)})
|
|
15
|
-
}
|
|
16
|
-
|
|
17
11
|
// This is normally given through window.PlayPilotLinkInjections.initialize({ token: 'some-token' })
|
|
18
12
|
// @ts-ignore
|
|
19
13
|
if (browser) window.PlayPilotLinkInjections = { token: 'ZoAL14yqzevMyQiwckbvyetOkeIUeEDN', selector: 'article' }
|
|
20
14
|
</script>
|
|
21
15
|
|
|
16
|
+
<title>PlayPilot Link Injections</title>
|
|
17
|
+
|
|
22
18
|
<meta property="article:modified_time" content="2025-05-16T20:00:00+00:00" />
|
|
23
19
|
|
|
24
|
-
<div>
|
|
25
|
-
{#
|
|
26
|
-
<
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
20
|
+
<div class="main">
|
|
21
|
+
{#if page.error}
|
|
22
|
+
<div class="error">
|
|
23
|
+
<h3>Error {page.status}: {page.error.message}</h3>
|
|
24
|
+
|
|
25
|
+
{#if page.status === 404}
|
|
26
|
+
<p>You probably meant to stay on the root. There are no other pages.</p>
|
|
27
|
+
<a href="/">Go home</a>
|
|
28
|
+
{/if}
|
|
29
|
+
</div>
|
|
30
|
+
{:else}
|
|
31
|
+
{#key Math.random()}
|
|
32
|
+
<article>
|
|
33
|
+
<header>
|
|
34
|
+
<h1>The main title for this article</h1>
|
|
35
|
+
<time datetime="14:00">1 hour ago</time>
|
|
36
|
+
</header>
|
|
37
|
+
|
|
38
|
+
<p>Following the success of John M. Chu's 2018 romantic-comedy Crazy Rich Asians, Quan was inspired to return to acting. He first scored a supporting role in the Netflix movie Finding 'Ohana, before securing a starring role in the absurdist comedy-drama Everything Everywhere all At Once. A critical and commercial success, the film earned $143 million against a budget of $14-25 million, and saw Quan win the Academy Award for Best Supporting Actor. Following his win, Quan struggled to choose projects he was satisfied with, passing on an action-comedy three times, before finally taking his first leading role in it, following advice from Spielberg.</p>
|
|
39
|
+
|
|
40
|
+
<h2>A smaller heading, possibly with an injection in it</h2>
|
|
41
|
+
<p>In an interview with Epire & Magazine, Quan reveals he quested starring in Love Hurts, which sees him Love Hurts in the leading role of a former assassin turned successful realtor, whose past returns when his brother attempts to hunt him down. The movie is in a similar vein to successful films such as The Long Kiss Goodnight and Nobody, and Quan discussed how he was reluctant to take the part due to his conditioned beliefs about how an action hero should look. But he reveals that he changed his mind following a meeting with Spielberg, who convinced him to do it.</p>
|
|
42
|
+
|
|
43
|
+
<h2>Different languages</h2>
|
|
44
|
+
<p><strong>Jason Momoa</strong> (”Aquaman”), <strong>Jack Black</strong> (”Nacho Libre”) och <strong>Jennifer Coolidge</strong> (”The White Lotus”) medverkar i den <strong>Jared Hess</strong>-regisserade (”Napolen Dynamite”) filmen. Filmen följer fyra utbölingar som via en magisk portal sugs in i en värld där allt är kubformat. För att komma hem igen måste de övervinna den färgstarka världen.</p>
|
|
45
|
+
<p>De tre ’Jurassic World’-film kunne have givet indtryk af, at der ikke skal meget til, før dinosaurer igen ville kunne dominere kloden. Men det har vist sig ikke at holde stik.</p>
|
|
46
|
+
<p>Het komt elk jaar voor dat films met torenhoge budgetten flink floppen aan de box-office, ongeacht de kwaliteit van de film. Dit is het geval bij een aantal films die dit jaar zijn uitgekomen. Denk aan Mickey 17, Black Bag en de onlangs uitgebrachte animatiefilm Elio. In 2015 was de superheldenfilm Fantastic Four een van de grootste flops.</p>
|
|
47
|
+
|
|
48
|
+
<h2>A matching link is already present</h2>
|
|
49
|
+
<p>Following their post-credits scene in <a href="/">John Wick</a>, in a new John Wick spinoff.</p>
|
|
50
|
+
|
|
51
|
+
<h2>Mixed and breaking elements <small>(DigitalSpy.com tends to do this)</small></h2>
|
|
52
|
+
<p>The following bold word is broken by <strong>multi</strong><strong>ple</strong> elements, but visually looks like one. An element might also only be par<em>tially</em> styled. Or a full title like "The Lord of the Rings: <em>The Two Towers</em>" might only have part styled.</p>
|
|
53
|
+
|
|
54
|
+
<h2>Paragraph are broken up by many elements <small>(Exact example from TimeOut.com)</small></h2>
|
|
55
|
+
<!-- Svelte removes empty spaces inside of elements, it seems. We want to this to match TimeOut.com exactly. -->
|
|
56
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
57
|
+
{@html '<p><span>Some of that dino-fatigue plays into the laborious opening stretches here. Big beasts shamble though the Big Apple and New Yorkers just beep their horns and grumble about the tails in their tailbacks. Happily, director Gareth Edwards (</span><em><span>The Creator, Godzilla</span></em><span>) OG screenwriter David Koepp (</span><em><span>Jurassic Park </span></em><span>and</span><em><span> The Lost World: Jurassic Park</span></em><span>) soon pivot back to where the humans-messing-with-nature premise works best: on a tropical island overrun with hungry prehistoric beasties.<br></span><span><br></span><span>That long-winded prelude does fill in the odd handy blank: to survive, the dinosaurs have mostly retreated to the jungles and seas near the equator, no-go areas for humans. There, a small group of adventurers must venture on the dime of Rupert Friend’s slimy exec. His pharma company is on the hunt for a potential cure for heart disease that’s carried in the blood of three dino species. Scarlett Johansson and Ali Mahershala’s guns for hire will provide security; Jonathan Bailey’s palaeontologist will point out the correct creatures to aim the dart gun at. </span></p>'}
|
|
58
|
+
|
|
59
|
+
<h2>Example for a list</h2>
|
|
60
|
+
<ul>
|
|
61
|
+
<li><strong>Winner:</strong> The Zone of Interest</li>
|
|
62
|
+
<li>Oppenheimer and Oppenheimer and Oppenheimer, and Oppenheimer</li>
|
|
63
|
+
<li>Past Lives</li>
|
|
64
|
+
<li>Anatomy of a Fall</li>
|
|
65
|
+
<li>Killers of the Flower Moon</li>
|
|
66
|
+
</ul>
|
|
67
|
+
</article>
|
|
68
|
+
|
|
69
|
+
{#if browser}
|
|
70
|
+
{@render children()}
|
|
71
|
+
{/if}
|
|
72
|
+
{/key}
|
|
73
|
+
{/if}
|
|
52
74
|
</div>
|
|
53
75
|
|
|
54
76
|
<style lang="scss">
|
|
55
|
-
|
|
77
|
+
:global(body) {
|
|
78
|
+
margin: 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.main {
|
|
82
|
+
min-height: 100vh;
|
|
83
|
+
padding: margin(2);
|
|
84
|
+
background: #111;
|
|
85
|
+
color: lightgray;
|
|
86
|
+
font-family: 'Georgia', serif;
|
|
87
|
+
font-size: clamp(1rem, 3vw, 18px);
|
|
88
|
+
line-height: 1.8;
|
|
89
|
+
|
|
56
90
|
article {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
91
|
+
margin: 0 auto;
|
|
92
|
+
max-width: 60rem;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
header {
|
|
96
|
+
margin: 3rem 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
h1 {
|
|
100
|
+
margin: 0;
|
|
101
|
+
font-size: clamp(2rem, 8vw, 3rem);
|
|
60
102
|
color: white;
|
|
61
|
-
|
|
62
|
-
line-height: 1.4em;
|
|
103
|
+
}
|
|
63
104
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
105
|
+
h2 {
|
|
106
|
+
margin-top: 3rem;
|
|
107
|
+
font-size: 1.5rem;
|
|
108
|
+
color: white;
|
|
67
109
|
|
|
68
|
-
|
|
69
|
-
color: hotpink;
|
|
110
|
+
small {
|
|
70
111
|
font-style: italic;
|
|
112
|
+
color: gray;
|
|
113
|
+
font-weight: normal;
|
|
71
114
|
}
|
|
72
115
|
}
|
|
116
|
+
|
|
117
|
+
time {
|
|
118
|
+
font-style: italic;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
:global(a) {
|
|
122
|
+
color: hotpink;
|
|
123
|
+
font-style: italic;
|
|
124
|
+
}
|
|
73
125
|
}
|
|
74
126
|
</style>
|
|
75
127
|
|
package/src/routes/+page.svelte
CHANGED
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
|
|
59
59
|
if (config?.custom_style) insertCustomStyle(config.custom_style || '')
|
|
60
60
|
|
|
61
|
-
setElements(config?.html_selector || '')
|
|
61
|
+
setElements(config?.html_selector || '', config?.exclude_elements_selector || '')
|
|
62
62
|
} catch(error) {
|
|
63
63
|
// We also return if the config did not get fetched properly, as we can't determine what should and should
|
|
64
64
|
// get injected without it.
|
|
@@ -125,33 +125,34 @@
|
|
|
125
125
|
|
|
126
126
|
// Set elements to be used by script, if a selector is passed from the config request we update
|
|
127
127
|
// the selector on the window object.
|
|
128
|
-
|
|
128
|
+
// Additionally, a selector can be passed to exclude certain elements.
|
|
129
|
+
function setElements(configSelector: string, configExcludeElementsSelector: string): void {
|
|
129
130
|
if (configSelector) window.PlayPilotLinkInjections.selector = configSelector
|
|
130
131
|
|
|
131
132
|
parentElement = getLinkInjectionsParentElement()
|
|
132
|
-
elements = getLinkInjectionElements(parentElement)
|
|
133
|
+
elements = getLinkInjectionElements(parentElement, configExcludeElementsSelector)
|
|
133
134
|
}
|
|
134
135
|
|
|
135
|
-
function openEditorialMode() {
|
|
136
|
+
function openEditorialMode(): void {
|
|
136
137
|
isEditorialMode = true
|
|
137
138
|
setEditorialParamInUrl()
|
|
138
139
|
|
|
139
140
|
initialize()
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
function insertCustomStyle(customStyleString: string) {
|
|
143
|
+
function insertCustomStyle(customStyleString: string): void {
|
|
143
144
|
const id = 'playpilot-custom-style'
|
|
144
145
|
const existingElement = document.getElementById(id)
|
|
145
146
|
const styleElement = existingElement || document.createElement('style')
|
|
146
147
|
|
|
147
|
-
styleElement.textContent =
|
|
148
|
+
styleElement.textContent = `[data-playpilot-link-injections] { ${customStyleString} }`
|
|
148
149
|
styleElement.id = id
|
|
149
150
|
|
|
150
151
|
if (!existingElement) document.body.appendChild(styleElement)
|
|
151
152
|
}
|
|
152
153
|
</script>
|
|
153
154
|
|
|
154
|
-
<div class="playpilot-link-injections">
|
|
155
|
+
<div class="playpilot-link-injections" data-playpilot-link-injections>
|
|
155
156
|
{#if !isUrlExcluded && hasAuthToken && !isEditorialMode}
|
|
156
157
|
<EditorTrigger
|
|
157
158
|
onclick={openEditorialMode}
|
|
@@ -182,5 +183,8 @@
|
|
|
182
183
|
:global(*) {
|
|
183
184
|
box-sizing: border-box;
|
|
184
185
|
}
|
|
186
|
+
|
|
187
|
+
@include reset-svg();
|
|
188
|
+
@include global-outlines();
|
|
185
189
|
}
|
|
186
190
|
</style>
|
|
@@ -260,9 +260,6 @@
|
|
|
260
260
|
overflow-y: auto;
|
|
261
261
|
overflow-x: hidden;
|
|
262
262
|
line-height: normal;
|
|
263
|
-
|
|
264
|
-
@include reset-svg();
|
|
265
|
-
@include global-outlines();
|
|
266
263
|
}
|
|
267
264
|
|
|
268
265
|
.panel-open {
|
|
@@ -380,7 +377,7 @@
|
|
|
380
377
|
}
|
|
381
378
|
|
|
382
379
|
.alert {
|
|
383
|
-
margin:
|
|
380
|
+
margin: margin(0.5) margin(0.5) 0;
|
|
384
381
|
}
|
|
385
382
|
|
|
386
383
|
.panel {
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
import TitleSearch from './Search/TitleSearch.svelte'
|
|
8
8
|
import { playPilotBaseUrl } from '$lib/constants'
|
|
9
9
|
import { generateInjectionKey } from '$lib/api'
|
|
10
|
-
import { decodeHtmlEntities } from '$lib/html'
|
|
11
10
|
import { getLinkInjectionsParentElement } from '$lib/linkInjection'
|
|
12
11
|
import type { LinkInjection } from '$lib/types/injection'
|
|
13
12
|
import type { TitleData } from '$lib/types/title'
|
|
14
13
|
import { heading } from '$lib/actions/heading'
|
|
14
|
+
import { cleanPhrase } from '$lib/text'
|
|
15
15
|
|
|
16
16
|
interface Props {
|
|
17
17
|
pageText: string
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
selectionSentence = findSentenceForSelection(selection, selectionText)
|
|
59
59
|
|
|
60
60
|
const nodeContent = selection.getRangeAt(0).commonAncestorContainer.textContent
|
|
61
|
-
const documentTextContent =
|
|
62
|
-
if (!nodeContent || !documentTextContent.includes(nodeContent)) { // Selected content is not within the ALI selector
|
|
61
|
+
const documentTextContent = cleanPhrase(pageText)
|
|
62
|
+
if (!nodeContent || !documentTextContent.includes(cleanPhrase(nodeContent))) { // Selected content is not within the ALI selector
|
|
63
63
|
error = 'Selection was not inside of given content'
|
|
64
64
|
}
|
|
65
65
|
}
|
|
@@ -76,9 +76,6 @@
|
|
|
76
76
|
transform: translateY(calc(-100% + var(--offset)));
|
|
77
77
|
z-index: 2147483647; // As high as she goes
|
|
78
78
|
|
|
79
|
-
@include reset-svg();
|
|
80
|
-
@include global-outlines();
|
|
81
|
-
|
|
82
79
|
&.flip {
|
|
83
80
|
top: auto;
|
|
84
81
|
bottom: calc(var(--offset) + 1px); /* Add 1 pixel to account for rounding errors */
|