├── .all-contributorsrc ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ └── validate.yml ├── .gitignore ├── .huskyrc.js ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── other ├── MAINTAINING.md ├── USERS.md └── manual-releases.md ├── package.json ├── src ├── __tests__ │ └── index.ts └── index.ts ├── tsconfig.json └── vitest.config.js /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "transformer-oembed", 3 | "projectOwner": "remark-embedder", 4 | "imageSize": 100, 5 | "commit": false, 6 | "commitConvention": "none", 7 | "contributorsPerLine": 7, 8 | "repoHost": "https://github.com", 9 | "repoType": "github", 10 | "skipCi": false, 11 | "files": [ 12 | "README.md" 13 | ], 14 | "contributors": [ 15 | { 16 | "login": "kentcdodds", 17 | "name": "Kent C. Dodds", 18 | "avatar_url": "https://avatars.githubusercontent.com/u/1500684?v=3", 19 | "profile": "https://kentcdodds.com", 20 | "contributions": [ 21 | "code", 22 | "doc", 23 | "infra", 24 | "test" 25 | ] 26 | }, 27 | { 28 | "login": "MichaelDeBoey", 29 | "name": "Michaël De Boey", 30 | "avatar_url": "https://avatars3.githubusercontent.com/u/6643991?v=4", 31 | "profile": "https://michaeldeboey.be", 32 | "contributions": [ 33 | "code", 34 | "doc", 35 | "maintenance" 36 | ] 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | - `@remark-embedder/transformer-oembed` version: 15 | - `node` version: 16 | - `npm` version: 17 | 18 | Relevant code or config 19 | 20 | ```js 21 | 22 | ``` 23 | 24 | What you did: 25 | 26 | What happened: 27 | 28 | 29 | 30 | Reproduction repository: 31 | 32 | 36 | 37 | Problem description: 38 | 39 | Suggested solution: 40 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | **What**: 20 | 21 | 22 | 23 | **Why**: 24 | 25 | 26 | 27 | **How**: 28 | 29 | 30 | 31 | **Checklist**: 32 | 33 | 34 | 35 | 36 | - [ ] Documentation 37 | - [ ] Tests 38 | - [ ] Ready to be merged 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: daily 7 | 8 | - package-ecosystem: npm 9 | directory: / 10 | schedule: 11 | interval: daily 12 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: validate 2 | on: 3 | push: 4 | branches: 5 | - '+([0-9])?(.{+([0-9]),x}).x' 6 | - 'main' 7 | - 'next' 8 | - 'next-major' 9 | - 'beta' 10 | - 'alpha' 11 | - '!all-contributors/**' 12 | pull_request: {} 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | main: 19 | # ignore all-contributors PRs 20 | if: ${{ !contains(github.head_ref, 'all-contributors') }} 21 | strategy: 22 | matrix: 23 | node: [18, 20, 22] 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: ⬇️ Checkout repo 27 | uses: actions/checkout@v4 28 | 29 | - name: ⎔ Setup node 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: ${{ matrix.node }} 33 | 34 | - name: 📥 Download deps 35 | uses: bahmutov/npm-install@v1 36 | with: 37 | useLockFile: false 38 | 39 | - name: ▶️ Run validate script 40 | run: npm run validate 41 | 42 | - name: ⬆️ Upload coverage report 43 | uses: codecov/codecov-action@v4 44 | 45 | release: 46 | needs: main 47 | runs-on: ubuntu-latest 48 | if: 49 | ${{ github.repository == 'remark-embedder/transformer-oembed' && 50 | contains('refs/heads/main,refs/heads/beta,refs/heads/next,refs/heads/alpha', 51 | github.ref) && github.event_name == 'push' }} 52 | steps: 53 | - name: ⬇️ Checkout repo 54 | uses: actions/checkout@v4 55 | 56 | - name: ⎔ Setup node 57 | uses: actions/setup-node@v4 58 | with: 59 | node-version: 20 60 | 61 | - name: 📥 Download deps 62 | uses: bahmutov/npm-install@v1 63 | with: 64 | useLockFile: false 65 | 66 | - name: 🏗 Run build script 67 | run: npm run build 68 | 69 | - name: 🚀 Release 70 | uses: cycjimmy/semantic-release-action@v4 71 | with: 72 | semantic_version: 17 73 | branches: | 74 | [ 75 | '+([0-9])?(.{+([0-9]),x}).x', 76 | 'main', 77 | 'next', 78 | 'next-major', 79 | {name: 'beta', prerelease: true}, 80 | {name: 'alpha', prerelease: true} 81 | ] 82 | env: 83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 84 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | .DS_Store 5 | 6 | # these cause more harm than good 7 | # when working with contributors 8 | package-lock.json 9 | yarn.lock 10 | -------------------------------------------------------------------------------- /.huskyrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('kcd-scripts/husky') 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('kcd-scripts/prettier') 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | The changelog is automatically updated using 4 | [semantic-release](https://github.com/semantic-release/semantic-release). You 5 | can see it on the [releases page](../../releases). 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | me+coc@kentcdodds.com. All complaints will be reviewed and investigated promptly 64 | and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for being willing to contribute! 4 | 5 | **Working on your first Pull Request?** You can learn how from this _free_ 6 | series [How to Contribute to an Open Source Project on GitHub][egghead] 7 | 8 | ## Project setup 9 | 10 | 1. Fork and clone the repo 11 | 2. Run `npm run setup -s` to install dependencies and run validation 12 | 3. Create a branch for your PR with `git checkout -b pr/your-branch-name` 13 | 14 | > Tip: Keep your `main` branch pointing at the original repository and make pull 15 | > requests from branches on your fork. To do this, run: 16 | > 17 | > ``` 18 | > git remote add upstream https://github.com/remark-embedder/transformer-oembed 19 | > git fetch upstream 20 | > git branch --set-upstream-to=upstream/main main 21 | > ``` 22 | > 23 | > This will add the original repository as a "remote" called "upstream," Then 24 | > fetch the git information from that remote, then set your local `main` branch 25 | > to use the upstream main branch whenever you run `git pull`. Then you can make 26 | > all of your pull request branches based on this `main` branch. Whenever you 27 | > want to update your version of `main`, do a regular `git pull`. 28 | 29 | ## Committing and Pushing changes 30 | 31 | Please make sure to run the tests before you commit your changes. You can run 32 | `npm run test:update` which will update any snapshots that need updating. Make 33 | sure to include those changes (if they exist) in your commit. 34 | 35 | ## Help needed 36 | 37 | Please checkout the [the open issues][issues] 38 | 39 | Also, please watch the repo and respond to questions/bug reports/feature 40 | requests! Thanks! 41 | 42 | 43 | [egghead]: https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github 44 | [issues]: https://github.com/remark-embedder/transformer-oembed/issues 45 | 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2020 Kent C. Dodds 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

@remark-embedder/transformer-oembed

3 | 4 |

@remark-embedder transformer for oEmbed supported links

5 |
6 | 7 | --- 8 | 9 | 10 | [![Build Status][build-badge]][build] 11 | [![Code Coverage][coverage-badge]][coverage] 12 | [![version][version-badge]][package] 13 | [![downloads][downloads-badge]][npmtrends] 14 | [![MIT License][license-badge]][license] 15 | [![All Contributors][all-contributors-badge]](#contributors-) 16 | [![PRs Welcome][prs-badge]][prs] 17 | [![Code of Conduct][coc-badge]][coc] 18 | 19 | 20 | ## The problem 21 | 22 | You're using [`@remark-embedder/core`][@remark-embedder/core] to automatically 23 | convert URLs in your markdown to the embedded version of those URLs and want to 24 | have a transform for providers that support an oEmbed API. [Learn more about 25 | oEmbed from oembed.com][oembed.com] 26 | 27 | ## This solution 28 | 29 | This is a [`@remark-embedder`][@remark-embedder/core] transform for supported 30 | oembed API providers. Find the list of supported providers on 31 | [oembed.com][oembed.com]. 32 | 33 | ## Table of Contents 34 | 35 | 36 | 37 | 38 | - [Installation](#installation) 39 | - [Usage](#usage) 40 | - [Config](#config) 41 | - [Config as a function](#config-as-a-function) 42 | - [Inspiration](#inspiration) 43 | - [Other Solutions](#other-solutions) 44 | - [Issues](#issues) 45 | - [🐛 Bugs](#-bugs) 46 | - [💡 Feature Requests](#-feature-requests) 47 | - [Contributors ✨](#contributors-) 48 | - [LICENSE](#license) 49 | 50 | 51 | 52 | ## Installation 53 | 54 | This module is distributed via [npm][npm] which is bundled with [node][node] and 55 | should be installed as one of your project's `dependencies`: 56 | 57 | ```shell 58 | npm install @remark-embedder/transformer-oembed 59 | ``` 60 | 61 | ## Usage 62 | 63 | ```ts 64 | import remarkEmbedder from '@remark-embedder/core' 65 | import oembedTransformer from '@remark-embedder/transformer-oembed' 66 | // or, if you're using CommonJS require: 67 | // const {default: oembedTransformer} = require('@remark-embedder/transformer-oembed') 68 | import remark from 'remark' 69 | import html from 'remark-html' 70 | 71 | const exampleMarkdown = ` 72 | # My favorite YouTube video 73 | 74 | [This](https://www.youtube.com/watch?v=dQw4w9WgXcQ) is a great YouTube video. 75 | Watch it here: 76 | 77 | https://www.youtube.com/watch?v=dQw4w9WgXcQ 78 | 79 | Isn't it great!? 80 | ` 81 | 82 | async function go() { 83 | const result = await remark() 84 | .use(remarkEmbedder, { 85 | transformers: [oembedTransformer], 86 | }) 87 | .use(html) 88 | .process(exampleMarkdown) 89 | 90 | console.log(result.toString()) 91 | } 92 | 93 | go() 94 | ``` 95 | 96 | This will result in: 97 | 98 | ```html 99 |

My favorite YouTube video

100 |

101 | This is a great 102 | YouTube video. Watch it here: 103 |

104 | 112 |

Isn't it great!?

113 | ``` 114 | 115 | ### Config 116 | 117 | Some oembed providers offer special configuration via query parameters. You can 118 | provide those via config: 119 | 120 | ```ts 121 | // ... 122 | import type {Config} from '@remark-embedder/transformer-oembed' 123 | 124 | // ... 125 | 126 | async function go() { 127 | const result = await remark() 128 | .use(remarkEmbedder, { 129 | transformers: [ 130 | [ 131 | oembedTransformer, 132 | {params: {theme: 'dark', dnt: true, omit_script: true}} as Config, 133 | ], 134 | ], 135 | }) 136 | .use(html) 137 | .process(`https://twitter.com/kentcdodds/status/783161196945944580`) 138 | 139 | console.log(result.toString()) 140 | } 141 | 142 | // ... 143 | ``` 144 | 145 | That results in (notice the `data-` attributes which are specific to [twitter's 146 | oEmbed API][twitter-oembed-docs]): 147 | 148 | ```html 149 |
150 |

151 | I spent a few minutes working on this, just for you all. I promise, it wont 152 | disappoint. Though it may surprise 🎉

🙏 153 | https://t.co/wgTJYYHOzD 154 |

155 | — Kent C. Dodds (@kentcdodds) 156 | October 4, 2016 160 |
161 | ``` 162 | 163 | This could also be used to provide an access token for providers that require 164 | this (like [Instagram][instagram-oembed-docs]). 165 | 166 | ### Config as a function 167 | 168 | You can also provide configuration as a function so you can determine what 169 | configuration to give based on the provider and/or the URL. Like so: 170 | 171 | ```ts 172 | const oembedConfig: Config = ({url, provider}) => { 173 | if (provider.provider_name === 'Instagram') { 174 | return { 175 | params: {access_token: '{app-id}|{client-token}'}, 176 | } 177 | } 178 | } 179 | const remarkEmbedderConfig = { 180 | transformers: [[oembedTransformer, oembedConfig]], 181 | } 182 | // ... etc... 183 | ``` 184 | 185 | ## Inspiration 186 | 187 | It's a long story... Check out the inspiration on 188 | [`@remark-embedder/core`][@remark-embedder/core] 189 | 190 | ## Other Solutions 191 | 192 | - [`remark-oembed`][remark-oembed]: This one requires client-side JS to work 193 | which was unacceptable for our use cases. 194 | 195 | ## Issues 196 | 197 | _Looking to contribute? Look for the [Good First Issue][good-first-issue] 198 | label._ 199 | 200 | ### 🐛 Bugs 201 | 202 | Please file an issue for bugs, missing documentation, or unexpected behavior. 203 | 204 | [**See Bugs**][bugs] 205 | 206 | ### 💡 Feature Requests 207 | 208 | Please file an issue to suggest new features. Vote on feature requests by adding 209 | a 👍. This helps maintainers prioritize what to work on. 210 | 211 | [**See Feature Requests**][requests] 212 | 213 | ## Contributors ✨ 214 | 215 | Thanks goes to these people ([emoji key][emojis]): 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 |

Kent C. Dodds

💻 📖 🚇 ⚠️

Michaël De Boey

💻 📖 🚧
226 | 227 | 228 | 229 | 230 | 231 | 232 | This project follows the [all-contributors][all-contributors] specification. 233 | Contributions of any kind welcome! 234 | 235 | ## LICENSE 236 | 237 | MIT 238 | 239 | 240 | [npm]: https://www.npmjs.com 241 | [node]: https://nodejs.org 242 | [build-badge]: https://img.shields.io/github/workflow/status/remark-embedder/transformer-oembed/validate?logo=github&style=flat-square 243 | [build]: https://github.com/remark-embedder/transformer-oembed/actions?query=workflow%3Avalidate 244 | [coverage-badge]: https://img.shields.io/codecov/c/github/remark-embedder/transformer-oembed.svg?style=flat-square 245 | [coverage]: https://codecov.io/github/remark-embedder/transformer-oembed 246 | [version-badge]: https://img.shields.io/npm/v/@remark-embedder/transformer-oembed.svg?style=flat-square 247 | [package]: https://www.npmjs.com/package/@remark-embedder/transformer-oembed 248 | [downloads-badge]: https://img.shields.io/npm/dm/@remark-embedder/transformer-oembed.svg?style=flat-square 249 | [npmtrends]: https://www.npmtrends.com/@remark-embedder/transformer-oembed 250 | [license-badge]: https://img.shields.io/npm/l/@remark-embedder/transformer-oembed.svg?style=flat-square 251 | [license]: https://github.com/remark-embedder/transformer-oembed/blob/main/LICENSE 252 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square 253 | [prs]: https://makeapullrequest.com 254 | [coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square 255 | [coc]: https://github.com/remark-embedder/transformer-oembed/blob/main/CODE_OF_CONDUCT.md 256 | [emojis]: https://github.com/all-contributors/all-contributors#emoji-key 257 | [all-contributors]: https://github.com/all-contributors/all-contributors 258 | [all-contributors-badge]: https://img.shields.io/github/all-contributors/remark-embedder/transformer-oembed?color=orange&style=flat-square 259 | [bugs]: https://github.com/remark-embedder/transformer-oembed/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Acreated-desc+label%3Abug 260 | [requests]: https://github.com/remark-embedder/transformer-oembed/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement 261 | [good-first-issue]: https://github.com/remark-embedder/transformer-oembed/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement+label%3A%22good+first+issue%22 262 | 263 | [@remark-embedder/core]: https://github.com/remark-embedder/core 264 | [instagram-oembed-docs]: https://developers.facebook.com/docs/instagram/oembed 265 | [oembed.com]: https://oembed.com 266 | [remark-oembed]: https://github.com/sergioramos/remark-oembed 267 | [twitter-oembed-docs]: https://developer.twitter.com/en/docs/twitter-api/v1/tweets/post-and-engage/api-reference/get-statuses-oembed 268 | 269 | -------------------------------------------------------------------------------- /other/MAINTAINING.md: -------------------------------------------------------------------------------- 1 | # Maintaining 2 | 3 | 4 | 5 | 6 | **Table of Contents** 7 | 8 | - [Code of Conduct](#code-of-conduct) 9 | - [Issues](#issues) 10 | - [Pull Requests](#pull-requests) 11 | - [Release](#release) 12 | - [Thanks!](#thanks) 13 | 14 | 15 | 16 | This is documentation for maintainers of this project. 17 | 18 | ## Code of Conduct 19 | 20 | Please review, understand, and be an example of it. Violations of the code of 21 | conduct are taken seriously, even (especially) for maintainers. 22 | 23 | ## Issues 24 | 25 | We want to support and build the community. We do that best by helping people 26 | learn to solve their own problems. We have an issue template and hopefully most 27 | folks follow it. If it's not clear what the issue is, invite them to create a 28 | minimal reproduction of what they're trying to accomplish or the bug they think 29 | they've found. 30 | 31 | Once it's determined that a code change is necessary, point people to 32 | [makeapullrequest.com](https://makeapullrequest.com) and invite them to make a 33 | pull request. If they're the one who needs the feature, they're the one who can 34 | build it. If they need some hand holding and you have time to lend a hand, 35 | please do so. It's an investment into another human being, and an investment 36 | into a potential maintainer. 37 | 38 | Remember that this is open source, so the code is not yours, it's ours. If 39 | someone needs a change in the codebase, you don't have to make it happen 40 | yourself. Commit as much time to the project as you want/need to. Nobody can ask 41 | any more of you than that. 42 | 43 | ## Pull Requests 44 | 45 | As a maintainer, you're fine to make your branches on the main repo or on your 46 | own fork. Either way is fine. 47 | 48 | When we receive a pull request, a GitHub Action is kicked off automatically (see 49 | the `.github/workflows/validate.yml` for what runs in the Action). We avoid 50 | merging anything that breaks the GitHub Action. 51 | 52 | Please review PRs and focus on the code rather than the individual. You never 53 | know when this is someone's first ever PR and we want their experience to be as 54 | positive as possible, so be uplifting and constructive. 55 | 56 | When you merge the pull request, 99% of the time you should use the 57 | [Squash and merge](https://help.github.com/articles/merging-a-pull-request/) 58 | feature. This keeps our git history clean, but more importantly, this allows us 59 | to make any necessary changes to the commit message so we release what we want 60 | to release. See the next section on Releases for more about that. 61 | 62 | ## Release 63 | 64 | Our releases are automatic. They happen whenever code lands into `main`. A 65 | GitHub Action gets kicked off and if it's successful, a tool called 66 | [`semantic-release`](https://github.com/semantic-release/semantic-release) is 67 | used to automatically publish a new release to npm as well as a changelog to 68 | GitHub. It is only able to determine the version and whether a release is 69 | necessary by the git commit messages. With this in mind, **please brush up on 70 | [the commit message convention][commit] which drives our releases.** 71 | 72 | > One important note about this: Please make sure that commit messages do NOT 73 | > contain the words "BREAKING CHANGE" in them unless we want to push a major 74 | > version. I've been burned by this more than once where someone will include 75 | > "BREAKING CHANGE: None" and it will end up releasing a new major version. Not 76 | > a huge deal honestly, but kind of annoying... 77 | 78 | ## Thanks! 79 | 80 | Thank you so much for helping to maintain this project! 81 | 82 | 83 | [commit]: https://github.com/conventional-changelog-archived-repos/conventional-changelog-angular/blob/ed32559941719a130bb0327f886d6a32a8cbc2ba/convention.md 84 | 85 | -------------------------------------------------------------------------------- /other/USERS.md: -------------------------------------------------------------------------------- 1 | # Users 2 | 3 | If you or your company uses this project, add your name to this list! Eventually 4 | we may have a website to showcase these (wanna build it!?) 5 | 6 | > No users have been added yet! 7 | 8 | 13 | -------------------------------------------------------------------------------- /other/manual-releases.md: -------------------------------------------------------------------------------- 1 | # manual-releases 2 | 3 | This project has an automated release set up. So things are only released when 4 | there are useful changes in the code that justify a release. But sometimes 5 | things get messed up one way or another and we need to trigger the release 6 | ourselves. When this happens, simply bump the number below and commit that with 7 | the following commit message based on your needs: 8 | 9 | **Major** 10 | 11 | ``` 12 | fix(release): manually release a major version 13 | 14 | There was an issue with a major release, so this manual-releases.md 15 | change is to release a new major version. 16 | 17 | Reference: # 18 | 19 | BREAKING CHANGE: 20 | ``` 21 | 22 | **Minor** 23 | 24 | ``` 25 | feat(release): manually release a minor version 26 | 27 | There was an issue with a minor release, so this manual-releases.md 28 | change is to release a new minor version. 29 | 30 | Reference: # 31 | ``` 32 | 33 | **Patch** 34 | 35 | ``` 36 | fix(release): manually release a patch version 37 | 38 | There was an issue with a patch release, so this manual-releases.md 39 | change is to release a new patch version. 40 | 41 | Reference: # 42 | ``` 43 | 44 | The number of times we've had to do a manual release is: 0 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@remark-embedder/transformer-oembed", 3 | "version": "0.0.0-semantically-released", 4 | "publishConfig": { 5 | "access": "public" 6 | }, 7 | "description": "@remark-embedder transformer for oEmbed supported links", 8 | "main": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "keywords": [ 11 | "remark", 12 | "remark-plugin", 13 | "remark-embedder", 14 | "oEmbed" 15 | ], 16 | "author": "Kent C. Dodds (https://kentcdodds.com)", 17 | "license": "MIT", 18 | "engines": { 19 | "node": ">=12", 20 | "npm": ">=6" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/remark-embedder/transformer-oembed" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/remark-embedder/transformer-oembed/issues" 28 | }, 29 | "homepage": "https://github.com/remark-embedder/transformer-oembed#readme", 30 | "files": [ 31 | "dist" 32 | ], 33 | "scripts": { 34 | "build": "kcd-scripts build", 35 | "lint": "kcd-scripts lint", 36 | "setup": "npm install && npm run validate -s", 37 | "test": "vitest test", 38 | "test:update": "npm test -- --updateSnapshot --coverage", 39 | "typecheck": "kcd-scripts typecheck", 40 | "validate": "kcd-scripts validate" 41 | }, 42 | "dependencies": { 43 | "@babel/runtime": "^7.24.5" 44 | }, 45 | "devDependencies": { 46 | "@remark-embedder/core": "^3.0.3", 47 | "@vitest/coverage-v8": "^1.6.0", 48 | "kcd-scripts": "^15.0.1", 49 | "msw": "^2.3.0", 50 | "remark": "^15.0.1", 51 | "remark-html": "^16.0.1", 52 | "typescript": "^5.4.5", 53 | "vitest": "^1.6.0" 54 | }, 55 | "peerDependencies": { 56 | "@remark-embedder/core": "^3.0.0" 57 | }, 58 | "eslintConfig": { 59 | "extends": "./node_modules/kcd-scripts/eslint.js", 60 | "rules": { 61 | "babel/camelcase": "off" 62 | } 63 | }, 64 | "eslintIgnore": [ 65 | "node_modules", 66 | "coverage", 67 | "dist" 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /src/__tests__/index.ts: -------------------------------------------------------------------------------- 1 | import remarkEmbedder from '@remark-embedder/core' 2 | import {http, HttpResponse} from 'msw' 3 | import {setupServer} from 'msw/node' 4 | import {remark} from 'remark' 5 | import remarkHTML from 'remark-html' 6 | import {expect, test, beforeAll, afterEach, afterAll} from 'vitest' 7 | 8 | import transformer, {type Config} from '../' 9 | 10 | // this removes the quotes around strings... 11 | const unquoteSerializer = { 12 | serialize: (val: string) => val.trim(), 13 | test: (val: unknown) => typeof val === 'string', 14 | } 15 | 16 | expect.addSnapshotSerializer(unquoteSerializer) 17 | 18 | const server = setupServer( 19 | http.get('https://oembed.com/providers.json', () => 20 | HttpResponse.json([ 21 | { 22 | provider_name: 'Beautiful.AI', 23 | provider_url: 'https://www.beautiful.ai', 24 | endpoints: [ 25 | { 26 | url: 'https://www.beautiful.ai/api/oembed', 27 | // no scheme 😱 28 | discovery: true, 29 | }, 30 | ], 31 | }, 32 | { 33 | provider_name: 'Twitter', 34 | provider_url: 'https://www.twitter.com', 35 | endpoints: [ 36 | { 37 | schemes: [ 38 | 'https://twitter.com/*/status/*', 39 | 'https://*.twitter.com/*/status/*', 40 | 'https://twitter.com/*/moments/*', 41 | 'https://*.twitter.com/*/moments/*', 42 | ], 43 | url: 'https://publish.twitter.com/oembed', 44 | }, 45 | ], 46 | }, 47 | ]), 48 | ), 49 | http.get('https://publish.twitter.com/oembed', () => 50 | HttpResponse.json({ 51 | html: '', 52 | }), 53 | ), 54 | ) 55 | 56 | // enable API mocking in test runs using the same request handlers 57 | // as for the client-side mocking. 58 | beforeAll(() => server.listen()) 59 | afterEach(() => server.resetHandlers()) 60 | afterAll(() => server.close()) 61 | 62 | test('smoke test', async () => { 63 | const result = await remark() 64 | .use(remarkEmbedder, { 65 | transformers: [ 66 | [ 67 | transformer, 68 | {params: {dnt: true, omit_script: true, theme: 'dark'}} as Config, 69 | ], 70 | ], 71 | }) 72 | .use(remarkHTML, {sanitize: false}) 73 | .process( 74 | ` 75 | Here's a great tweet: 76 | 77 | https://twitter.com/kentcdodds/status/783161196945944580 78 | 79 | And here's an example of no provider: 80 | 81 | https://example.com 82 | `.trim(), 83 | ) 84 | 85 | expect(result.toString()).toMatchInlineSnapshot(` 86 |

Here's a great tweet:

87 | 88 |

And here's an example of no provider:

89 |

https://example.com

90 | `) 91 | }) 92 | 93 | test('no config required', async () => { 94 | const result = await remark() 95 | .use(remarkEmbedder, { 96 | transformers: [transformer], 97 | }) 98 | .use(remarkHTML, {sanitize: false}) 99 | .process(`https://twitter.com/kentcdodds/status/783161196945944580`) 100 | 101 | expect(result.toString()).toMatchInlineSnapshot( 102 | ``, 103 | ) 104 | }) 105 | 106 | test('config can be a function', async () => { 107 | const config: Config = () => ({ 108 | params: {dnt: true}, 109 | }) 110 | const result = await remark() 111 | .use(remarkEmbedder, { 112 | transformers: [[transformer, config]], 113 | }) 114 | .use(remarkHTML, {sanitize: false}) 115 | .process(`https://twitter.com/kentcdodds/status/783161196945944580`) 116 | 117 | expect(result.toString()).toMatchInlineSnapshot( 118 | ``, 119 | ) 120 | }) 121 | 122 | test('config function does not need to return anything', async () => { 123 | const config: Config = () => null 124 | const result = await remark() 125 | .use(remarkEmbedder, { 126 | transformers: [[transformer, config]], 127 | }) 128 | .use(remarkHTML, {sanitize: false}) 129 | .process(`https://twitter.com/kentcdodds/status/783161196945944580`) 130 | 131 | expect(result.toString()).toMatchInlineSnapshot( 132 | ``, 133 | ) 134 | }) 135 | 136 | test('sends the correct search params', async () => { 137 | let request: Request 138 | 139 | server.use( 140 | http.get('https://publish.twitter.com/oembed', ({request: req}) => { 141 | request = req 142 | return HttpResponse.json({ 143 | html: 'whatever', 144 | }) 145 | }), 146 | ) 147 | 148 | await remark() 149 | .use(remarkEmbedder, { 150 | transformers: [ 151 | [ 152 | transformer, 153 | {params: {dnt: true, omit_script: true, theme: 'dark'}} as Config, 154 | ], 155 | ], 156 | }) 157 | .use(remarkHTML, {sanitize: false}) 158 | .process(`https://twitter.com/kentcdodds/status/783161196945944580`) 159 | 160 | // @ts-expect-error it doesn't think request will be assigned by now... But it will! 161 | expect(request.url.toString()).toMatchInlineSnapshot( 162 | `https://publish.twitter.com/oembed?url=https%3A%2F%2Ftwitter.com%2Fkentcdodds%2Fstatus%2F783161196945944580&dnt=true&omit_script=true&theme=dark&format=json`, 163 | ) 164 | }) 165 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {URL} from 'url' 2 | import {type Transformer} from '@remark-embedder/core' 3 | 4 | type Provider = { 5 | provider_name: string 6 | provider_url: string 7 | endpoints: Array<{ 8 | schemes?: string[] 9 | discovery?: boolean 10 | url: string 11 | }> 12 | } 13 | type Providers = Array 14 | 15 | // eslint-disable-next-line @typescript-eslint/no-namespace 16 | declare namespace getProviders { 17 | let cache: Providers | undefined 18 | } 19 | 20 | async function getProviders(): Promise { 21 | if (!getProviders.cache) { 22 | const res = await fetch('https://oembed.com/providers.json') 23 | getProviders.cache = (await res.json()) as Providers 24 | } 25 | 26 | return getProviders.cache 27 | } 28 | 29 | // TODO: Support providers that do not have schemes 30 | async function getProviderEndpointURLForURL( 31 | url: string, 32 | ): Promise<{provider: Provider; endpoint: string} | null> { 33 | const providers = await getProviders() 34 | for (const provider of providers) { 35 | for (const endpoint of provider.endpoints) { 36 | if ( 37 | endpoint.schemes?.some(scheme => 38 | new RegExp(scheme.replace(/\*/g, '(.*)')).test(url), 39 | ) 40 | ) { 41 | return {provider, endpoint: endpoint.url} 42 | } 43 | } 44 | } 45 | return null 46 | } 47 | 48 | type Config = { 49 | params?: {[key: string]: unknown} 50 | } 51 | 52 | type GetConfig = ({ 53 | url, 54 | provider, 55 | }: { 56 | url: string 57 | provider: Provider 58 | }) => Config | null | undefined 59 | 60 | type OEmbedData = { 61 | html: string 62 | } 63 | 64 | const transformer: Transformer = { 65 | name: '@remark-embedder/transformer-oembed', 66 | shouldTransform: async url => { 67 | const result = await getProviderEndpointURLForURL(url) 68 | return Boolean(result) 69 | }, 70 | getHTML: async (urlString, getConfig = {}) => { 71 | const result = await getProviderEndpointURLForURL(urlString) 72 | 73 | // istanbul ignore if (shouldTransform prevents this, but if someone calls this directly then this would save them) 74 | if (!result) return null 75 | 76 | const {provider, endpoint} = result 77 | 78 | const url = new URL(endpoint) 79 | url.searchParams.set('url', urlString) 80 | 81 | let config: Config = getConfig as Config 82 | if (typeof getConfig === 'function') { 83 | // I really have no idea what's happening here: 84 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 85 | config = getConfig({url: urlString, provider}) ?? {} 86 | } 87 | 88 | for (const [key, value] of Object.entries(config.params ?? {})) { 89 | url.searchParams.set(key, String(value)) 90 | } 91 | 92 | // format has to be json so it is not configurable 93 | url.searchParams.set('format', 'json') 94 | 95 | const res = await fetch(url.toString()) 96 | const data = (await res.json()) as OEmbedData 97 | 98 | return data.html 99 | }, 100 | } 101 | 102 | export default transformer 103 | type ExportedConfig = Config | GetConfig 104 | export type {ExportedConfig as Config} 105 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/kcd-scripts/shared-tsconfig.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /vitest.config.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | module.exports = { 5 | test: { 6 | include: ['**/__tests__/**.ts'], 7 | environment: 'node', 8 | }, 9 | } 10 | --------------------------------------------------------------------------------