Example Test
Backtick fences work normally:
Backtick fences work normally:
console.log( "This doesn't run" );
this.$customToken();
// Single line comment are parsed as inline markdown.
Squiggle fences will also run the code:
import GUI from 'lil-gui';
const gui = new GUI();
gui.add( document, 'title' );
This comment automatically embeds an external file:
../../scripts/examples.js
import glob from 'glob';
import markdownit from 'markdown-it';
import hljs from 'highlight.js';
import hbs from 'handlebars';
import rimraf from 'rimraf';
import fs from 'fs';
import { join } from 'path';
import pkg from '../package.json';
// deletes generated index.html files from markdown-based examples
const CLEAN = process.argv.includes( '--clean' );
// creates a markdown parser with a custom syntax highlighting pass for code blocks
const md = getMarkdown();
const TEMPLATE = 'homepage/example.hbs.html';
const template = hbs.compile( fs.readFileSync( TEMPLATE, 'utf-8' ) );
// markdown examples are expected to have a .gitignore containing index.html
glob( 'examples/*/.gitignore', ( err, files ) => {
if ( err ) {
return console.error( err );
}
const dirs = files.map( f => f.replace( '/.gitignore', '' ) );
if ( CLEAN ) {
return clean( dirs );
}
console.time( 'examples' );
dirs.forEach( makeExample );
console.timeEnd( 'examples' );
} );
// regexp for special markdown directives
const BACKTICK_FENCE = /```\S+\n[\s\S]*?\n```/gmi;
const SQUIGGLE_FENCE = /~~~js([\s\S]*?)\n~~~/gmi;
const SHOW_EXTERNAL = /<!-- show (\S+) -->/gmi;
const INCLUDE_EXTERNAL = /<!-- include (\S+) -->/gmi;
const HEADING = /^#+ ([\S\s]*?)$/mi;
function makeExample( dir ) {
const slug = dir.substr( dir.lastIndexOf( '/' ) + 1 );
let body;
// read example contents from examples/xyz/xyz.md
try {
body = fs.readFileSync( join( dir, slug + '.md' ), 'utf-8' );
} catch ( e ) {
console.error( dir, `doesn't have ${slug}.md` );
return;
}
// find title
let title = dir;
try {
title = body.match( HEADING )[ 1 ];
} catch ( e ) {
console.error( dir, "doesn't have a title" );
}
// backtick code blocks are broken out of <main> for full bleed style
body = body.replace( BACKTICK_FENCE, block => scriptSection( block ) );
// same for squiggle blocks, but they also get a header, and are executed
body = body.replace( SQUIGGLE_FENCE, ( block, code ) => {
// remove whitespace that could confuse markdown
code = code.replace( /\n{2,}/g, '\n' );
const script = `<script type="module">${code}</script>`;
const section = scriptSection( block, 'This page:' );
return script + '\n' + section;
} );
// external code blocks aren't executed, but they get a header like squiggle blocks
body = body.replace( SHOW_EXTERNAL, ( _, scriptPath ) => {
const file = fs.readFileSync( join( dir, scriptPath ), 'utf-8' );
const block = '```js\n' + file.trim() + '\n```';
return scriptSection( block, `<code>${scriptPath}</code>` );
} );
// render markdown
body = md.render( body );
// include external html after markdown is rendered
body = body.replace( INCLUDE_EXTERNAL, ( _, path ) => {
return fs.readFileSync( join( dir, path ), 'utf-8' );
} );
// render hbs.html
let html = template( { title, body, pkg } );
// clean up any lingering mains from scriptSection
html = html.replace( /<main>\s*<\/main>/gmi, '' );
fs.writeFileSync( join( dir, 'index.html' ), html );
}
// breaks <pre>'s out of <main> so they can be 100% width, optionally applies header
function scriptSection( fenceBlock, header ) {
let section = '<section class="script">';
if ( header ) section += `<header>${header}</header>`;
section += '\n\n';
section += fenceBlock;
section += '\n\n';
section += '</section>';
return `</main>${section}<main>`;
}
function getMarkdown() {
const CUSTOM_TOKENS = [
/\$customToken/,
/\$isPrimitive/,
/\$constructor/,
/\$widget/,
/\$onFinishChange/,
/\$updateDisplay/,
/\$copy/,
/\$compare/,
/\$onChange/,
/\$id/,
/\$style/,
/\$value/,
/(?<=CustomController\.)register/
].map( re => new RegExp( re, 'g' ) );
const SINGLE_LINE_COMMENT = /^(\s*<span class="hljs-comment">)(.*)(<\/span>)/gm;
return markdownit( {
html: true,
highlight: function( code, language ) {
if ( !language || !hljs.getLanguage( language ) ) return '';
let v = hljs.highlight( code, { language } ).value;
// wraps whole word matches for any CUSTOM_TOKEN in span.hljs-custom
for ( let token of CUSTOM_TOKENS ) {
v = v.replace( token, '<span class="hljs-custom">$&</span>' );
}
// render single line comments as inline markdown
v = v.replace( SINGLE_LINE_COMMENT, ( ...match ) => {
return match[ 1 ] + md.renderInline( match[ 2 ] ) + match[ 3 ];
} );
return v;
}
} );
}
function clean( dirs ) {
dirs
.map( dir => join( dir, 'index.html' ) )
.filter( fs.existsSync )
.map( index => rimraf( index, err => {
if ( err ) {
return console.error( err );
}
console.log( 'deleted', index );
} ) );
}