@riebeckite/plugin-rich-embed 0.0.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/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright Rerurate_514
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # @riebeckite/plugin-rich-embed
2
+
3
+ Build-time media embeds for ` ```embed ` code blocks.
4
+
5
+ [日本語](./README_ja.md)
6
+
7
+ ## Overview
8
+
9
+ `richEmbed()` turns an ` ```embed ` fenced code block into a responsive
10
+ `<figure class="rb-rich-embed">`. Provider detection is pure string inspection:
11
+ the plugin never performs a network request at build time, and it ships no
12
+ client-side JavaScript.
13
+
14
+ ## Usage
15
+
16
+ ```ts
17
+ import { defineConfig } from "@riebeckite/core";
18
+ import { richEmbed } from "@riebeckite/plugin-rich-embed";
19
+
20
+ export default defineConfig({
21
+ // ...
22
+ plugins: [
23
+ richEmbed({
24
+ allowHosts: ["player.example.com"],
25
+ }),
26
+ ],
27
+ });
28
+ ```
29
+
30
+ ## Syntax
31
+
32
+ The first non-empty line is the URL. Every following `key: value` line is an
33
+ option.
34
+
35
+ ````md
36
+ ```embed
37
+ https://www.youtube.com/watch?v=dQw4w9WgXcQ
38
+ title: Demo video
39
+ caption: A short caption under the frame
40
+ aspect: 16/9
41
+ start: 30
42
+ ```
43
+ ````
44
+
45
+ | Block option | Description |
46
+ | ------------ | ----------- |
47
+ | `title` | `title` attribute of the `<iframe>` (also the link label for Gist) |
48
+ | `caption` | Rendered as `<figcaption class="rb-rich-embed__caption">` |
49
+ | `aspect` | Aspect ratio such as `16/9` or `4/3` (default `16/9`) |
50
+ | `start` | Start time in seconds (YouTube only) |
51
+
52
+ Unknown option keys are ignored. An invalid `aspect` falls back to `16/9`.
53
+
54
+ ## Supported providers
55
+
56
+ | Provider | Recognised URLs | Output `src` |
57
+ | -------- | --------------- | ------------ |
58
+ | YouTube | `youtube.com/watch?v=`, `youtu.be/`, `/shorts/`, `/embed/`, `/live/` | `https://www.youtube-nocookie.com/embed/<id>` |
59
+ | Vimeo | `vimeo.com/<numeric id>` | `https://player.vimeo.com/video/<id>` |
60
+ | Spotify | `open.spotify.com/(track\|album\|playlist\|episode\|show)/<id>` | `https://open.spotify.com/embed/<type>/<id>` |
61
+ | CodePen | `codepen.io/<user>/pen/<id>` | `https://codepen.io/<user>/embed/<id>` |
62
+ | GitHub Gist | `gist.github.com/...` | link card (gists cannot be iframed) |
63
+
64
+ When the host is not recognised, the block is left untouched and a warning
65
+ diagnostic is emitted. A generic `<iframe>` is only produced when the host is
66
+ listed in `allowHosts`.
67
+
68
+ ## Options
69
+
70
+ | Option | Type | Default | Description |
71
+ | ------ | ---- | ------- | ----------- |
72
+ | `allowHosts` | `string[]` | `[]` | Hostnames allowed to use the generic iframe fallback |
73
+ | `providers` | `RichEmbedProvider[]` | all | Allowlist of providers to enable |
74
+ | `disable` | `RichEmbedProvider[]` | `[]` | Providers to disable (wins over `providers`) |
75
+
76
+ ## Output HTML
77
+
78
+ ```html
79
+ <figure
80
+ class="rb-rich-embed"
81
+ data-rich-embed="youtube"
82
+ data-rich-embed-marker="RIEBECKITE_EXTERNAL_RICHEMBED_MARKER"
83
+ >
84
+ <div class="rb-rich-embed__frame" style="--rb-rich-embed-aspect:16/9">
85
+ <iframe
86
+ src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ"
87
+ loading="lazy"
88
+ allowfullscreen
89
+ referrerpolicy="strict-origin-when-cross-origin"
90
+ title="Demo video"
91
+ ></iframe>
92
+ </div>
93
+ <figcaption class="rb-rich-embed__caption">A short caption</figcaption>
94
+ </figure>
95
+ ```
96
+
97
+ Gist blocks produce `div.rb-rich-embed__card > a.rb-rich-embed__link` instead of
98
+ an iframe.
99
+
100
+ ### CSS hooks
101
+
102
+ - `.rb-rich-embed` — outer figure
103
+ - `.rb-rich-embed__frame` — responsive wrapper (reads `--rb-rich-embed-aspect`)
104
+ - `.rb-rich-embed__caption` — optional caption
105
+ - `.rb-rich-embed__card`, `.rb-rich-embed__link` — Gist link card
106
+
107
+ The stylesheet is shipped as `@riebeckite/plugin-rich-embed/style.css`.
108
+
109
+ ## Diagnostics
110
+
111
+ Unsupported or invalid blocks keep their original code block and report a
112
+ warning through the vfile message channel:
113
+
114
+ - `source: "@riebeckite/plugin-rich-embed"`
115
+ - `ruleId: "unsupported-embed"`
116
+
117
+ ## Security
118
+
119
+ - Only `https:` URLs are accepted.
120
+ - Provider output is constructed from validated path segments; dynamic
121
+ segments are passed through `encodeURIComponent`.
122
+ - A generic iframe requires an explicit `allowHosts` entry.
123
+ - No raw user HTML is ever emitted: titles, captions, and labels are written as
124
+ escaped text nodes.
125
+
126
+ ## Limitations
127
+
128
+ - No OEMBED fetching or title/thumbnail discovery — everything is derived from
129
+ the URL and the block options.
130
+ - GitHub Gists cannot be embedded in an iframe, so they render as a link card.
131
+ - Generic embeds are emitted verbatim from the `https:` URL, so only allow hosts
132
+ you trust.
133
+
134
+ ## Exports
135
+
136
+ - `richEmbed(options?)` — plugin factory
137
+ - `richEmbedPlugin` — alias of `richEmbed`
138
+ - Types: `RichEmbedOptions`, `RichEmbedProvider`
139
+
140
+ ## See also
141
+
142
+ - [Plugin guide](../../docs/plugins_en.md)
package/README_ja.md ADDED
@@ -0,0 +1,132 @@
1
+ # @riebeckite/plugin-rich-embed
2
+
3
+ ` ```embed ` コードブロックを、ビルド時に外部メディアの埋め込みへ変換するプラグインです。
4
+
5
+ [English](./README.md)
6
+
7
+ ## 概要
8
+
9
+ `richEmbed()` は ` ```embed ` フェンスをレスポンシブな
10
+ `<figure class="rb-rich-embed">` に置き換えます。プロバイダの判定は URL の文字列解析だけで行うため、ビルド時にネットワークへアクセスしません。クライアント側 JavaScript も不要です。
11
+
12
+ ## 使い方
13
+
14
+ ```ts
15
+ import { defineConfig } from "@riebeckite/core";
16
+ import { richEmbed } from "@riebeckite/plugin-rich-embed";
17
+
18
+ export default defineConfig({
19
+ // ...
20
+ plugins: [
21
+ richEmbed({
22
+ allowHosts: ["player.example.com"],
23
+ }),
24
+ ],
25
+ });
26
+ ```
27
+
28
+ ## 構文
29
+
30
+ 最初の空でない行が URL、それ以降の `key: value` 行がオプションです。
31
+
32
+ ````md
33
+ ```embed
34
+ https://www.youtube.com/watch?v=dQw4w9WgXcQ
35
+ title: デモ動画
36
+ caption: 枠の下に表示するキャプション
37
+ aspect: 16/9
38
+ start: 30
39
+ ```
40
+ ````
41
+
42
+ | ブロックオプション | 説明 |
43
+ | ------------------ | ---- |
44
+ | `title` | `<iframe>` の `title` 属性(Gist ではリンクのラベル) |
45
+ | `caption` | `<figcaption class="rb-rich-embed__caption">` として出力 |
46
+ | `aspect` | `16/9` や `4/3` などのアスペクト比(既定は `16/9`) |
47
+ | `start` | 開始位置(秒、YouTube のみ) |
48
+
49
+ 未知のキーは無視します。`aspect` が不正な場合は `16/9` に戻します。
50
+
51
+ ## 対応プロバイダ
52
+
53
+ | プロバイダ | 認識する URL | 出力する `src` |
54
+ | ---------- | ------------ | -------------- |
55
+ | YouTube | `youtube.com/watch?v=`, `youtu.be/`, `/shorts/`, `/embed/`, `/live/` | `https://www.youtube-nocookie.com/embed/<id>` |
56
+ | Vimeo | `vimeo.com/<数値ID>` | `https://player.vimeo.com/video/<id>` |
57
+ | Spotify | `open.spotify.com/(track\|album\|playlist\|episode\|show)/<id>` | `https://open.spotify.com/embed/<type>/<id>` |
58
+ | CodePen | `codepen.io/<user>/pen/<id>` | `https://codepen.io/<user>/embed/<id>` |
59
+ | GitHub Gist | `gist.github.com/...` | リンクカード(iframe 埋め込み不可) |
60
+
61
+ ホストを判別できない場合はブロックをそのまま残し、警告の診断を出します。汎用 `<iframe>` を出力するのは、ホストが `allowHosts` に含まれる場合だけです。
62
+
63
+ ## オプション
64
+
65
+ | オプション | 型 | 既定値 | 説明 |
66
+ | ---------- | -- | ------ | ---- |
67
+ | `allowHosts` | `string[]` | `[]` | 汎用 iframe へのフォールバックを許可するホスト名 |
68
+ | `providers` | `RichEmbedProvider[]` | すべて | 有効にするプロバイダの許可リスト |
69
+ | `disable` | `RichEmbedProvider[]` | `[]` | 無効にするプロバイダ(`providers` より優先) |
70
+
71
+ ## 出力 HTML
72
+
73
+ ```html
74
+ <figure
75
+ class="rb-rich-embed"
76
+ data-rich-embed="youtube"
77
+ data-rich-embed-marker="RIEBECKITE_EXTERNAL_RICHEMBED_MARKER"
78
+ >
79
+ <div class="rb-rich-embed__frame" style="--rb-rich-embed-aspect:16/9">
80
+ <iframe
81
+ src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ"
82
+ loading="lazy"
83
+ allowfullscreen
84
+ referrerpolicy="strict-origin-when-cross-origin"
85
+ title="デモ動画"
86
+ ></iframe>
87
+ </div>
88
+ <figcaption class="rb-rich-embed__caption">キャプション</figcaption>
89
+ </figure>
90
+ ```
91
+
92
+ Gist の場合は iframe の代わりに
93
+ `div.rb-rich-embed__card > a.rb-rich-embed__link` を出力します。
94
+
95
+ ### CSS フック
96
+
97
+ - `.rb-rich-embed` — 外側の figure
98
+ - `.rb-rich-embed__frame` — レスポンシブなラッパー(`--rb-rich-embed-aspect` を参照)
99
+ - `.rb-rich-embed__caption` — 任意のキャプション
100
+ - `.rb-rich-embed__card`, `.rb-rich-embed__link` — Gist のリンクカード
101
+
102
+ スタイルシートは `@riebeckite/plugin-rich-embed/style.css` として配布されます。
103
+
104
+ ## 診断
105
+
106
+ 未対応または不正なブロックは元のコードブロックを保持し、vfile のメッセージとして警告を報告します。
107
+
108
+ - `source: "@riebeckite/plugin-rich-embed"`
109
+ - `ruleId: "unsupported-embed"`
110
+
111
+ ## セキュリティ
112
+
113
+ - `https:` の URL だけを受理します。
114
+ - プロバイダの出力は検証済みのパスセグメントから組み立て、動的な部分は `encodeURIComponent` を通します。
115
+ - 汎用 iframe には `allowHosts` への明示的な登録が必要です。
116
+ - 生のユーザー HTML は出力しません。タイトル・キャプション・ラベルはすべてエスケープされるテキストノードとして書き込みます。
117
+
118
+ ## 制限事項
119
+
120
+ - OEMBED の取得やタイトル・サムネイルの自動検出は行いません。URL とブロックオプションだけから組み立てます。
121
+ - GitHub Gist は iframe で埋め込めないため、リンクカードとして表示します。
122
+ - 汎用埋め込みは `https:` の URL をそのまま使うため、信頼できるホストだけを許可してください。
123
+
124
+ ## エクスポート
125
+
126
+ - `richEmbed(options?)` — プラグインファクトリ
127
+ - `richEmbedPlugin` — `richEmbed` の別名
128
+ - 型: `RichEmbedOptions`, `RichEmbedProvider`
129
+
130
+ ## 関連
131
+
132
+ - [プラグインガイド](../../docs/plugins_ja.md)
@@ -0,0 +1,4 @@
1
+ import type { RichEmbedOptions } from "./src/types.js";
2
+ export type { RichEmbedOptions, RichEmbedProvider } from "./src/types.js";
3
+ export declare function richEmbed(options?: RichEmbedOptions): import("@riebeckite/core").RiebeckitePlugin<RichEmbedOptions>;
4
+ export declare const richEmbedPlugin: typeof richEmbed;
package/dist/index.js ADDED
@@ -0,0 +1,420 @@
1
+ // index.ts
2
+ import {
3
+ createStyleAsset,
4
+ definePlugin
5
+ } from "@riebeckite/core";
6
+
7
+ // src/providers.ts
8
+ var RICH_EMBED_PROVIDERS = [
9
+ "youtube",
10
+ "vimeo",
11
+ "spotify",
12
+ "codepen",
13
+ "gist",
14
+ "generic"
15
+ ];
16
+ var DEFAULT_ASPECT = "16/9";
17
+ var PROVIDER_TITLES = {
18
+ youtube: "YouTube video",
19
+ vimeo: "Vimeo video",
20
+ spotify: "Spotify embed",
21
+ codepen: "CodePen embed",
22
+ gist: "GitHub Gist",
23
+ generic: "Embedded content"
24
+ };
25
+ var YOUTUBE_HOSTS = /* @__PURE__ */ new Set([
26
+ "youtube.com",
27
+ "www.youtube.com",
28
+ "m.youtube.com",
29
+ "music.youtube.com",
30
+ "youtube-nocookie.com",
31
+ "www.youtube-nocookie.com"
32
+ ]);
33
+ var VIMEO_HOSTS = /* @__PURE__ */ new Set(["vimeo.com", "www.vimeo.com", "player.vimeo.com"]);
34
+ var SPOTIFY_HOSTS = /* @__PURE__ */ new Set(["open.spotify.com"]);
35
+ var CODEPEN_HOSTS = /* @__PURE__ */ new Set(["codepen.io", "www.codepen.io"]);
36
+ var GIST_HOSTS = /* @__PURE__ */ new Set(["gist.github.com"]);
37
+ var SPOTIFY_TYPES = /* @__PURE__ */ new Set(["track", "album", "playlist", "episode", "show"]);
38
+ var YOUTUBE_ID = /^[A-Za-z0-9_-]{11}$/;
39
+ var NUMERIC_ID = /^\d+$/;
40
+ var SLUG_ID = /^[A-Za-z0-9]+$/;
41
+ var USERNAME = /^[A-Za-z0-9_-]+$/;
42
+ var ASPECT_PATTERN = /^\d+(?:\.\d+)?\/\d+(?:\.\d+)?$/;
43
+ var OPTION_LINE = /^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*)$/;
44
+ function parseEmbedBlock(source) {
45
+ const lines = source.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
46
+ if (lines.length === 0) return null;
47
+ const [url, ...optionLines] = lines;
48
+ const options = {};
49
+ for (const line of optionLines) {
50
+ const match = OPTION_LINE.exec(line);
51
+ if (!match) continue;
52
+ const key = match[1].toLowerCase();
53
+ const value = match[2].trim();
54
+ if (value.length === 0) continue;
55
+ if (key === "title") options.title = value;
56
+ else if (key === "caption") options.caption = value;
57
+ else if (key === "aspect") options.aspect = normalizeAspect(value) ?? void 0;
58
+ else if (key === "start") options.start = parseStart(value);
59
+ }
60
+ return { url, options };
61
+ }
62
+ function normalizeAspect(value) {
63
+ const normalized = value.replace(/\s+/g, "");
64
+ return ASPECT_PATTERN.test(normalized) ? normalized : null;
65
+ }
66
+ function resolveEmbed(urlText, block, config) {
67
+ let url;
68
+ try {
69
+ url = new URL(urlText);
70
+ } catch {
71
+ return {
72
+ kind: "unsupported",
73
+ provider: null,
74
+ message: "Expected an absolute https URL on the first line."
75
+ };
76
+ }
77
+ if (url.protocol !== "https:") {
78
+ return {
79
+ kind: "unsupported",
80
+ provider: null,
81
+ message: "Only https: URLs are supported."
82
+ };
83
+ }
84
+ const host = url.hostname.toLowerCase();
85
+ const aspect = block.aspect ?? DEFAULT_ASPECT;
86
+ const detection = detectProvider(url, host);
87
+ if (detection) {
88
+ if (!isProviderEnabled(detection.provider, config)) {
89
+ return {
90
+ kind: "unsupported",
91
+ provider: detection.provider,
92
+ message: `Provider "${detection.provider}" is disabled by configuration.`
93
+ };
94
+ }
95
+ if (detection.kind === "link") {
96
+ return {
97
+ kind: "link",
98
+ provider: detection.provider,
99
+ href: detection.href,
100
+ label: block.title ?? PROVIDER_TITLES[detection.provider],
101
+ aspect
102
+ };
103
+ }
104
+ return {
105
+ kind: "iframe",
106
+ provider: detection.provider,
107
+ src: withStart(detection.provider, detection.src, block.start),
108
+ title: block.title ?? PROVIDER_TITLES[detection.provider],
109
+ aspect
110
+ };
111
+ }
112
+ if (isProviderEnabled("generic", config) && isAllowedHost(host, config)) {
113
+ return {
114
+ kind: "iframe",
115
+ provider: "generic",
116
+ src: url.href,
117
+ title: block.title ?? PROVIDER_TITLES.generic,
118
+ aspect
119
+ };
120
+ }
121
+ return {
122
+ kind: "unsupported",
123
+ provider: null,
124
+ message: `No supported provider for "${host}". Add the host to allowHosts to embed it as a generic iframe.`
125
+ };
126
+ }
127
+ function detectProvider(url, host) {
128
+ if (YOUTUBE_HOSTS.has(host)) {
129
+ const id = youtubeId(url);
130
+ if (id) {
131
+ return {
132
+ kind: "iframe",
133
+ provider: "youtube",
134
+ src: `https://www.youtube-nocookie.com/embed/${encodeURIComponent(id)}`
135
+ };
136
+ }
137
+ }
138
+ if (VIMEO_HOSTS.has(host)) {
139
+ const id = pathSegments(url)[0];
140
+ if (id && NUMERIC_ID.test(id)) {
141
+ return {
142
+ kind: "iframe",
143
+ provider: "vimeo",
144
+ src: `https://player.vimeo.com/video/${encodeURIComponent(id)}`
145
+ };
146
+ }
147
+ }
148
+ if (SPOTIFY_HOSTS.has(host)) {
149
+ const [type, id] = pathSegments(url);
150
+ if (type && SPOTIFY_TYPES.has(type) && id && SLUG_ID.test(id)) {
151
+ return {
152
+ kind: "iframe",
153
+ provider: "spotify",
154
+ src: `https://open.spotify.com/embed/${encodeURIComponent(type)}/${encodeURIComponent(id)}`
155
+ };
156
+ }
157
+ }
158
+ if (CODEPEN_HOSTS.has(host)) {
159
+ const [user, marker, id] = pathSegments(url);
160
+ if (user && USERNAME.test(user) && marker === "pen" && id && SLUG_ID.test(id)) {
161
+ return {
162
+ kind: "iframe",
163
+ provider: "codepen",
164
+ src: `https://codepen.io/${encodeURIComponent(user)}/embed/${encodeURIComponent(id)}`
165
+ };
166
+ }
167
+ }
168
+ if (GIST_HOSTS.has(host)) {
169
+ return { kind: "link", provider: "gist", href: url.href };
170
+ }
171
+ return null;
172
+ }
173
+ function youtubeId(url) {
174
+ const segments = pathSegments(url);
175
+ if (url.hostname.toLowerCase() === "youtu.be") {
176
+ return segments[0] && YOUTUBE_ID.test(segments[0]) ? segments[0] : null;
177
+ }
178
+ if (url.pathname === "/watch" || url.pathname === "/watch/") {
179
+ const id = url.searchParams.get("v");
180
+ return id && YOUTUBE_ID.test(id) ? id : null;
181
+ }
182
+ if (segments[0] === "shorts" || segments[0] === "embed" || segments[0] === "live") {
183
+ return segments[1] && YOUTUBE_ID.test(segments[1]) ? segments[1] : null;
184
+ }
185
+ return null;
186
+ }
187
+ function pathSegments(url) {
188
+ return url.pathname.split("/").filter((segment) => segment.length > 0);
189
+ }
190
+ function isAllowedHost(host, config) {
191
+ return (config.allowHosts ?? []).some(
192
+ (allowed) => allowed.trim().toLowerCase() === host
193
+ );
194
+ }
195
+ function isProviderEnabled(provider, config) {
196
+ if (config.disable?.includes(provider)) return false;
197
+ if (config.providers && !config.providers.includes(provider)) return false;
198
+ return true;
199
+ }
200
+ function withStart(provider, src, start) {
201
+ if (provider !== "youtube" || start === void 0) return src;
202
+ const separator = src.includes("?") ? "&" : "?";
203
+ return `${src}${separator}start=${start}`;
204
+ }
205
+ function parseStart(value) {
206
+ if (!/^\d+$/.test(value)) return void 0;
207
+ const seconds = Number.parseInt(value, 10);
208
+ return Number.isFinite(seconds) && seconds >= 0 ? seconds : void 0;
209
+ }
210
+
211
+ // src/hast.ts
212
+ function visitElements(node, visitor, parent, index) {
213
+ if (isElementNode(node)) visitor(node, parent, index);
214
+ for (const [childIndex, child] of [...getChildren(node)].entries()) {
215
+ visitElements(
216
+ child,
217
+ visitor,
218
+ hasChildren(node) ? node : parent,
219
+ childIndex
220
+ );
221
+ }
222
+ }
223
+ function element(tagName, properties = {}, children = []) {
224
+ return { type: "element", tagName, properties, children };
225
+ }
226
+ function text(value) {
227
+ return { type: "text", value };
228
+ }
229
+ function getTextContent(node) {
230
+ if (node.type === "text" && "value" in node && typeof node.value === "string") {
231
+ return node.value;
232
+ }
233
+ return getChildren(node).map(getTextContent).join("");
234
+ }
235
+ function hasClass(node, className) {
236
+ const value = node.properties?.className;
237
+ const classes = Array.isArray(value) ? value.map(String) : typeof value === "string" ? value.split(/\s+/) : [];
238
+ return classes.includes(className);
239
+ }
240
+ function isElementNode(node) {
241
+ return node.type === "element";
242
+ }
243
+ function hasChildren(node) {
244
+ return Array.isArray(node.children);
245
+ }
246
+ function getChildren(node) {
247
+ const children = node.children;
248
+ return Array.isArray(children) ? children : [];
249
+ }
250
+
251
+ // src/rehype.ts
252
+ var DIAGNOSTIC_SOURCE = "@riebeckite/plugin-rich-embed";
253
+ var RICH_EMBED_MARKER = "RIEBECKITE_EXTERNAL_RICHEMBED_MARKER";
254
+ function rehypeRichEmbed(options = {}) {
255
+ return (tree, file) => {
256
+ visitElements(tree, (node, parent, index) => {
257
+ if (!parent || index === void 0 || !isEmbedCodeBlock(node)) return;
258
+ const code = findDirectChild(node, "code");
259
+ const source = (code ? getTextContent(code) : getTextContent(node)).trim();
260
+ const parsed = parseEmbedBlock(source);
261
+ if (!parsed) {
262
+ reportDiagnostic(file, source, "the embed block is empty.");
263
+ return;
264
+ }
265
+ const resolution = resolveEmbed(parsed.url, parsed.options, options);
266
+ if (resolution.kind === "unsupported") {
267
+ reportDiagnostic(file, parsed.url, resolution.message);
268
+ return;
269
+ }
270
+ parent.children = parent.children ?? [];
271
+ parent.children[index] = buildFigure(resolution, parsed.options);
272
+ });
273
+ };
274
+ }
275
+ function buildFigure(resolution, block) {
276
+ const children = [];
277
+ if (resolution.kind === "iframe") {
278
+ children.push(
279
+ element(
280
+ "div",
281
+ {
282
+ className: "rb-rich-embed__frame",
283
+ style: `--rb-rich-embed-aspect:${resolution.aspect}`
284
+ },
285
+ [
286
+ element("iframe", {
287
+ src: resolution.src,
288
+ loading: "lazy",
289
+ allowFullscreen: true,
290
+ referrerPolicy: "strict-origin-when-cross-origin",
291
+ title: resolution.title
292
+ })
293
+ ]
294
+ )
295
+ );
296
+ } else {
297
+ children.push(
298
+ element("div", { className: "rb-rich-embed__card" }, [
299
+ element(
300
+ "a",
301
+ {
302
+ className: "rb-rich-embed__link",
303
+ href: resolution.href,
304
+ rel: "noopener noreferrer",
305
+ target: "_blank"
306
+ },
307
+ [text(resolution.label)]
308
+ )
309
+ ])
310
+ );
311
+ }
312
+ const caption = block.caption?.trim();
313
+ if (caption) {
314
+ children.push(
315
+ element("figcaption", { className: "rb-rich-embed__caption" }, [
316
+ text(caption)
317
+ ])
318
+ );
319
+ }
320
+ return element(
321
+ "figure",
322
+ {
323
+ className: "rb-rich-embed",
324
+ dataRichEmbed: resolution.provider,
325
+ dataRichEmbedMarker: RICH_EMBED_MARKER
326
+ },
327
+ children
328
+ );
329
+ }
330
+ function reportDiagnostic(file, target, message) {
331
+ const reporter = file?.message;
332
+ if (typeof reporter !== "function") return;
333
+ const diagnostic = reporter.call(
334
+ file,
335
+ `Rich embed kept as a code block: ${message} (${target})`
336
+ );
337
+ if (diagnostic && typeof diagnostic === "object") {
338
+ Object.assign(diagnostic, {
339
+ source: DIAGNOSTIC_SOURCE,
340
+ ruleId: "unsupported-embed"
341
+ });
342
+ }
343
+ }
344
+ function isEmbedCodeBlock(node) {
345
+ if (node.tagName !== "pre") return false;
346
+ const code = findDirectChild(node, "code");
347
+ return Boolean(code && hasClass(code, "language-embed"));
348
+ }
349
+ function findDirectChild(node, tagName) {
350
+ return node.children?.find(
351
+ (child) => child.type === "element" && child.tagName === tagName
352
+ ) ?? null;
353
+ }
354
+
355
+ // index.ts
356
+ function richEmbed(options = {}) {
357
+ return definePlugin({
358
+ name: "rich-embed",
359
+ options,
360
+ validateOptions: validateRichEmbedOptions,
361
+ extendHtmlPipeline: (pipeline) => {
362
+ pipeline.use(rehypeRichEmbed, options);
363
+ },
364
+ assets: [createStyleAsset("rich-embed")]
365
+ });
366
+ }
367
+ var richEmbedPlugin = richEmbed;
368
+ function validateRichEmbedOptions(options) {
369
+ if (!options) return [];
370
+ const issues = [];
371
+ if (options.allowHosts !== void 0) {
372
+ if (!Array.isArray(options.allowHosts)) {
373
+ issues.push({
374
+ path: "allowHosts",
375
+ message: "Expected an array of hostnames."
376
+ });
377
+ } else {
378
+ for (const [index, host] of options.allowHosts.entries()) {
379
+ if (typeof host !== "string" || host.trim().length === 0) {
380
+ issues.push({
381
+ path: `allowHosts[${index}]`,
382
+ message: "Expected a non-empty hostname string."
383
+ });
384
+ } else if (host.includes("/") || host.includes(":")) {
385
+ issues.push({
386
+ path: `allowHosts[${index}]`,
387
+ message: "Expected a hostname only (no scheme, path, or port)."
388
+ });
389
+ }
390
+ }
391
+ }
392
+ }
393
+ for (const key of ["providers", "disable"]) {
394
+ const value = options[key];
395
+ if (value === void 0) continue;
396
+ if (!Array.isArray(value)) {
397
+ issues.push({
398
+ path: key,
399
+ message: "Expected an array of provider names."
400
+ });
401
+ continue;
402
+ }
403
+ for (const [index, provider] of value.entries()) {
404
+ if (!isRichEmbedProvider(provider)) {
405
+ issues.push({
406
+ path: `${key}[${index}]`,
407
+ message: `Unknown provider "${String(provider)}".`
408
+ });
409
+ }
410
+ }
411
+ }
412
+ return issues;
413
+ }
414
+ function isRichEmbedProvider(value) {
415
+ return typeof value === "string" && RICH_EMBED_PROVIDERS.includes(value);
416
+ }
417
+ export {
418
+ richEmbed,
419
+ richEmbedPlugin
420
+ };
@@ -0,0 +1,8 @@
1
+ import type { ElementNode, HastNode, ParentNode, TextNode } from "./types.js";
2
+ export declare function visitElements(node: HastNode, visitor: (node: ElementNode, parent?: ParentNode, index?: number) => void, parent?: ParentNode, index?: number): void;
3
+ export declare function element(tagName: string, properties?: Record<string, unknown>, children?: HastNode[]): ElementNode;
4
+ export declare function text(value: string): TextNode;
5
+ export declare function getTextContent(node: HastNode): string;
6
+ export declare function getStringProperty(node: ElementNode, key: string): string | null;
7
+ export declare function hasClass(node: ElementNode, className: string): boolean;
8
+ export declare function mergeClassName(current: unknown, next: string): string;
@@ -0,0 +1,38 @@
1
+ import type { RichEmbedBlockOptions, RichEmbedOptions, RichEmbedProvider } from "./types.js";
2
+ export declare const RICH_EMBED_PROVIDERS: readonly RichEmbedProvider[];
3
+ export declare const DEFAULT_ASPECT = "16/9";
4
+ export type ParsedEmbedBlock = {
5
+ url: string;
6
+ options: RichEmbedBlockOptions;
7
+ };
8
+ export type EmbedResolution = {
9
+ kind: "iframe";
10
+ provider: RichEmbedProvider;
11
+ src: string;
12
+ title: string;
13
+ aspect: string;
14
+ } | {
15
+ kind: "link";
16
+ provider: "gist";
17
+ href: string;
18
+ label: string;
19
+ aspect: string;
20
+ } | {
21
+ kind: "unsupported";
22
+ provider: RichEmbedProvider | null;
23
+ message: string;
24
+ };
25
+ export type SupportedEmbedResolution = Exclude<EmbedResolution, {
26
+ kind: "unsupported";
27
+ }>;
28
+ /**
29
+ * Parses an `embed` fenced code block. The first non-empty line is the URL and
30
+ * every following `key: value` line is an option.
31
+ */
32
+ export declare function parseEmbedBlock(source: string): ParsedEmbedBlock | null;
33
+ export declare function normalizeAspect(value: string): string | null;
34
+ /**
35
+ * Resolves a URL to an embeddable target using only string inspection. No
36
+ * network request is performed.
37
+ */
38
+ export declare function resolveEmbed(urlText: string, block: RichEmbedBlockOptions, config: RichEmbedOptions): EmbedResolution;
@@ -0,0 +1,2 @@
1
+ import type { HastNode, RichEmbedOptions } from "./types.js";
2
+ export declare function rehypeRichEmbed(options?: RichEmbedOptions): (tree: HastNode, file: unknown) => void;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Providers that `richEmbed()` can recognise from a URL alone. Detection never
3
+ * performs a network request; it only inspects the parsed https URL.
4
+ */
5
+ export type RichEmbedProvider = "youtube" | "vimeo" | "spotify" | "codepen" | "gist" | "generic";
6
+ export type RichEmbedOptions = {
7
+ /**
8
+ * Hostnames (exact match, case-insensitive) allowed to fall back to a generic
9
+ * `<iframe>` when no built-in provider recognises the URL. Empty by default,
10
+ * so unknown hosts are never embedded.
11
+ */
12
+ allowHosts?: readonly string[];
13
+ /**
14
+ * Allowlist of providers to enable. When omitted, every provider is enabled.
15
+ */
16
+ providers?: readonly RichEmbedProvider[];
17
+ /**
18
+ * Providers to disable. Takes precedence over `providers`.
19
+ */
20
+ disable?: readonly RichEmbedProvider[];
21
+ };
22
+ /** Options parsed from the lines that follow the URL in an `embed` fence. */
23
+ export type RichEmbedBlockOptions = {
24
+ title?: string;
25
+ caption?: string;
26
+ aspect?: string;
27
+ start?: number;
28
+ };
29
+ export type ElementNode = {
30
+ type: string;
31
+ tagName?: string;
32
+ properties?: Record<string, unknown>;
33
+ children?: HastNode[];
34
+ };
35
+ export type ParentNode = HastNode & {
36
+ children?: HastNode[];
37
+ };
38
+ export type TextNode = {
39
+ type: "text";
40
+ value: string;
41
+ };
42
+ export type RawNode = {
43
+ type: "raw";
44
+ value: string;
45
+ };
46
+ export type HastNode = ElementNode | TextNode | RawNode | {
47
+ type: string;
48
+ [key: string]: unknown;
49
+ };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@riebeckite/plugin-rich-embed",
3
+ "version": "0.0.1",
4
+ "license": "Apache-2.0",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "source": "./index.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./style.css": "./style.css"
16
+ },
17
+ "dependencies": {
18
+ "@riebeckite/core": "0.0.1"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "files": [
24
+ "LICENSE",
25
+ "README.md",
26
+ "README_ja.md",
27
+ "dist",
28
+ "style.css"
29
+ ],
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/Rerurate514/riebeckite.git",
33
+ "directory": "packages/plugins/rich-embed"
34
+ },
35
+ "homepage": "https://github.com/Rerurate514/riebeckite#readme",
36
+ "bugs": {
37
+ "url": "https://github.com/Rerurate514/riebeckite/issues"
38
+ },
39
+ "description": "Build-time rich media embeds for Riebeckite code blocks.",
40
+ "keywords": [
41
+ "riebeckite",
42
+ "plugin",
43
+ "embeds",
44
+ "markdown"
45
+ ],
46
+ "engines": {
47
+ "node": "^20.19.0 || >=22.12.0"
48
+ },
49
+ "sideEffects": [
50
+ "./style.css"
51
+ ],
52
+ "scripts": {
53
+ "build": "node ../../../scripts/build_package.mjs"
54
+ }
55
+ }
package/style.css ADDED
@@ -0,0 +1,78 @@
1
+ /*
2
+ * @riebeckite/plugin-rich-embed
3
+ * Stable class namespace: rb-rich-embed*
4
+ */
5
+ .rb-rich-embed {
6
+ --rb-rich-embed-paper: var(--rb-color-paper, #ffffff);
7
+ --rb-rich-embed-ink: var(--rb-color-ink, #000000);
8
+ --rb-rich-embed-muted: var(--rb-color-muted, #69645c);
9
+ --rb-rich-embed-rule: var(
10
+ --rb-color-border,
11
+ color-mix(in srgb, var(--rb-rich-embed-ink) 18%, transparent)
12
+ );
13
+ margin: 2rem 0;
14
+ border: 1px solid var(--rb-rich-embed-rule);
15
+ background: color-mix(
16
+ in srgb,
17
+ var(--rb-rich-embed-paper) 96%,
18
+ var(--rb-rich-embed-ink)
19
+ );
20
+ color: var(--rb-rich-embed-ink);
21
+ }
22
+
23
+ .rb-rich-embed__frame {
24
+ position: relative;
25
+ aspect-ratio: var(--rb-rich-embed-aspect, 16 / 9);
26
+ width: 100%;
27
+ overflow: hidden;
28
+ background: color-mix(in srgb, var(--rb-rich-embed-ink) 6%, transparent);
29
+ }
30
+
31
+ .rb-rich-embed__frame > iframe {
32
+ position: absolute;
33
+ inset: 0;
34
+ display: block;
35
+ width: 100%;
36
+ height: 100%;
37
+ border: 0;
38
+ }
39
+
40
+ .rb-rich-embed__caption {
41
+ padding: 0.625rem 0.75rem;
42
+ border-top: 1px solid var(--rb-rich-embed-rule);
43
+ color: var(--rb-rich-embed-muted);
44
+ font-size: 0.875rem;
45
+ }
46
+
47
+ .rb-rich-embed__card {
48
+ display: flex;
49
+ align-items: center;
50
+ gap: 0.5rem;
51
+ padding: 1rem 1.25rem;
52
+ }
53
+
54
+ .rb-rich-embed__link {
55
+ color: var(--rb-color-accent, var(--rb-rich-embed-ink));
56
+ font-weight: 600;
57
+ text-decoration: underline;
58
+ text-underline-offset: 0.15em;
59
+ }
60
+
61
+ .rb-rich-embed__link::after {
62
+ content: "↗";
63
+ margin-inline-start: 0.35em;
64
+ font-weight: 400;
65
+ text-decoration: none;
66
+ }
67
+
68
+ html[data-theme="dark"] .rb-rich-embed,
69
+ html.dark .rb-rich-embed {
70
+ --rb-rich-embed-paper: var(--rb-color-ink, #000000);
71
+ --rb-rich-embed-ink: var(--rb-color-paper, #ffffff);
72
+ }
73
+
74
+ @media (prefers-reduced-motion: reduce) {
75
+ .rb-rich-embed__link {
76
+ transition: none;
77
+ }
78
+ }