├── .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 | --------------------------------------------------------------------------------