Skip to content

πŸ”— Remark plugin to convert URLs to embed code in markdown.

License

Notifications You must be signed in to change notification settings

johnstonmatt/core

Β 
Β 

Repository files navigation

@remark-embedder/core πŸ”—

Remark plugin to convert URLs to embed code in markdown.


Build Status Code Coverage version downloads MIT License All Contributors PRs Welcome Code of Conduct

The problem

I used to write blog posts on Medium. When I moved on to my own site, I started writing my blog posts in markdown and I missed the ability to just copy a URL (like for a tweet), paste it in the blog post, and have Medium auto-embed it for me.

This solution

This allows you to transform a link in your markdown into the embedded version of that link. It's a remark plugin (the de-facto standard markdown parser). You provide a "transformer" the the plugin does the rest.

Table of Contents

Installation

This module is distributed via npm which is bundled with node and should be installed as one of your project's dependencies:

npm install --save @remark-embedder/core

Usage

Here's the most complete, simplest, practical example I can offer:

import remark from 'remark'
import html from 'remark-html'
import remarkEmbedder from '@remark-embedder/core'
// or, if you're using CJS:
// const {default: remarkEmbedder} = require('@remark-embedder/core')

const codesandboxTransformer = {
  name: 'Codesandbox',
  // shouldTransform can also be async
  shouldTransform(url) {
    const {host, pathname} = new URL(url)
    return (
      ['codesandbox.io', 'www.codesandbox.io'].includes(host) &&
      pathname.includes('/s/')
    )
  },
  // getHTML can also be async
  getHTML(url) {
    const iframeUrl = url.replace('/s/', '/embed/')
    return `<iframe src="${iframeUrl}" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"></iframe>`
  },
}

const exampleMarkdown = `
This is a codesandbox:

https://codesandbox.io/s/css-variables-vs-themeprovider-df90h
`

async function go() {
  const result = await remark()
    .use(remarkEmbedder, {
      transformers: [codesandboxTransformer],
    })
    .use(html)
    .process(exampleMarkdown)

  console.log(result.toString())
  // logs:
  // <p>This is a codesandbox:</p>
  // <iframe src="https://codesandbox.io/embed/css-variables-vs-themeprovider-df90h" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"></iframe>
}

Options

