Skip welcome & menu and move to editor
Welcome to JS Bin
Load cached copy from
 
<!DOCTYPE html>
<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("&#x3C;", '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

Dismiss x
public
Bin info
anonymouspro
0viewers