├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE.md
├── README.md
├── package.json
├── src
├── index.ts
└── utils
│ ├── fetchOembed.ts
│ ├── fetchOembedProviders.ts
│ ├── getProviderEndpointForLinkUrl.ts
│ ├── index.ts
│ ├── selectPossibleOembedLinkNodes.ts
│ └── transformLinkNodeToOembedNode.ts
├── test
└── index.test.ts
├── tsconfig.json
├── types
└── unist-util-select.d.ts
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .DS_Store
3 | node_modules
4 | .rts2_cache_cjs
5 | .rts2_cache_es
6 | .rts2_cache_umd
7 | dist
8 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | !dist
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - '12'
5 | - '10'
6 | - '8'
7 |
8 | jobs:
9 | include:
10 | # Define the release stage that runs semantic-release
11 | - stage: release
12 | node_js: lts/*
13 | # Advanced: optionally overwrite your default `script` step to skip the tests
14 | # script: skip
15 | script:
16 | - commitlint-travis
17 | deploy:
18 | provider: script
19 | skip_cleanup: true
20 | script: 'yarn build && npx semantic-release'
21 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Benedicte Raae
4 |
5 | Copyright (c) 2019 Agent of User
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy of
8 | this software and associated documentation files (the "Software"), to deal in
9 | the Software without restriction, including without limitation the rights to
10 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
11 | of the Software, and to permit persons to whom the Software is furnished to do
12 | so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # remark-oembed
2 |
3 | [![Downloads][downloads-badge]][downloads] [![Chat][chat-badge]][chat]
4 |
5 | Converts URLs surrounded by newlines into embeds.
6 |
7 | The important part of this code is taken directly from
8 | [Benedicte Raae](https://www.raae.codes/)'s excellent
9 | [gatsby-remark-oembed](https://github.com/raae/gatsby-remark-oembed) plugin, so
10 | thank you very much [@raae](https://github.com/raae) 🙏
11 |
12 | ## Installation
13 |
14 | [yarn][]:
15 |
16 | ```bash
17 | yarn add @agentofuser/remark-oembed
18 | ```
19 |
20 | ## Usage
21 |
22 | Say we have the following file, `demo.md`:
23 |
24 |
25 | ```markdown
26 | Hey this is a nice youtube video about making modern react apps with gatsby:
27 |
28 | https://www.youtube.com/watch?v=GN0xHSk2P8Q
29 |
30 | Check it out 👆
31 | ```
32 |
33 | And our script, `example.js`, looks as follows:
34 |
35 | ```javascript
36 | var fs = require('fs')
37 | var remark = require('remark')
38 | var oembed = require('@agentofuser/remark-oembed')
39 |
40 | remark()
41 | .use(oembed)
42 | .process(fs.readFileSync('demo.md'), function(err, file) {
43 | if (err) throw err
44 | console.log(String(file))
45 | })
46 | ```
47 |
48 | Now, running `node example` yields:
49 |
50 | ```markdown
51 | Hey this is a nice youtube video about making modern react apps with gatsby:
52 |
53 |
65 |
66 | Check it out 👆
67 | ```
68 |
69 | ## API
70 |
71 | ### `remark().use(oembed)`
72 |
73 | Converts URLs surrounded by newlines into embeds.
74 |
75 | ## Contribute
76 |
77 | See [`contributing.md` in `remarkjs/remark`][contribute] for ways to get
78 | started.
79 |
80 | This organisation has a [Code of Conduct][coc]. By interacting with this
81 | repository, organisation, or community you agree to abide by its terms.
82 |
83 | ## License
84 |
85 | [MIT][license] © [Agent of User][author]
86 |
87 |
88 |
89 | [build-badge]: https://img.shields.io/travis/agentofuser/remark-oembed.svg
90 | [build]: https://travis-ci.org/agentofuser/remark-oembed
91 | [downloads-badge]: https://img.shields.io/npm/dm/remark-oembed.svg
92 | [downloads]: https://www.npmjs.com/package/@agentofuser/remark-oembed
93 | [chat-badge]:
94 | https://img.shields.io/badge/join%20the%20community-on%20spectrum-7b16ff.svg
95 | [chat]: https://spectrum.chat/unified/remark
96 | [yarn]: https://yarnpkg.com/en/docs/install
97 | [license]: LICENSE.md
98 | [author]: https://agentofuser.com
99 | [remark]: https://github.com/remarkjs/remark
100 | [contribute]: https://github.com/remarkjs/remark/blob/master/contributing.md
101 | [coc]: https://github.com/remarkjs/remark/blob/master/code-of-conduct.md
102 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@agentofuser/remark-oembed",
3 | "publishConfig": {
4 | "access": "public"
5 | },
6 | "version": "1.0.4",
7 | "description": "oembed things",
8 | "license": "MIT",
9 | "keywords": [
10 | "remark",
11 | "remark-plugin",
12 | "markdown",
13 | "plugin",
14 | "embed",
15 | "oembed",
16 | "youtube"
17 | ],
18 | "repository": "agentofuser/remark-oembed",
19 | "bugs": "https://github.com/agentofuser/remark-oembed/issues",
20 | "author": "Agent of User (https://agentofuser.com)",
21 | "contributors": [
22 | "Agent of User (https://agentofuser.com)"
23 | ],
24 | "main": "dist/index.js",
25 | "umd:main": "dist/remark-oembed.umd.production.js",
26 | "module": "dist/remark-oembed.es.production.js",
27 | "typings": "dist/index.d.ts",
28 | "files": [
29 | "dist"
30 | ],
31 | "scripts": {
32 | "build": "tsdx build",
33 | "commit": "git-cz",
34 | "commit:retry": "git-cz --retry",
35 | "semantic-release": "semantic-release",
36 | "semantic-release:one-time-setup": "semantic-release-cli setup",
37 | "start": "tsdx watch",
38 | "test": "tsdx test"
39 | },
40 | "release": {
41 | "plugins": [
42 | "@semantic-release/commit-analyzer",
43 | "@semantic-release/release-notes-generator",
44 | "@semantic-release/npm",
45 | "@semantic-release/github",
46 | "@semantic-release/git"
47 | ]
48 | },
49 | "commitlint": {
50 | "extends": [
51 | "@commitlint/config-conventional"
52 | ],
53 | "rules": {
54 | "header-max-length": [
55 | 2,
56 | "always",
57 | 50
58 | ],
59 | "body-max-line-length": [
60 | 2,
61 | "always",
62 | 72
63 | ],
64 | "footer-max-line-length": [
65 | 2,
66 | "always",
67 | 72
68 | ],
69 | "scope-empty": [
70 | 2,
71 | "never"
72 | ]
73 | }
74 | },
75 | "config": {
76 | "commitizen": {
77 | "path": "cz-conventional-changelog"
78 | }
79 | },
80 | "peerDependencies": {},
81 | "husky": {
82 | "hooks": {
83 | "pre-commit": "pretty-quick --staged",
84 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
85 | }
86 | },
87 | "prettier": {
88 | "printWidth": 79,
89 | "semi": false,
90 | "singleQuote": true,
91 | "trailingComma": "es5",
92 | "proseWrap": "always"
93 | },
94 | "devDependencies": {
95 | "@commitlint/cli": "^8.0.0",
96 | "@commitlint/config-conventional": "^8.0.0",
97 | "@commitlint/travis-cli": "^8.0.0",
98 | "@semantic-release/commit-analyzer": "^6.1.0",
99 | "@semantic-release/git": "^7.0.8",
100 | "@semantic-release/github": "^5.2.10",
101 | "@semantic-release/npm": "^5.1.7",
102 | "@semantic-release/release-notes-generator": "^7.1.4",
103 | "@types/jest": "^24.0.13",
104 | "commitizen": "^3.1.1",
105 | "cz-conventional-changelog": "^2.1.0",
106 | "husky": "^2.3.0",
107 | "prettier": "^1.17.1",
108 | "pretty-quick": "^1.11.0",
109 | "remark": "^10.0.1",
110 | "semantic-release": "^15.13.12",
111 | "semantic-release-cli": "^5.1.1",
112 | "tsdx": "^0.6.1",
113 | "tslib": "^1.9.3",
114 | "typescript": "^3.5.1"
115 | },
116 | "dependencies": {
117 | "axios": "^0.19.0",
118 | "unist-util-select": "^2.0.2"
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | getProviderEndpointForLinkUrl,
3 | fetchOembed,
4 | transformLinkNodeToOembedNode,
5 | fetchOembedProviders,
6 | selectPossibleOembedLinkNodes,
7 | } from './utils'
8 |
9 | // For each node this is the process
10 | const processNode = async (node: { url: string }, providers = []) => {
11 | let mutatedNode = node
12 | try {
13 | const endpoint = getProviderEndpointForLinkUrl(node.url, providers)
14 | if (endpoint.url) {
15 | const oembedResponse = await fetchOembed(endpoint)
16 | mutatedNode = transformLinkNodeToOembedNode(node, oembedResponse)
17 | }
18 | } catch (error) {
19 | error.url = node.url
20 | throw error
21 | }
22 | return mutatedNode
23 | }
24 |
25 | async function transformer(tree: any, _file: any) {
26 | const providers = await fetchOembedProviders()
27 |
28 | const usePrefix = false
29 | const nodes = selectPossibleOembedLinkNodes(tree, usePrefix)
30 |
31 | await Promise.all(nodes.map(node => processNode(node, providers)))
32 |
33 | return tree
34 | }
35 |
36 | function attacher() {
37 | return transformer
38 | }
39 |
40 | export default attacher
41 |
--------------------------------------------------------------------------------
/src/utils/fetchOembed.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | const fetchOembed = async (endpoint: { url: any; params: any }) => {
4 | const response = await axios.get(endpoint.url, {
5 | params: {
6 | format: 'json',
7 | ...endpoint.params,
8 | },
9 | })
10 | return response.data
11 | }
12 |
13 | export default fetchOembed
14 |
--------------------------------------------------------------------------------
/src/utils/fetchOembedProviders.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | const OEMBED_PROVIDERS_URL = 'https://oembed.com/providers.json'
4 |
5 | const fetchOembededProviders = async () => {
6 | const response = await axios.get(OEMBED_PROVIDERS_URL)
7 | return response.data
8 | }
9 |
10 | export default fetchOembededProviders
11 |
--------------------------------------------------------------------------------
/src/utils/getProviderEndpointForLinkUrl.ts:
--------------------------------------------------------------------------------
1 | const getProviderEndpointForLinkUrl = (linkUrl: string, providers: any) => {
2 | const transformedEndpoint: { url: string; params: any } = {
3 | url: null,
4 | params: null,
5 | }
6 |
7 | for (const provider of providers || []) {
8 | for (const endpoint of provider.endpoints || []) {
9 | for (let schema of endpoint.schemes || []) {
10 | schema = schema.replace('*', '.*')
11 | const regExp = new RegExp(schema)
12 | if (regExp.test(linkUrl)) {
13 | transformedEndpoint.url = endpoint.url
14 | transformedEndpoint.params = {
15 | url: linkUrl,
16 | ...provider.params,
17 | }
18 | }
19 | }
20 | }
21 | }
22 |
23 | return transformedEndpoint
24 | }
25 |
26 | export default getProviderEndpointForLinkUrl
27 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | import fetchOembed from './fetchOembed'
2 | import fetchOembedProviders from './fetchOembedProviders'
3 |
4 | import getProviderEndpointForLinkUrl from './getProviderEndpointForLinkUrl'
5 | import selectPossibleOembedLinkNodes from './selectPossibleOembedLinkNodes'
6 | import transformLinkNodeToOembedNode from './transformLinkNodeToOembedNode'
7 |
8 | export {
9 | fetchOembed,
10 | fetchOembedProviders,
11 | getProviderEndpointForLinkUrl,
12 | selectPossibleOembedLinkNodes,
13 | transformLinkNodeToOembedNode,
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/selectPossibleOembedLinkNodes.ts:
--------------------------------------------------------------------------------
1 | import { selectAll } from 'unist-util-select'
2 |
3 | const selectPossibleOembedLinkNodes = (
4 | markdownAST: any,
5 | usePrefix = false
6 | ) => {
7 | let res = []
8 | if (usePrefix === true) {
9 | const nodes = selectAll(markdownAST, 'inlineCode')
10 | nodes.forEach(node => {
11 | if (node.value.startsWith('oembed:')) {
12 | const mutatedNode = node
13 | mutatedNode.url = mutatedNode.value.substring(7).trim()
14 | res.push(mutatedNode)
15 | }
16 | })
17 | } else {
18 | res = selectAll('paragraph link:only-child', markdownAST)
19 | }
20 | return res || []
21 | }
22 |
23 | export default selectPossibleOembedLinkNodes
24 |
--------------------------------------------------------------------------------
/src/utils/transformLinkNodeToOembedNode.ts:
--------------------------------------------------------------------------------
1 | const transformLinkNodeToOembedNode = (node, oembedResult) => {
2 | if (oembedResult.html) {
3 | node.type = 'html'
4 | node.value = oembedResult.html
5 | delete node.children
6 | } else if (oembedResult.type === 'photo') {
7 | node.type = 'html'
8 | node.value = `
9 |
14 | `
15 | delete node.children
16 | }
17 |
18 | return node
19 | }
20 |
21 | export default transformLinkNodeToOembedNode
22 |
--------------------------------------------------------------------------------
/test/index.test.ts:
--------------------------------------------------------------------------------
1 | import remark from 'remark'
2 | import oembed from '../src'
3 |
4 | const markdown = `
5 | Hey this is a nice youtube video about making modern react apps with gatsby:
6 |
7 | https://www.youtube.com/watch?v=GN0xHSk2P8Q
8 |
9 | Check it out 👆
10 | `
11 | const markdownWithEmbed = `Hey this is a nice youtube video about making modern react apps with gatsby:
12 |
13 |
14 |
15 | Check it out 👆
16 | `
17 |
18 | test('oembed', async () => {
19 | await new Promise(resolve => {
20 | remark()
21 | .use(oembed)
22 | .process(markdown, function(err, file) {
23 | if (err) throw err
24 | resolve(expect(String(file)).toEqual(markdownWithEmbed))
25 | })
26 | })
27 | })
28 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["src", "types"],
3 | "compilerOptions": {
4 | "target": "es5",
5 | "module": "esnext",
6 | "lib": ["esnext"],
7 | "importHelpers": true,
8 | "declaration": true,
9 | "sourceMap": true,
10 | "rootDir": "./",
11 | // "strict": true,
12 | // "noImplicitAny": true,
13 | // "strictNullChecks": true,
14 | // "strictFunctionTypes": true,
15 | // "strictPropertyInitialization": true,
16 | // "noImplicitThis": true,
17 | // "alwaysStrict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noImplicitReturns": true,
21 | "noFallthroughCasesInSwitch": true,
22 | "moduleResolution": "node",
23 | "baseUrl": "./",
24 | "paths": {
25 | "*": ["src/*", "types/*", "node_modules/*"]
26 | },
27 | "esModuleInterop": true
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/types/unist-util-select.d.ts:
--------------------------------------------------------------------------------
1 | export function matches(selector: any, node: any): any
2 | export function select(selector: any, node: any): any
3 | export function selectAll(selector: any, node: any): any
4 |
--------------------------------------------------------------------------------