@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/index.html CHANGED
@@ -6,6 +6,6 @@
6
6
  </head>
7
7
  <body>
8
8
  <div id="app"></div>
9
- <script type="module" src="/src/main.js"></script>
9
+ <script type="module" src="/src/main.ts"></script>
10
10
  </body>
11
11
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "3.7.2",
3
+ "version": "3.8.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -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 { getLargestValueInArray } from './array'
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
- * @returns A list of all HTMLElements that contain text, without repeating the same text in
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
- // Check if this element has a direct text node
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
- // Otherwise we add all children to be checked in this same loop.
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
- // Replace starting from the end of the previously injected link, making sure injections with the same or overlapping
148
- // phrases can still be injected. Additionally we check the index of where the node value starts. This can differentiate from
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(startingIndex, valueIndex, sentenceIndex, 0)
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
- (!fullText.includes(cleanPhrase(injection.sentence)) ? 'Given sentence was not found in the article.' : 'The link failed to inject for unknown reasons.')
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
- // @ts-ignore
269
- injectionElement.addEventListener('mouseenter', (event) => openLinkPopover(event, injection))
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: document.body, props: { title: injection.title_details!, onclose: destroyLinkModal } })
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: document.body, props: { event, title: injection.title_details! } })
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
+ }
@@ -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 html Despite not being used, the request still requires a valid html string
14
+ * @param pageText Despite not being used, the request still requires a valid pageText string
15
15
  */
16
- export async function fetchAsSession(html: string): Promise<SessionResponse> {
17
- const { automation_enabled, injections_enabled, session_id, session_last_ping } = await fetchLinkInjections(html)
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(html)
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 html Despite not being used, the request still requires a valid html string
36
+ * @param pageText Despite not being used, the request still requires a valid pageText string
37
37
  */
38
- export async function saveCurrentSession(html: string): Promise<SessionResponse> {
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(html, { params })
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
- token: string
5
- editorial_token: string
6
- selector: string
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
+ }
@@ -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
- {#key Math.random()}
26
- <article use:noClass>
27
- <h1 use:noClass>Some heading</h1>
28
- <time datetime="14:00">1 hour ago</time>
29
- <p use:noClass>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>
30
- <p use:noClass>In an interview with Epire &amp; 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>
31
-
32
- <h2 use:noClass>A smaller heading with an injection in it</h2>
33
- <p use:noClass><strong use:noClass>Jason Momoa</strong> (”Aquaman”), <strong use:noClass>Jack Black</strong> (”Nacho Libre”) och <strong use:noClass>Jennifer Coolidge</strong> (”The White Lotus”) medverkar i den <strong use:noClass>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>
34
-
35
- <p use:noClass>
36
- Following their post-credits scene in <a use:noClass href="/">John Wick</a>, in a new John Wick spinoff.
37
- </p>
38
-
39
- <ul use:noClass>
40
- <li use:noClass><strong use:noClass>Winner:</strong> The Zone of Interest</li>
41
- <li use:noClass>Oppenheimer and Oppenheimer and Oppenheimer, and Oppenheimer</li>
42
- <li use:noClass>Past Lives</li>
43
- <li use:noClass>Anatomy of a Fall</li>
44
- <li use:noClass>Killers of the Flower Moon</li>
45
- </ul>
46
- </article>
47
-
48
- {#if browser}
49
- {@render children()}
50
- {/if}
51
- {/key}
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 &amp; 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.&nbsp;</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
- div :global {
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
- min-height: 100vh;
58
- padding: margin(2);
59
- background: #222;
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
- font-family: sans-serif;
62
- line-height: 1.4em;
103
+ }
63
104
 
64
- > :first-child {
65
- margin-top: 0;
66
- }
105
+ h2 {
106
+ margin-top: 3rem;
107
+ font-size: 1.5rem;
108
+ color: white;
67
109
 
68
- a {
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
 
@@ -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
- function setElements(configSelector: string) {
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 = `${window.PlayPilotLinkInjections?.selector || 'body'}, .modal, .popover { ${customStyleString} }`
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: 0 margin(0.5) margin(0.5);
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 = decodeHtmlEntities(pageText)
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
  }
@@ -64,9 +64,6 @@
64
64
  @media (min-width: 600px) {
65
65
  padding: margin(2);
66
66
  }
67
-
68
- @include reset-svg();
69
- @include global-outlines();
70
67
  }
71
68
 
72
69
 
@@ -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 */