UNPKG

remark-codesandbox

Version:

🎩 Create CodeSandbox directly from code blocks

223 lines (186 loc) • 6.59 kB
'use strict'; const visit = require('unist-util-visit'); const is = require('unist-util-is'); const toString = require('mdast-util-to-string'); const u = require('unist-builder'); const { getParameters } = require('codesandbox/lib/api/define'); const fetch = require('isomorphic-fetch'); const getTemplate = require('./getTemplate'); const { parseMeta, mergeQuery, toBasePath, mergeStyle } = require('./utils'); let URLSearchParams; if (typeof window === 'undefined') { // URLSearchParams is added to the global object in node v10 URLSearchParams = global.URLSearchParams || require('url').URLSearchParams; } else { URLSearchParams = window.URLSearchParams; } const DEFAULT_CUSTOM_TEMPLATES = { react: { extends: 'new', }, 'react-component': { extends: 'new', entry: 'src/App.js', }, }; const PLUGIN_ONLY_QUERY_PARAMS = ['overrideEntry', 'entry', 'style']; function codesandbox(options = {}) { const templates = new Map(); const mode = options.mode || 'meta'; const customTemplates = { ...DEFAULT_CUSTOM_TEMPLATES, ...(options.customTemplates || {}), }; const defaultQuery = mode === 'iframe' ? { fontsize: '14px', hidenavigation: 1, theme: 'dark', } : undefined; const autoDeploy = options.autoDeploy || false; let baseQuery = defaultQuery; if (typeof options.query !== 'undefined') { baseQuery = options.query; } else if (typeof options.iframeQuery !== 'undefined') { // DEPRECATED: To support the legacy iframeQuery key console.warn( `options.iframeQuery is now deprecated and will be removed in the upcoming version, please use options.query instead.` ); baseQuery = options.iframeQuery; } return async function transformer(tree, file) { let title; const codes = []; // Walk the tree once and record everything we need visit(tree, (node, index, parent) => { if (!title && is(node, ['heading', { depth: 1 }])) { title = toString(node); } else if (is(node, 'code')) { codes.push([node, index, parent]); } }); for (const [node, _, parent] of codes) { const meta = parseMeta(node.meta || ''); const sandboxMeta = meta.codesandbox; // No `codesandbox` meta set, skipping if (!sandboxMeta) { continue; } const [templateID, queryString] = sandboxMeta.split('?'); const template = await getTemplate( templates, templateID, customTemplates, file ); template.title = title || template.title; const query = mergeQuery(baseQuery, template.query, queryString); const entryPath = query.has('entry') ? toBasePath(query.get('entry')) : template.entry; // If there is no predefined `module` key, then we assign it to the entry file if (!query.has('module')) { query.set( 'module', // `entry` doesn't start with leading slash, but `module` requires it entryPath.startsWith('/') ? entryPath : `/${entryPath}` ); } const overrideEntry = query.get('overrideEntry'); const style = query.get('style') || ''; // Remove any options that are only for the plugin and not relevant to CodeSandbox PLUGIN_ONLY_QUERY_PARAMS.forEach((param) => { query.delete(param); }); if(!template.files[entryPath]) { throw new Error(`Entry "${entryPath}" is not present in template "${templateID}".`); } let entryFileContent = template.files[entryPath].content; if (!overrideEntry) { entryFileContent = node.value; } else if (overrideEntry !== 'false') { const [overrideRangeStart, overrideRangeEnd] = overrideEntry.split('-'); const lines = entryFileContent.split('\n'); entryFileContent = [ ...lines.slice(0, Number(overrideRangeStart) - 1), node.value, ...(overrideRangeEnd === '' ? [] : lines.slice(Number(overrideRangeEnd))), ].join('\n'); } const parameters = getParameters({ files: { ...template.files, [entryPath]: { content: entryFileContent }, }, }); let url; if (autoDeploy) { const { sandbox_id } = await fetch( 'https://codesandbox.io/api/v1/sandboxes/define', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ parameters, json: 1 }), } ).then((res) => res.json()); url = `https://codesandbox.io/s/${sandbox_id}?${query.toString()}`; } else { url = `https://codesandbox.io/api/v1/sandboxes/define?${new URLSearchParams( { parameters, query, } ).toString()}`; } switch (mode) { case 'button': { const button = u('paragraph', [ u('link', { url }, [ u('image', { url: 'https://codesandbox.io/static/img/play-codesandbox.svg', alt: 'Edit on CodeSandbox', }), ]), ]); // Insert the button directly after the code block const index = parent.children.indexOf(node); parent.children.splice(index + 1, 0, button); break; } case 'iframe': { // Construct the iframe AST const iframe = u('html', { value: `<iframe src="${autoDeploy ? url.replace('/s/', '/embed/') : `${url}&embed=1`}" style="${mergeStyle( 'width:100%; height:500px; border:0; border-radius:4px; overflow:hidden;', style )}" title="${template.title || ''}" allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin" ></iframe>`, }); // Replace the code block with the iframe const index = parent.children.indexOf(node); parent.children.splice(index, 1, iframe); break; } case 'meta': default: { // TODO: We might still want to make this happen regardless of the mode? node.data = node.data || {}; node.data.hProperties = node.data.hProperties || {}; node.data.codesandboxUrl = url; node.data.hProperties.dataCodesandboxUrl = url; break; } } } }; } module.exports = codesandbox;