@pmeweb/content 2.3.0 → 2.3.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/README.md +26 -9
- package/assets/embed/embed.css +39 -13
- package/dist/embeds/facade.d.ts +9 -7
- package/dist/embeds/facade.js +29 -12
- package/dist/embeds/integration.d.ts +5 -3
- package/dist/embeds/integration.js +2 -2
- package/dist/embeds/posters.d.ts +0 -1
- package/dist/embeds/posters.js +2 -4
- package/dist/embeds/providers.d.ts +8 -14
- package/dist/embeds/providers.js +2 -10
- package/dist/embeds/rehype.d.ts +3 -2
- package/dist/embeds/rehype.js +6 -4
- package/dist/embeds/scan.d.ts +0 -1
- package/dist/embeds/scan.js +2 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,9 @@ collections in `src/content.schema.json`; this package is what reads that declar
|
|
|
12
12
|
| `@pmeweb/content/collection` | `titleFieldError`, `thumbnailError`, `richErrors`, `collectionErrors`, `schemaErrors`, `richBodyField`, `hasThumbnails` | portal, `zodFrom` |
|
|
13
13
|
| `@pmeweb/content/published` | `getPublished`, the one filter every site's pages go through; `getPublishedItems` | sites |
|
|
14
14
|
| `@pmeweb/content/prose.css` | the structure of a row of images in a rich body | sites' `Prose.astro` |
|
|
15
|
+
| `@pmeweb/content/embeds` | the players' provider table and detector (browser-safe) | sites, portal editor |
|
|
16
|
+
| `@pmeweb/content/embeds/astro` | `rehypeEmbeds`, `embeds()` integration (Node) | sites' `astro.config.mjs` |
|
|
17
|
+
| `@pmeweb/content/EmbedPrivacy.astro` | the privacy page's paragraph per player provider | sites' privacy pages |
|
|
15
18
|
|
|
16
19
|
## Dated kinds
|
|
17
20
|
|
|
@@ -54,11 +57,12 @@ reads both once, `const APERCU = portalPreview(process.env)`, and filters with
|
|
|
54
57
|
drafts in the preview, removed ones only in a preview that asked for them. Only the exact
|
|
55
58
|
string `'1'` counts, and the past flag means nothing without the preview flag.
|
|
56
59
|
|
|
57
|
-
## Embeds (2.3.0)
|
|
60
|
+
## Embeds (2.3.0, 2.3.1)
|
|
58
61
|
|
|
59
62
|
A YouTube or SoundCloud link alone on its paragraph (`[words](url)`, no title attribute, words
|
|
60
63
|
that are not the URL itself, not in a list or a quote) becomes a click-to-load player: the
|
|
61
|
-
original link around a poster
|
|
64
|
+
original link around a poster (YouTube: the video's thumbnail, served by the site; SoundCloud: a
|
|
65
|
+
waveform drawn in the page), swapped for the provider's iframe on click.
|
|
62
66
|
Nothing loads from the provider before that click.
|
|
63
67
|
|
|
64
68
|
Two subpaths:
|
|
@@ -66,16 +70,16 @@ Two subpaths:
|
|
|
66
70
|
- `@pmeweb/content/embeds`: pure and browser-safe (the portal's editor imports it). The provider
|
|
67
71
|
table `PROVIDERS`, `embedFromUrl`, `embedFromLink`, `embedProviders(bodies)`, `embedFrameHosts`,
|
|
68
72
|
`supportedLabels()`.
|
|
69
|
-
- `@pmeweb/content/embeds/astro`: Node only. `rehypeEmbeds(
|
|
70
|
-
`embeds(
|
|
73
|
+
- `@pmeweb/content/embeds/astro`: Node only. `rehypeEmbeds()` (a satteri hast plugin),
|
|
74
|
+
`embeds()` (the Astro integration), `scanPages` / `selfCheck`.
|
|
71
75
|
|
|
72
76
|
A site with a `rich` body wires FOUR pieces:
|
|
73
77
|
|
|
74
|
-
1. `rehypeEmbeds(
|
|
78
|
+
1. `rehypeEmbeds()` through `satteri({ hastPlugins: [...] })` in
|
|
75
79
|
`astro.config.mjs`, AND through every `createSatteriMarkdownProcessor` the site calls itself.
|
|
76
|
-
2. `embeds(
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
2. `embeds()` in `integrations`. At build it writes `dist/_embed/player.js`, `embed.css` and the
|
|
81
|
+
YouTube posters (the thumbnail fetched at build and re-encoded to WebP, or a drawn dark poster
|
|
82
|
+
when the fetch fails), and FAILS the build when a player line was not converted
|
|
79
83
|
or the privacy page does not mention a provider in use.
|
|
80
84
|
3. `<EmbedPrivacy lang providers={embedProviders(bodies)} />` (`@pmeweb/content/EmbedPrivacy.astro`)
|
|
81
85
|
on every privacy page, `bodies` being the published posts' raw Markdown.
|
|
@@ -84,7 +88,20 @@ A site with a `rich` body wires FOUR pieces:
|
|
|
84
88
|
`astro dev` serves the script and the stylesheet but not the posters (they are build output):
|
|
85
89
|
the facade shows its dark background behind the title.
|
|
86
90
|
|
|
87
|
-
The look is the site's, through `--pmw-embed-radius
|
|
91
|
+
The look is the site's, through `--pmw-embed-radius`, `--pmw-embed-focus`, and for audio
|
|
92
|
+
`--pmw-embed-audio-bg`, `-line`, `-wave`, `-fg` and `-accent` (light by default, like SoundCloud's own
|
|
93
|
+
player). `embed.css` names element and class in every selector, so a site's `.prose img` or
|
|
94
|
+
`.prose a` (Astro-scoped, two classes and an element) does not reach inside the facade.
|
|
95
|
+
|
|
96
|
+
### 2.3.1
|
|
97
|
+
|
|
98
|
+
- The poster no longer takes the site's article image styles (margin, border, `height: auto`):
|
|
99
|
+
2.3.0 showed a band above it and cut its bottom off.
|
|
100
|
+
- SoundCloud's facade is drawn in the page (inline SVG), light and themeable, laid out like its
|
|
101
|
+
player; no image file for audio.
|
|
102
|
+
- No line under the player any more (« Lecture par … Politique de confidentialité »). The privacy
|
|
103
|
+
page still lists each provider. `rehypeEmbeds` and `embeds` take no options; 2.3.0's
|
|
104
|
+
`{ lang, privacyHref }` are accepted and ignored.
|
|
88
105
|
|
|
89
106
|
Adding a provider = one entry in `src/embeds/providers.ts` + its privacy text, approved by the
|
|
90
107
|
operator + a minor release. `prepublishOnly` refuses a privacy text still carrying `[À VALIDER`.
|
package/assets/embed/embed.css
CHANGED
|
@@ -1,14 +1,40 @@
|
|
|
1
1
|
/* @pmeweb/content/embeds -- the STRUCTURE of a player facade. The look is per site, through the
|
|
2
|
-
*
|
|
3
|
-
* hold a player.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
.pmw-embed__link img
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
.pmw-
|
|
12
|
-
.pmw-
|
|
13
|
-
.pmw-embed__frame {
|
|
14
|
-
.pmw-
|
|
2
|
+
* --pmw-embed-* variables. Written to dist/_embed/embed.css by the integration, linked only by pages that
|
|
3
|
+
* hold a player.
|
|
4
|
+
*
|
|
5
|
+
* EVERY SELECTOR NAMES THE ELEMENT AND ITS CLASS (figure.pmw-embed img.pmw-embed__poster: two
|
|
6
|
+
* classes, two elements). A site's article styles reach inside the facade -- Astro compiles
|
|
7
|
+
* `.prose :global(img)` to `.prose[data-astro-cid-…] img` (two classes, one element), which beat
|
|
8
|
+
* 2.3.0's `.pmw-embed__link img` and gave the poster the article's margin, border and
|
|
9
|
+
* `height: auto`: a band above the poster and its bottom cut off (test-contenu, 2026-11-05). So
|
|
10
|
+
* the poster and the link also reset every property an article style usually sets. */
|
|
11
|
+
figure.pmw-embed { margin: 1.5em 0; padding: 0; }
|
|
12
|
+
figure.pmw-embed a.pmw-embed__link { position: relative; display: block; overflow: hidden; margin: 0; padding: 0; border: 0; border-radius: var(--pmw-embed-radius, 8px); background: #1f2328; color: #fff; text-decoration: none; }
|
|
13
|
+
figure.pmw-embed--video a.pmw-embed__link, figure.pmw-embed--video iframe.pmw-embed__frame { width: 100%; aspect-ratio: 16 / 9; }
|
|
14
|
+
figure.pmw-embed--audio a.pmw-embed__link, figure.pmw-embed--audio iframe.pmw-embed__frame { width: 100%; height: 166px; }
|
|
15
|
+
figure.pmw-embed img.pmw-embed__poster { display: block; width: 100%; height: 100%; max-width: none; margin: 0; padding: 0; border: 0; border-radius: 0; box-shadow: none; object-fit: cover; }
|
|
16
|
+
figure.pmw-embed span.pmw-embed__play { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
|
|
17
|
+
figure.pmw-embed span.pmw-embed__title { position: absolute; left: 0; right: 0; bottom: 0; padding: 0.6em 1em; background: rgba(0, 0, 0, 0.78); color: #fff; font-weight: 600; line-height: 1.35; text-decoration: none; }
|
|
18
|
+
/* White ring inside a dark one: visible on a light page and on a dark one, whatever the site's
|
|
19
|
+
* link color. A site may set its own with --pmw-embed-focus. */
|
|
20
|
+
figure.pmw-embed a.pmw-embed__link:focus-visible { outline: 3px solid var(--pmw-embed-focus, #fff); outline-offset: 2px; box-shadow: 0 0 0 7px #1f2328; }
|
|
21
|
+
figure.pmw-embed--video a.pmw-embed__link:hover .pmw-embed__play path:first-child { fill: #c00; }
|
|
22
|
+
|
|
23
|
+
/* AUDIO: drawn in the page (facade.ts WAVE), so every color is the site's. Light by default, like
|
|
24
|
+
* SoundCloud's own player, which is what the click turns it into:
|
|
25
|
+
* --pmw-embed-audio-bg panel (#f2f2f2)
|
|
26
|
+
* --pmw-embed-audio-line panel border (#dcdcdc)
|
|
27
|
+
* --pmw-embed-audio-wave waveform bars (#c4c4c4)
|
|
28
|
+
* --pmw-embed-audio-fg title text (#1f2328)
|
|
29
|
+
* --pmw-embed-audio-accent play button (#ff5500, SoundCloud's orange) */
|
|
30
|
+
/* Laid out like that player too, on a grid rather than over the poster: the button on the left,
|
|
31
|
+
* the title on top, the waveform under it -- so a title wrapping to two lines on a phone pushes the
|
|
32
|
+
* waveform down instead of running over it. */
|
|
33
|
+
figure.pmw-embed--audio a.pmw-embed__link { display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto 1fr; column-gap: 1em; row-gap: 0.5em; padding: 1em; box-sizing: border-box; background: var(--pmw-embed-audio-bg, #f2f2f2); color: var(--pmw-embed-audio-fg, #1f2328); border: 1px solid var(--pmw-embed-audio-line, #dcdcdc); }
|
|
34
|
+
figure.pmw-embed--audio span.pmw-embed__play { position: static; grid-column: 1; grid-row: 1 / 3; align-self: center; }
|
|
35
|
+
figure.pmw-embed--audio span.pmw-embed__title { position: static; grid-column: 2; grid-row: 1; padding: 0; background: none; color: inherit; }
|
|
36
|
+
figure.pmw-embed--audio svg.pmw-embed__wave { grid-column: 2; grid-row: 2; display: block; width: 100%; height: 100%; min-height: 2em; }
|
|
37
|
+
figure.pmw-embed svg.pmw-embed__wave rect { fill: var(--pmw-embed-audio-wave, #c4c4c4); }
|
|
38
|
+
figure.pmw-embed span.pmw-embed__play--round circle { fill: var(--pmw-embed-audio-accent, #ff5500); }
|
|
39
|
+
figure.pmw-embed a.pmw-embed__link:hover span.pmw-embed__play--round svg { filter: brightness(0.9); }
|
|
40
|
+
figure.pmw-embed iframe.pmw-embed__frame { display: block; margin: 0; border: 0; border-radius: var(--pmw-embed-radius, 8px); }
|
package/dist/embeds/facade.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ProviderKey } from './providers.js';
|
|
2
2
|
import type { Embed } from './detect.js';
|
|
3
|
-
export interface FacadeOptions {
|
|
4
|
-
lang: Lang;
|
|
5
|
-
privacyHref: string;
|
|
6
|
-
}
|
|
7
3
|
/** The two files a document with a player loads, once per document. */
|
|
8
4
|
export declare const ASSET_TAGS = "<link rel=\"stylesheet\" href=\"/_embed/embed.css\"><script type=\"module\" src=\"/_embed/player.js\"></script>";
|
|
9
5
|
export declare const POSTER_WIDTHS: readonly [640, 960];
|
|
10
|
-
/** A
|
|
6
|
+
/** A thumbnail's file, one per video and width (posters.ts writes it). */
|
|
11
7
|
export declare function posterPath(provider: ProviderKey, id: string, width: number): string;
|
|
12
|
-
|
|
8
|
+
/**
|
|
9
|
+
* No words of the package's own under the player (2.3.0 wrote « Lecture par YouTube. Politique de
|
|
10
|
+
* confidentialité »; the operator removed it, 2026-11-05): the facade carries the client's title
|
|
11
|
+
* and nothing else, so it needs no language. The privacy page still names each provider
|
|
12
|
+
* (EmbedPrivacy.astro), and the build still refuses a site where it does not.
|
|
13
|
+
*/
|
|
14
|
+
export declare function facadeHtml(e: Embed): string;
|
package/dist/embeds/facade.js
CHANGED
|
@@ -1,26 +1,43 @@
|
|
|
1
|
-
// The HTML a player line becomes on the site: the ORIGINAL link around a poster served by
|
|
2
|
-
// so without JavaScript it is exactly today's link and
|
|
1
|
+
// The HTML a player line becomes on the site: the ORIGINAL link around a poster -- an image served by
|
|
2
|
+
// the site, or a waveform drawn in the page -- so without JavaScript it is exactly today's link and
|
|
3
|
+
// nothing third-party loads before a click.
|
|
3
4
|
// `player.js` (assets/embed/) swaps the link for the iframe named in `data-embed` on click.
|
|
4
|
-
import {
|
|
5
|
+
import { PROVIDERS } from './providers.js';
|
|
5
6
|
/** The two files a document with a player loads, once per document. */
|
|
6
7
|
export const ASSET_TAGS = '<link rel="stylesheet" href="/_embed/embed.css"><script type="module" src="/_embed/player.js"></script>';
|
|
7
8
|
export const POSTER_WIDTHS = [640, 960];
|
|
8
9
|
const esc = (s) => s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
9
|
-
/** A
|
|
10
|
+
/** A thumbnail's file, one per video and width (posters.ts writes it). */
|
|
10
11
|
export function posterPath(provider, id, width) {
|
|
11
|
-
return
|
|
12
|
-
? `/_embed/${provider}-drawn-${width}.webp`
|
|
13
|
-
: `/_embed/${provider}-${id}-${width}.webp`;
|
|
12
|
+
return `/_embed/${provider}-${id}-${width}.webp`;
|
|
14
13
|
}
|
|
15
14
|
const PLAY = '<svg viewBox="0 0 68 48" width="68" height="48" focusable="false"><path d="M66.5 7.7a8.5 8.5 0 0 0-6-6C55.2.3 34 .3 34 .3s-21.2 0-26.5 1.4a8.5 8.5 0 0 0-6 6C.1 13 .1 24 .1 24s0 11 1.4 16.3a8.5 8.5 0 0 0 6 6C12.8 47.7 34 47.7 34 47.7s21.2 0 26.5-1.4a8.5 8.5 0 0 0 6-6C67.9 35 67.9 24 67.9 24s0-11-1.4-16.3z" fill="rgba(0,0,0,.8)"/><path d="M27 14l18 10-18 10z" fill="#fff"/></svg>';
|
|
16
|
-
|
|
15
|
+
/** A round play button for audio: the rounded rectangle above is YouTube's own mark. */
|
|
16
|
+
const PLAY_ROUND = '<svg viewBox="0 0 56 56" width="56" height="56" focusable="false"><circle cx="28" cy="28" r="28"/><path d="M22 17l18 11-18 11z" fill="#fff"/></svg>';
|
|
17
|
+
/**
|
|
18
|
+
* An audio poster drawn IN THE PAGE (poster kind `inline`): a waveform whose colors are the site's
|
|
19
|
+
* (`--pmw-embed-audio-*`, embed.css), light by default like SoundCloud's own player, so the click
|
|
20
|
+
* does not flip a dark panel into a white one. Stretched to the box (preserveAspectRatio none).
|
|
21
|
+
*/
|
|
22
|
+
const WAVE = `<svg class="pmw-embed__wave" viewBox="0 0 640 100" preserveAspectRatio="none" aria-hidden="true" focusable="false">${Array.from({ length: 64 }, (_, i) => {
|
|
23
|
+
const h = Math.round((0.25 + 0.75 * Math.abs(Math.sin(i * 0.7))) * 56);
|
|
24
|
+
return `<rect x="${i * 10 + 2}" y="${50 - h / 2}" width="6" height="${h}" rx="3"/>`;
|
|
25
|
+
}).join('')}</svg>`;
|
|
26
|
+
/**
|
|
27
|
+
* No words of the package's own under the player (2.3.0 wrote « Lecture par YouTube. Politique de
|
|
28
|
+
* confidentialité »; the operator removed it, 2026-11-05): the facade carries the client's title
|
|
29
|
+
* and nothing else, so it needs no language. The privacy page still names each provider
|
|
30
|
+
* (EmbedPrivacy.astro), and the build still refuses a site where it does not.
|
|
31
|
+
*/
|
|
32
|
+
export function facadeHtml(e) {
|
|
17
33
|
const p = PROVIDERS[e.provider];
|
|
18
|
-
const copy = p.copy[o.lang];
|
|
19
34
|
const [w1, w2] = POSTER_WIDTHS;
|
|
20
35
|
const height = p.shape === 'video' ? Math.round((w2 * 9) / 16) : 166;
|
|
21
|
-
const
|
|
36
|
+
const poster = p.poster.kind === 'inline' ? WAVE : `<img class="pmw-embed__poster" src="${posterPath(e.provider, e.id, w2)}" srcset="${posterPath(e.provider, e.id, w1)} ${w1}w, ${posterPath(e.provider, e.id, w2)} ${w2}w" sizes="(max-width: 720px) 100vw, 720px" width="${w2}" height="${height}" alt="" loading="lazy" decoding="async">`;
|
|
37
|
+
const play = p.shape === 'audio'
|
|
38
|
+
? `<span class="pmw-embed__play pmw-embed__play--round" aria-hidden="true">${PLAY_ROUND}</span>`
|
|
39
|
+
: `<span class="pmw-embed__play" aria-hidden="true">${PLAY}</span>`;
|
|
22
40
|
return `<figure class="pmw-embed pmw-embed--${p.shape}" data-provider="${e.provider}" data-embed="${esc(p.embed(e.id))}" data-title="${esc(e.title)}">`
|
|
23
|
-
+ `<a class="pmw-embed__link" href="${esc(e.url)}" target="_blank" rel="noopener">${
|
|
24
|
-
+ `<figcaption class="pmw-embed__note">${esc(copy.note)} <a href="${esc(o.privacyHref)}#${EMBED_PRIVACY_ID}">${esc(copy.noteLink)}</a></figcaption>`
|
|
41
|
+
+ `<a class="pmw-embed__link" href="${esc(e.url)}" target="_blank" rel="noopener">${poster}${play}<span class="pmw-embed__title">${esc(e.title)}</span></a>`
|
|
25
42
|
+ '</figure>';
|
|
26
43
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { AstroIntegration } from 'astro';
|
|
2
|
-
|
|
3
|
-
export interface EmbedsOptions
|
|
2
|
+
/** 2.3.0's `lang` and `privacyHref` are accepted and ignored (the line under the player is gone). */
|
|
3
|
+
export interface EmbedsOptions {
|
|
4
|
+
lang?: string;
|
|
5
|
+
privacyHref?: string;
|
|
4
6
|
/** For tests; the build uses the global fetch. */
|
|
5
7
|
fetch?: (url: string) => Promise<Response>;
|
|
6
8
|
}
|
|
7
|
-
export declare function embeds(o
|
|
9
|
+
export declare function embeds(o?: EmbedsOptions): AstroIntegration;
|
|
@@ -18,7 +18,7 @@ function* htmlPages(root, rel = '') {
|
|
|
18
18
|
yield { path: p, html: readFileSync(join(root, p), 'utf8') };
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
-
export function embeds(o) {
|
|
21
|
+
export function embeds(o = {}) {
|
|
22
22
|
return {
|
|
23
23
|
name: '@pmeweb/content/embeds',
|
|
24
24
|
hooks: {
|
|
@@ -44,7 +44,7 @@ export function embeds(o) {
|
|
|
44
44
|
copyFileSync(join(ASSETS, 'player.js'), join(out, 'player.js'));
|
|
45
45
|
copyFileSync(join(ASSETS, 'embed.css'), join(out, 'embed.css'));
|
|
46
46
|
await writePosters({
|
|
47
|
-
dir: out, thumbnails: scan.thumbnails,
|
|
47
|
+
dir: out, thumbnails: scan.thumbnails,
|
|
48
48
|
fetch: o.fetch ?? ((url) => fetch(url)), warn: (m) => logger.warn(m),
|
|
49
49
|
});
|
|
50
50
|
},
|
package/dist/embeds/posters.d.ts
CHANGED
package/dist/embeds/posters.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// The posters, written into dist/_embed/ at build
|
|
1
|
+
// The thumbnail posters, written into dist/_embed/ at build (an `inline` poster is drawn in the page,
|
|
2
|
+
// facade.ts, and has no file). A thumbnail is fetched from the provider HERE,
|
|
2
3
|
// never by the visitor's browser, re-encoded to WebP and cropped to 16:9 (hqdefault is 4:3 with
|
|
3
4
|
// black bars). Any failure writes the drawn poster under the same names: a poster never fails a
|
|
4
5
|
// build. The preview builder may have no outbound network [UNVERIFIED]; it then shows the drawn
|
|
@@ -50,9 +51,6 @@ async function write(dir, provider, id, source) {
|
|
|
50
51
|
}
|
|
51
52
|
export async function writePosters(job) {
|
|
52
53
|
mkdirSync(job.dir, { recursive: true });
|
|
53
|
-
for (const provider of job.drawn) {
|
|
54
|
-
await write(job.dir, provider, 'drawn', Buffer.from(drawnSvg(PROVIDERS[provider].shape)));
|
|
55
|
-
}
|
|
56
54
|
for (const { provider, id } of job.thumbnails) {
|
|
57
55
|
const poster = PROVIDERS[provider].poster;
|
|
58
56
|
const got = poster.kind === 'thumbnail' ? await fetchFirst(poster.urls(id), job.fetch) : null;
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
export type Lang = 'fr' | 'en';
|
|
2
2
|
export interface ProviderCopy {
|
|
3
|
-
/** The line under the poster: `note` then a link whose words are `noteLink`. */
|
|
4
|
-
note: string;
|
|
5
|
-
noteLink: string;
|
|
6
3
|
/** The privacy page's paragraph, then a link to the provider's own policy. */
|
|
7
4
|
privacy: string;
|
|
8
5
|
policyLabel: string;
|
|
9
6
|
policyHref: string;
|
|
10
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* What the facade shows before the click. `thumbnail`: the provider's image, fetched at build and
|
|
10
|
+
* served by the site (posters.ts). `inline`: drawn in the page itself (facade.ts), so its colors
|
|
11
|
+
* are the site's CSS variables -- a provider with no thumbnail to fetch.
|
|
12
|
+
*/
|
|
11
13
|
export type Poster = {
|
|
12
14
|
kind: 'thumbnail';
|
|
13
15
|
urls(id: string): string[];
|
|
14
16
|
} | {
|
|
15
|
-
kind: '
|
|
17
|
+
kind: 'inline';
|
|
16
18
|
};
|
|
17
19
|
export interface Provider {
|
|
18
20
|
label: string;
|
|
@@ -41,15 +43,11 @@ export declare const PROVIDERS: {
|
|
|
41
43
|
readonly oembed: "https://www.youtube.com/oembed";
|
|
42
44
|
readonly copy: {
|
|
43
45
|
readonly fr: {
|
|
44
|
-
readonly note: "Lecture par YouTube.";
|
|
45
|
-
readonly noteLink: "Politique de confidentialité";
|
|
46
46
|
readonly privacy: "Certains articles intègrent des vidéos YouTube (Google). Rien n’est chargé depuis YouTube tant que vous n’appuyez pas sur le bouton de lecture : l’image d’aperçu est servie par ce site. Dès que vous lancez une vidéo, elle est lue depuis youtube-nocookie.com, et YouTube peut alors recueillir des données et déposer des témoins selon sa propre politique.";
|
|
47
47
|
readonly policyLabel: "Politique de confidentialité de Google";
|
|
48
48
|
readonly policyHref: "https://policies.google.com/privacy?hl=fr";
|
|
49
49
|
};
|
|
50
50
|
readonly en: {
|
|
51
|
-
readonly note: "Plays from YouTube.";
|
|
52
|
-
readonly noteLink: "Privacy policy";
|
|
53
51
|
readonly privacy: "Some articles embed YouTube (Google) videos. Nothing loads from YouTube until you press play: the preview image is served by this site. Once you start a video, it plays from youtube-nocookie.com, and YouTube may then collect data and set cookies under its own policy.";
|
|
54
52
|
readonly policyLabel: "Google’s privacy policy";
|
|
55
53
|
readonly policyHref: "https://policies.google.com/privacy?hl=en";
|
|
@@ -62,21 +60,17 @@ export declare const PROVIDERS: {
|
|
|
62
60
|
readonly embed: (id: string) => string;
|
|
63
61
|
readonly frameHost: "https://w.soundcloud.com";
|
|
64
62
|
readonly poster: {
|
|
65
|
-
readonly kind: "
|
|
63
|
+
readonly kind: "inline";
|
|
66
64
|
};
|
|
67
65
|
readonly shape: "audio";
|
|
68
66
|
readonly oembed: "https://soundcloud.com/oembed";
|
|
69
67
|
readonly copy: {
|
|
70
68
|
readonly fr: {
|
|
71
|
-
readonly note: "Lecture par SoundCloud.";
|
|
72
|
-
readonly noteLink: "Politique de confidentialité";
|
|
73
69
|
readonly privacy: "Certains articles intègrent des enregistrements SoundCloud. Rien n’est chargé depuis SoundCloud tant que vous n’appuyez pas sur le bouton de lecture. Dès que vous lancez un enregistrement, il est lu depuis w.soundcloud.com, et SoundCloud peut alors recueillir des données et déposer des témoins selon sa propre politique.";
|
|
74
70
|
readonly policyLabel: "Politique de confidentialité de SoundCloud";
|
|
75
71
|
readonly policyHref: "https://soundcloud.com/pages/privacy";
|
|
76
72
|
};
|
|
77
73
|
readonly en: {
|
|
78
|
-
readonly note: "Plays from SoundCloud.";
|
|
79
|
-
readonly noteLink: "Privacy policy";
|
|
80
74
|
readonly privacy: "Some articles embed SoundCloud recordings. Nothing loads from SoundCloud until you press play. Once you start a recording, it plays from w.soundcloud.com, and SoundCloud may then collect data and set cookies under its own policy.";
|
|
81
75
|
readonly policyLabel: "SoundCloud’s privacy policy";
|
|
82
76
|
readonly policyHref: "https://soundcloud.com/pages/privacy";
|
|
@@ -89,6 +83,6 @@ export type ProviderKey = keyof typeof PROVIDERS;
|
|
|
89
83
|
export declare const PROVIDER_KEYS: ProviderKey[];
|
|
90
84
|
/** The privacy section's heading, one per language, shared by every provider. */
|
|
91
85
|
export declare const EMBED_PRIVACY_HEADING: Record<Lang, string>;
|
|
92
|
-
/** The privacy section's anchor,
|
|
86
|
+
/** The privacy section's anchor (`EmbedPrivacy.astro`), for a site that links to it. */
|
|
93
87
|
export declare const EMBED_PRIVACY_ID = "lecteurs-integres";
|
|
94
88
|
export {};
|
package/dist/embeds/providers.js
CHANGED
|
@@ -43,15 +43,11 @@ export const PROVIDERS = {
|
|
|
43
43
|
oembed: 'https://www.youtube.com/oembed',
|
|
44
44
|
copy: {
|
|
45
45
|
fr: {
|
|
46
|
-
note: 'Lecture par YouTube.',
|
|
47
|
-
noteLink: 'Politique de confidentialité',
|
|
48
46
|
privacy: 'Certains articles intègrent des vidéos YouTube (Google). Rien n’est chargé depuis YouTube tant que vous n’appuyez pas sur le bouton de lecture : l’image d’aperçu est servie par ce site. Dès que vous lancez une vidéo, elle est lue depuis youtube-nocookie.com, et YouTube peut alors recueillir des données et déposer des témoins selon sa propre politique.',
|
|
49
47
|
policyLabel: 'Politique de confidentialité de Google',
|
|
50
48
|
policyHref: 'https://policies.google.com/privacy?hl=fr',
|
|
51
49
|
},
|
|
52
50
|
en: {
|
|
53
|
-
note: 'Plays from YouTube.',
|
|
54
|
-
noteLink: 'Privacy policy',
|
|
55
51
|
privacy: 'Some articles embed YouTube (Google) videos. Nothing loads from YouTube until you press play: the preview image is served by this site. Once you start a video, it plays from youtube-nocookie.com, and YouTube may then collect data and set cookies under its own policy.',
|
|
56
52
|
policyLabel: 'Google’s privacy policy',
|
|
57
53
|
policyHref: 'https://policies.google.com/privacy?hl=en',
|
|
@@ -63,20 +59,16 @@ export const PROVIDERS = {
|
|
|
63
59
|
id: soundcloudId,
|
|
64
60
|
embed: (id) => `https://w.soundcloud.com/player/?url=${encodeURIComponent(`https://soundcloud.com/${id}`)}&auto_play=true`,
|
|
65
61
|
frameHost: 'https://w.soundcloud.com',
|
|
66
|
-
poster: { kind: '
|
|
62
|
+
poster: { kind: 'inline' },
|
|
67
63
|
shape: 'audio',
|
|
68
64
|
oembed: 'https://soundcloud.com/oembed',
|
|
69
65
|
copy: {
|
|
70
66
|
fr: {
|
|
71
|
-
note: 'Lecture par SoundCloud.',
|
|
72
|
-
noteLink: 'Politique de confidentialité',
|
|
73
67
|
privacy: 'Certains articles intègrent des enregistrements SoundCloud. Rien n’est chargé depuis SoundCloud tant que vous n’appuyez pas sur le bouton de lecture. Dès que vous lancez un enregistrement, il est lu depuis w.soundcloud.com, et SoundCloud peut alors recueillir des données et déposer des témoins selon sa propre politique.',
|
|
74
68
|
policyLabel: 'Politique de confidentialité de SoundCloud',
|
|
75
69
|
policyHref: 'https://soundcloud.com/pages/privacy',
|
|
76
70
|
},
|
|
77
71
|
en: {
|
|
78
|
-
note: 'Plays from SoundCloud.',
|
|
79
|
-
noteLink: 'Privacy policy',
|
|
80
72
|
privacy: 'Some articles embed SoundCloud recordings. Nothing loads from SoundCloud until you press play. Once you start a recording, it plays from w.soundcloud.com, and SoundCloud may then collect data and set cookies under its own policy.',
|
|
81
73
|
policyLabel: 'SoundCloud’s privacy policy',
|
|
82
74
|
policyHref: 'https://soundcloud.com/pages/privacy',
|
|
@@ -91,5 +83,5 @@ export const EMBED_PRIVACY_HEADING = {
|
|
|
91
83
|
fr: 'Lecteurs vidéo et audio intégrés',
|
|
92
84
|
en: 'Embedded video and audio players',
|
|
93
85
|
};
|
|
94
|
-
/** The privacy section's anchor,
|
|
86
|
+
/** The privacy section's anchor (`EmbedPrivacy.astro`), for a site that links to it. */
|
|
95
87
|
export const EMBED_PRIVACY_ID = 'lecteurs-integres';
|
package/dist/embeds/rehype.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type FacadeOptions } from './facade.js';
|
|
2
1
|
interface El {
|
|
3
2
|
type: string;
|
|
4
3
|
tagName?: string;
|
|
@@ -14,7 +13,9 @@ interface Ctx {
|
|
|
14
13
|
replaceNode(node: never, next: unknown): void;
|
|
15
14
|
appendChild(node: never, child: unknown): void;
|
|
16
15
|
}
|
|
17
|
-
|
|
16
|
+
/** Takes no options. 2.3.0 took `{ lang, privacyHref }` for the line under the player, since
|
|
17
|
+
* removed; a site still passing them builds the same. */
|
|
18
|
+
export declare function rehypeEmbeds(_legacy?: unknown): {
|
|
18
19
|
name: string;
|
|
19
20
|
element: {
|
|
20
21
|
filter: string[];
|
package/dist/embeds/rehype.js
CHANGED
|
@@ -12,7 +12,7 @@ import { embedFromLink } from './detect.js';
|
|
|
12
12
|
import { ASSET_TAGS, facadeHtml } from './facade.js';
|
|
13
13
|
const FLAG = 'pmwEmbeds';
|
|
14
14
|
const blank = (n) => n.type === 'text' && (n.value ?? '').trim() === '';
|
|
15
|
-
function visitParagraph(
|
|
15
|
+
function visitParagraph() {
|
|
16
16
|
return (node, ctx) => {
|
|
17
17
|
if (ctx.parent(node)?.type !== 'root')
|
|
18
18
|
return;
|
|
@@ -33,14 +33,16 @@ function visitParagraph(o) {
|
|
|
33
33
|
const hit = embedFromLink(href, text[0].value ?? '');
|
|
34
34
|
if (!hit)
|
|
35
35
|
return;
|
|
36
|
-
ctx.replaceNode(node, { type: 'raw', value: facadeHtml(hit
|
|
36
|
+
ctx.replaceNode(node, { type: 'raw', value: facadeHtml(hit) });
|
|
37
37
|
ctx.data[FLAG] = true;
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
|
-
|
|
40
|
+
/** Takes no options. 2.3.0 took `{ lang, privacyHref }` for the line under the player, since
|
|
41
|
+
* removed; a site still passing them builds the same. */
|
|
42
|
+
export function rehypeEmbeds(_legacy) {
|
|
41
43
|
return {
|
|
42
44
|
name: 'pmeweb-embeds',
|
|
43
|
-
element: { filter: ['p'], visit: visitParagraph(
|
|
45
|
+
element: { filter: ['p'], visit: visitParagraph() },
|
|
44
46
|
after(root, ctx) {
|
|
45
47
|
if (ctx.data[FLAG] === true)
|
|
46
48
|
ctx.appendChild(root, { type: 'raw', value: ASSET_TAGS });
|
package/dist/embeds/scan.d.ts
CHANGED
package/dist/embeds/scan.js
CHANGED
|
@@ -14,18 +14,13 @@ const ordered = (s) => PROVIDER_KEYS.filter((k) => s.has(k));
|
|
|
14
14
|
const unescapeHtml = (s) => s.replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
|
|
15
15
|
export function scanPages(pages) {
|
|
16
16
|
const thumbs = new Map();
|
|
17
|
-
const drawn = new Set();
|
|
18
17
|
const used = new Set();
|
|
19
18
|
const privacy = new Set();
|
|
20
19
|
const unconverted = [];
|
|
21
20
|
for (const { path, html } of pages) {
|
|
22
21
|
for (const m of html.matchAll(POSTER)) {
|
|
23
22
|
const [, provider, id] = m;
|
|
24
|
-
if (
|
|
25
|
-
continue;
|
|
26
|
-
if (id === 'drawn' && PROVIDERS[provider].poster.kind === 'drawn')
|
|
27
|
-
drawn.add(provider);
|
|
28
|
-
else
|
|
23
|
+
if (isKey(provider) && PROVIDERS[provider].poster.kind === 'thumbnail')
|
|
29
24
|
thumbs.set(`${provider}:${id}`, { provider, id });
|
|
30
25
|
}
|
|
31
26
|
for (const m of html.matchAll(FIGURE))
|
|
@@ -41,7 +36,7 @@ export function scanPages(pages) {
|
|
|
41
36
|
unconverted.push({ path, href });
|
|
42
37
|
}
|
|
43
38
|
}
|
|
44
|
-
return { thumbnails: [...thumbs.values()],
|
|
39
|
+
return { thumbnails: [...thumbs.values()], used: ordered(used), privacy: ordered(privacy), unconverted };
|
|
45
40
|
}
|
|
46
41
|
/** The build's two refusals, as messages; empty when the site is wired. */
|
|
47
42
|
export function selfCheck(s) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmeweb/content",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"description": "PmeWeb content contract: content.schema.json types, zodFrom, field checks, scheduling. Read by every site's build and by the client portal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|