<html>
<head>
<!-- scroll to the bottom to actually see the tests -- most of this is setup -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<!--
to specify externals: https://github.com/ije/esm.sh#specify-external-dependencies
esm.sh had an error:
Failed to resolve module specifier "remark-parse". Relative references must start with either "/", "./", or "../".
It's possible that jsbin doesn't support import maps.
-->
<script type="importmap">
{
"import": {
"unified": "https://esm.sh/*unified",
"remarkParse": "https://esm.sh/*remark-parse",
"remarkRehype": "https://esm.sh/*remark-rehype",
"rehypeSanitize": "https://esm.sh/*rehype-sanitize",
"rehypeStringify": "https://esm.sh/*rehype-stringify",
"unist-util-flatmap": "https://esm.sh/unist-util-flatmap",
"uuid": "https://esm.sh/uuid",
"inflection": "https://esm.sh/inflection",
"rehypeRaw": "https://esm.sh/rehype-raw"
}
}
</script>
<script type="module">
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
//
// This is all setup, skip to the next section
//
// This is all boilerplace for (maybe one day) upgraded and shared
// code between these projects:
// - https://limber.glimdown.com/
// - https://docfy.dev
//
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
// import {unified} from 'unified';
// import remarkParse from 'remark-parse';
// import remarkRehype from 'remark-rehype';
// import rehypeSanitize from 'rehype-sanitize';
// import rehypeStringify from 'rehype-stringify';
import {unified} from 'https://esm.sh/unified';
import remarkParse from 'https://esm.sh/remark-parse';
import remarkRehype from 'https://esm.sh/remark-rehype';
import rehypeSanitize from 'https://esm.sh/rehype-sanitize';
import rehypeStringify from 'https://esm.sh/rehype-stringify';
import rehypeRaw from 'https://esm.sh/rehype-raw';
import flatMap from 'https://esm.sh/unist-util-flatmap';
import {visit} from 'https://esm.sh/unist-util-visit';
import {inspect} from "https://esm.sh/unist-util-inspect@8"
import {toHtml} from "https://esm.sh/hast-util-to-html@8"
import { v5 as uuidv5 } from 'https://esm.sh/uuid';
import * as inflection from 'https://esm.sh/inflection';
const example = `` +
`
# Title
some *bold* and _italic_ text.
<AComponent />
<With as |a|>
<a data-not-an-anchor>
<:block>
block content here
</:block>
</a>
</With>
\`\`\`gjs live
<template>
{{log "hello world"}}
</template>
\`\`\`
`;
///////////////////////////////////////////////////////////////////////////////
const NAMESPACE = '926f034a-f480-4112-a363-321244f4e5de';
const DEFAULT_PREFIX = 'ember-repl';
/**
* from: https://github.com/NullVoxPopuli/ember-repl/blob/main/addon/utils.ts
* For any given code block, a reasonably stable name can be
* generated.
* This can help with cacheing previously compiled components,
* and generally allowing a consumer to derive "known references" to user-input
*/
function nameFor(code, prefix = DEFAULT_PREFIX) {
let id = uuidv5(code, NAMESPACE);
return `${prefix ? `${prefix}-` : ''}${id}`;
}
/**
* Returns the text for invoking a component with a given name.
* It is assumed the component takes no arguments, as would be the
* case in REPLs / Playgrounds for the "root" component.
*/
function invocationOf(name) {
// assert(
// `You must pass a name to invocationOf. Received: \`${name}\``,
// typeof name === 'string' && name.length > 0
// );
if (name.length === 0) {
throw new Error(`name passed to invocationOf must have non-0 length`);
}
return `<${invocationName(name)} />`;
}
function invocationName(name) {
// this library is bad. ugh, I want `@ember/string` as a v2 addon.
return inflection.camelize(name.replaceAll(/-/g, '_'));
}
// From: https://github.com/NullVoxPopuli/limber/blob/77d67a0b6bcc9ebe6621906149de970c8a821bde/frontend/app/components/limber/output/compiler/formats/-compile/markdown-to-ember.ts#L121
// Extracts code blocks with specific tags into "data" which can be read by a "renderer"
// Demo: https://limber.glimdown.com (pick one of the non-default demos)
const ALLOWED_LANGUAGES = ['gjs', 'hbs'];
// TODO: extract and publish remark plugin
function liveCodeExtraction(options = {}) {
let { copyComponent, snippets, demo } = options;
let { classList: snippetClasses } = snippets || {};
let { classList: demoClasses } = demo || {};
snippetClasses ??= [];
demoClasses ??= [];
return function transformer(tree, file) {
return flatMap(tree, (node) => {
if (node.type !== 'code') return [node];
let { meta, lang, value } = node;
meta = meta?.trim();
if (!meta || !lang) return [node];
if (!ALLOWED_LANGUAGES.includes(lang)) return [node];
// apparently my browser targets don't support ??= yet
file.data.liveCode = file.data.liveCode || [];
let code = value.trim();
let name = nameFor(code);
let invocation = invocationOf(name);
let invokeNode = {
type: 'html',
value: `<div class="${demoClasses}">${invocation}</div>`,
};
let wrapper = {
// <p> is wrong, but I think I need to make a rehype plugin instead of remark for this
type: 'paragraph',
data: {
hProperties: { className: snippetClasses },
},
children: [node],
};
if (options.copyComponent) {
wrapper.children.push({
type: 'html',
value: copyComponent,
});
}
file.data.liveCode.push({
lang,
name,
code,
});
if (meta === 'live preview below') {
return [wrapper, invokeNode];
}
if (meta === 'live preview') {
return [invokeNode, wrapper];
}
if (meta === 'live') {
return [invokeNode];
}
return [wrapper];
});
};
}
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
//
// TESTS HERE
//
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
const liveCodeConfig = {
snippets: {
classList: ['glimdown-snippet', 'relative'],
},
demo: {
classList: ['glimdown-render'],
},
copyComponent: '<Limber::CopyMenu />',
};
const stringifyConfig = {
collapseEmptyAttributes: true,
closeSelfClosing: true,
allowParseErrors: true,
allowDangerousCharacters: true,
allowDangerousHtml: true,
characterReferences: { '<': '' },
entities: { subset: ['<'], useNamedReferences: true },
}
let inspector = () => function (options = {}) {
return function(tree) {
let label = options?.label ? `${options.label}\n` : '';
console.log(label, inspect(tree));
}
}
async function main() {
let withSanitize = await unified()
.use(remarkParse)
.use(liveCodeExtraction, liveCodeConfig)
.use(remarkRehype)
.use(rehypeSanitize)
.use(rehypeStringify, stringifyConfig)
.process(example);
let withoutSanitize = await unified()
.use(remarkParse)
.use(liveCodeExtraction, liveCodeConfig)
.use(remarkRehype)
.use(rehypeStringify, stringifyConfig)
.process(example);
let withRehypeRaw = await unified()
.use(remarkParse)
.use(liveCodeExtraction, liveCodeConfig)
.use(remarkRehype, {allowDangerousHtml: true})
.use(rehypeRaw, { })
.use(rehypeStringify, stringifyConfig)
.process(example)
let bypassGlimmer = await unified()
.use(remarkParse)
.use(inspector(), { label: 'Parsed Markdown' })
.use(liveCodeExtraction, liveCodeConfig)
.use(remarkRehype, {allowDangerousHtml: true})
.use(() => (tree) => {
visit(tree, ['text', 'raw'], function(node) {
// definitively not the better way, but this is supposed to detect "glimmer" nodes
if (node.value.match(/<\/?[A-Z:].*>/g)) {
node.type = 'glimmer_raw';
}
})
})
.use(rehypeRaw, { passThrough: ['glimmer_raw'] })
.use(() => (tree) => {
visit(tree, 'glimmer_raw', (node) => { node.type = 'raw' });
})
.use(rehypeStringify, stringifyConfig)
.process(example)
let outputSanitize = document.querySelector('#sanitized');
let outputUntrusted = document.querySelector('#untrusted');
let outputRaw = document.querySelector('#untrusted-raw');
let outputManual = document.querySelector('#manual');
let outputBypassGlimmer = document.querySelector('#glimmer-bypass');
outputSanitize.innerText = String(withSanitize);
outputUntrusted.innerText = String(withoutSanitize);
outputRaw.innerText = String(withRehypeRaw);
outputManual.innerText = String(withRehypeRaw).replaceAll(new RegExp("<", 'g'), '<');
outputBypassGlimmer.innerText = String(bypassGlimmer);
let liveCode = (withSanitize.data).liveCode || [];
let demos = document.querySelector('#demos');
demos.innerText = JSON.stringify(liveCode, null, 3);
document.getElementById('loader').remove();
}
main();
</script>
<style>
body {
display: grid;
gap: 0.5rem;
}
details {
padding: 0.5rem;
}
.incorrect {
border: 2px solid #aa0000;
}
.incorrect legend:before,
.incorrect summary:before {
content: "❌ ";
}
.better {
border: 2px solid #aaaa00;
}
.better legend:before,
.better summary:before {
content: "⚠️ ";
}
.good {
border: 2px solid green;
}
.good legend:before,
.good summary:before {
content: "✅ ";
}
</style>
</head>
<body>
<h1>Unified + Remark Tests</h1>
<div id="loader" style="padding: 1rem;">Loading...</div>
Remark and Rehype will not work with this syntax natively. Need custom parser / stringifier.<br />
<a href="https://github.com/orgs/unifiedjs/discussions/213">Discussion here.</a> Currently, this approach uses regex hacks (as does remark-hbs).
<h2>Tests / Approaches</h2>
<details class="good" open>
<summary>bypass glimmer</summary>
<pre id="glimmer-bypass"></pre>
</details>
<details class="better">
<summary>manual regex replace</summary>
<pre id="manual"></pre>
</details>
<details class="better">
<summary>without rehypeSanitize and with rehypeRaw</summary>
<pre id="untrusted-raw"></pre>
</details>
<details class="incorrect">
<summary>without rehypeSanitize</summary>
<pre id="untrusted"></pre>
</details>
<details class="incorrect">
<summary>with rehypeSanitize</summary>
<pre id="sanitized"></pre>
</details>
<h2>Fenced Demo Output</h2>
<pre id="demos"></pre>
<ul>
<li>lang: the language for which to choose which compiler pipeline to use to render the demo</li>
<li>name: is the kebab-case name of the component to invoke to render this demo</li>
<li>code: the extracted code to pass to a compiler pipeline, such as Babel</li>
</ul>
</body>
</html>
Output
You can jump to the latest bin by adding /latest
to your URL
Keyboard Shortcuts
Shortcut | Action |
---|---|
ctrl + [num] | Toggle nth panel |
ctrl + 0 | Close focused panel |
ctrl + enter | Re-render output. If console visible: run JS in console |
Ctrl + l | Clear the console |
ctrl + / | Toggle comment on selected lines |
ctrl + ] | Indents selected lines |
ctrl + [ | Unindents selected lines |
tab | Code complete & Emmet expand |
ctrl + shift + L | Beautify code in active panel |
ctrl + s | Save & lock current Bin from further changes |
ctrl + shift + s | Open the share options |
ctrl + y | Archive Bin |
Complete list of JS Bin shortcuts |
JS Bin URLs
URL | Action |
---|---|
/ | Show the full rendered output. This content will update in real time as it's updated from the /edit url. |
/edit | Edit the current bin |
/watch | Follow a Code Casting session |
/embed | Create an embeddable version of the bin |
/latest | Load the very latest bin (/latest goes in place of the revision) |
/[username]/last | View the last edited bin for this user |
/[username]/last/edit | Edit the last edited bin for this user |
/[username]/last/watch | Follow the Code Casting session for the latest bin for this user |
/quiet | Remove analytics and edit button from rendered output |
.js | Load only the JavaScript for a bin |
.css | Load only the CSS for a bin |
Except for username prefixed urls, the url may start with http://jsbin.com/abc and the url fragments can be added to the url to view it differently. |