import { Remarkable } from "remarkable" ;
import assert from "assert" ;
// Node is absolutely stupid. The remarkable project has an esm setting which
// specifies the dist/esm directory, but it completely ignores it and complains.
import { linkify } from "remarkable/dist/cjs/linkify.js" ;
import Prism from "../static/js/prism.cjs" ;
import _ from "lodash" ;
import slugify from "slugify" ;
import fs from "fs" ;
import PATH from "path" ;
import child _process from "child_process" ;
_ . templateSettings . interpolate = /{{([\s\S]+?)}}/g ;
class Ork {
constructor ( base , metadata ) {
// base needs to go away, but for now it's where the root of the db is
if ( metadata . code ) {
this . code _path = ` ${ base } ${ metadata . code } ` ;
} else {
this . code _path = false ;
}
}
js ( file = 'code.js' ) {
assert ( this . code _path , "You are using ork but no code path set in metadata." ) ;
return ` \` \` \` javascript \n ${ fs . readFileSync ( ` ${ this . code _path } / ${ file } ` ) . toString ( ) . trim ( ) } \n \` \` \` `
}
node ( file = 'code.js' , args = '' ) {
assert ( this . code _path , "You are using ork but no code path set." ) ;
let cmd = ` node " ${ file } " ${ args } ` ;
let result = child _process . execSync ( cmd , { cwd : this . code _path } ) ;
return ` \` \` \` shell-session \n $ ${ cmd } \n ${ result . toString ( ) . trim ( ) } \n \` \` \` `
}
}
const count _lines = ( str ) => {
let match = str . match ( /\n(?!$)/g ) ;
return match ? match . length + 1 : 1 ;
}
const line _number _spans = ( str ) => {
let count = count _lines ( str ) ;
let spans = new Array ( count + 1 ) . join ( '<span></span>' ) ;
return ` <span aria-hidden="true" class="line-numbers-rows"> ${ spans } </span> ` ;
}
/ * *
* Runs Prism on the string , defaulting to the "markup"
* language which will encode most code correctly even
* if you don ' t specify a language after the ` ` `
* * /
const run _prism = ( str , lang = "markup" ) => {
assert ( lang !== undefined , "Language can't be undefined." ) ;
// do the check again since the language might not exist
if ( Prism . languages [ lang ] ) {
return Prism . highlight ( str , Prism . languages [ lang ] , lang ) ;
} else {
console . error ( "Unknown language" , lang ) ;
return Prism . highlight ( str , Prism . languages [ "markup" ] , "markup" ) ;
}
}
export const highlight = ( str , lang ) => {
return run _prism ( str , lang ) + line _number _spans ( str ) ;
}
/* A separate renderer just for the titles that doesn't need anything else. */
export const title _render = new Remarkable ( 'full' ) . use ( rem => {
let pass = ( tokens , idx ) => '' ;
rem . renderer . rules . paragraph _open = pass ;
rem . renderer . rules . paragraph _close = pass ;
} ) ;
export const split = ( raw _md ) => {
let [ metadata , ... body ] = raw _md . split ( '------' ) ;
metadata = JSON . parse ( metadata ) ;
body = body . join ( '------' ) ;
return [ metadata , body ] ;
}
const null _cb = ( metadata , body ) => body ;
export const create _renderer = ( toc ) => {
const renderer = new Remarkable ( 'full' , {
html : true ,
highlight
} ) ;
renderer . use ( linkify ) ;
renderer . use ( rem => {
rem . renderer . rules . heading _open = ( tokens , idx ) => {
let level = tokens [ idx ] . hLevel ;
let content = tokens [ idx + 1 ] . content ;
let slug = slugify ( content , { lower : true , strict : true } ) ;
toc . push ( { level , content , slug } ) ;
return ` <h ${ level } id=" ${ slug } "> ` ;
}
rem . renderer . rules . heading _close = ( tokens , idx ) => {
return ` </h ${ tokens [ idx ] . hLevel } > \n ` ;
}
} ) ;
renderer . use ( rem => {
const orig _open = rem . renderer . rules . link _open ;
const orig _close = rem . renderer . rules . link _close ;
rem . renderer . rules . link _open = ( tokens , idx , options ) => {
return orig _open ( tokens , idx , options ) ;
}
rem . renderer . rules . link _close = ( tokens , idx , options ) => {
return orig _close ( tokens , idx , options ) ;
}
} ) ;
return renderer ;
}
export const render = ( raw _md , cb = null _cb ) => {
let toc = [ ] ;
let [ metadata , body ] = split ( raw _md ) ;
const renderer = create _renderer ( toc ) ;
let content = renderer . render ( cb ( metadata , body ) ) ;
// now we can use the TOC to figure out a title
metadata . title = toc [ 0 ] . content ;
try {
// finally, run the renderer on all of the TOC
toc . forEach ( t => t . html = title _render . render ( t . content ) ) ;
} catch ( error ) {
console . error ( error ) ;
}
metadata . original _md = body ;
return { toc , content , metadata } ;
}
export const render _with _code = ( source _dir , md _file ) => {
// get the file without path or extension
const tail _no _ext = PATH . basename ( md _file , PATH . extname ( md _file ) ) ;
// this is a closure that is passed to render to run Ork for the code wrapping
const load _code = ( metadata , body ) => {
return _ . template ( body ) ( { fs , ork : new Ork ( source _dir , metadata ) } ) ;
}
// load the .md file contents
let raw _md = fs . readFileSync ( md _file ) ;
let { toc , content , metadata } = render ( raw _md . toString ( ) , load _code ) ;
metadata . slug = slugify ( tail _no _ext , { lower : true , strict : true } ) ;
return { content , toc , metadata } ;
}
export default {
render ,
split ,
render _with _code ,
create _renderer
}