@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 +201 -0
- package/README.md +142 -0
- package/README_ja.md +132 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +420 -0
- package/dist/src/hast.d.ts +8 -0
- package/dist/src/providers.d.ts +38 -0
- package/dist/src/rehype.d.ts +2 -0
- package/dist/src/types.d.ts +49 -0
- package/package.json +55 -0
- package/style.css +78 -0
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)
|
package/dist/index.d.ts
ADDED
|
@@ -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,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
|
+
}
|