The transformers option is required (otherwise the plugin won't do anything), but there are a few optional options as well.

transformers: Array<Transformer>

The transformer objects are where you convert a link to it's HTML embed representation.

name: string

This is the name of your transformer. It's required because it's used in error messages. I suggest you use your module name if you're publishing this transformer as a package so people know where to open issues if there's a problem.

shouldTransform: (url: string) => boolean | Promise<boolean>

Only URLs on their own line will be transformed, but for your transformer to be called, you must first determine whether a given URL should be transformed by your transformer. The shouldTransform function accepts the URL string and returns a boolean. true if you want to transform, and false if not.

Typically this will involve checking whether the URL has all the requisite information for the transformation (it's the right host and has the right query params etc.).

You might also consider doing some basic checking, for example, if it looks a lot like the kind of URL that you would handle, but is missing important information and you're confident that's a mistake, you could log helpful information using console.log.

getHTML: (url: string, config: unknown) => string | null | Promise<string | null>

The getHTML function accepts the url string and a config option (learn more from the services option). It returns a string of HTML or a promise that resolves to that HTML. This HTML will be used to replace the link.

It's important that the HTML you return only has a single root element.

// This is ok βœ…
return `<iframe src="..."></iframe>`

// This is not ok ❌
return `<blockquote>...</blockquote><a href="...">...</a>`

// But this would be ok βœ…
return `<div><blockquote>...</blockquote><a href="...">...</a></div>`

Some services have endpoints that you can use to get the embed HTML (like twitter for example).

cache: Map<string, string | null>

Because some of your transforms may make network requests to retrieve the HTML, we support providing a cache. You could pass new Map(), but that would only be useful during the life of your process (which means it probably wouldn't be all that helpful). If you want to persist this to the file system (so it works across compilations), you could use something like lowdb.

The cache key is set to remark-embedder:${transformerName}:${urlString} and the value is the resulting HTML.

Also, while technically we treat the cache as a Map, all we really care about is that the cache has a get and a set and we await both of those calls to support async caches (like gatsby's).

Configuration

You can provide configuration for your transformer by specifying the transformer as an array. This may not seem very relevant if you're creating your own custom transformer where you can simply edit the code directly, but if the transformer is published to npm then allowing users to configure your transformer externally can be quite useful (especially if your transformer requires an API token to request the embed information like with instagram).

Here's a simple example:

const codesandboxTransformer = {
  name: 'Codesandbox',
  shouldTransform(url) {
    // ...
  },
  getHTML(url, config) {
    // ... config(url).height === '600px'
  },
}

const getCodesandboxConfig = url => ({height: '600px'})

const result = await remark()
  .use(remarkEmbedder, {
    transformers: [
      someUnconfiguredTransformer, // remember, not all transforms need/accept configuration
      [codesandboxTransformer, getCodesandboxConfig],
    ],
  })
  .use(html)
  .process(exampleMarkdown)

The config is typed as unknown so transformer authors have the liberty to set it as anything they like. The example above uses a function, but you could easily only offer an object. Personally, I think using the function gives the most flexibility for folks to configure the transform. In fact, I think a good pattern could be something like the following:

const codesandboxTransformer = {
  name: 'Codesandbox',
  shouldTransform(url) {
    // ...
  },
  // default config function returns what it's given
  getHTML(url, config = html => html) {
    const html = '... embed html here ...'
    return config({url, html})
  },
}

const getCodesandboxConfig = ({url, html}) => {
  if (hasSomeSpecialQueryParam(url)) {
    return modifyHTMLBasedOnQueryParam(html)
  }
  return html
}

const result = await remark()
  .use(remarkEmbedder, {
    transformers: [
      someUnconfiguredTransformer, // remember, not all transforms need/accept configuration
      [codesandboxTransformer, getCodesandboxConfig],
    ],
  })
  .use(html)
  .process(exampleMarkdown)

This pattern inverts control for folks who like what your transform does, but want to modify it slightly. If written like above (return config(...)) it could even allow the config function to be async.

Making a transformer module

Here's what our simple example would look like as a transformer module:

import type {Transformer} from '@remark-embedder/core'

type Config = (url: string) => {height: string}
const getDefaultConfig = () => ({some: 'defaultConfig'})

const transformer: Transformer<Config> = {
  // this should be the name of your module:
  name: '@remark-embedder/transformer-codesandbox',
  shouldTransform(url) {
    // do your thing and return true/false
    return false
  },
  getHTML(url, getConfig = getDefaultConfig) {
    // get the config...
    const config = getConfig(url)
    // do your thing and return the HTML
    return '<iframe>...</iframe>'
  },
}

export default transformer
export type {Config}

If you're not using TypeScript, simply remove the type import and the : Transformer bit.

If you're using CommonJS, then you'd also swap export default transformer for module.exports = transformer

NOTE: If you're using export default then CommonJS consumers will need to add a .default to get your transformer with require.

To take advantage of the config type you export, the user of your transform would need to cast their config when running it through remark. For example:

// ...
import type {Config as CodesandboxConfig} from '@remark-embedder/transformer-codesandbox'
import transformer from '@remark-embedder/transformer-codesandbox'

// ...

remark().use(remarkEmbedder, {
  transformers: [codesandboxTransformer, config as CodesandboxConfig],
})
// ...

Inspiration

This whole plugin was extracted out of Kent C. Dodds' Gatsby website into gatsby-remark-embedder by MichaΓ«l De Boey and then Kent extracted the remark plugin into this core package.

Other Solutions

  • MDX Embed: Allows you to use components in MDX files for common services. A pretty different approach to solving a similar problem.

Issues

Looking to contribute? Look for the Good First Issue label.

πŸ› Bugs

Please file an issue for bugs, missing documentation, or unexpected behavior.

See Bugs

πŸ’‘ Feature Requests

Please file an issue to suggest new features. Vote on feature requests by adding a πŸ‘. This helps maintainers prioritize what to work on.

See Feature Requests

Contributors ✨

Thanks goes to these people (emoji key):

Kent C. Dodds
Kent C. Dodds

πŸ’» πŸ“– πŸš‡ ⚠️

MichaΓ«l De Boey

πŸ› πŸ’» πŸ“– ⚠️

This project follows the all-contributors specification. Contributions of any kind welcome!

LICENSE

MIT

About

πŸ”— Remark plugin to convert URLs to embed code in markdown.

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published

Languages

  • TypeScript 99.1%
  • JavaScript 0.9%