@ -1,3 +1,12 @@
/ *
Utilities mostly for generating the documentation and static pages in
most sites . It ' s targetted at the kind of content I make , which is
programming docs , educational lessons , and other Markdown documents
with code in them .
The main feature of this module is the ` class Ork ` which load code from
external code files for inclusion in the output HTML .
* /
import { Remarkable } from "remarkable" ;
import { Remarkable } from "remarkable" ;
import assert from "assert" ;
import assert from "assert" ;
// Node is absolutely stupid. The remarkable project has an esm setting which
// Node is absolutely stupid. The remarkable project has an esm setting which
@ -12,10 +21,38 @@ import child_process from "child_process";
_ . templateSettings . interpolate = /{{([\s\S]+?)}}/g ;
_ . templateSettings . interpolate = /{{([\s\S]+?)}}/g ;
class Ork {
/ *
This is a little class I use to read in an external code
file , then wrap that code with the markdown code blocks .
Look in ` render_with_code ` to see how it is implemented :
1. Creates a ` lodash ` template that has a variable named ` ork ` .
2. When ` render_with_code ` runs it then first runs your ` .md ` file through this template .
3. That template can then call functions on ` Ork ` , as in : ` {{ ork.js() }} ` .
4. ` Ork ` then returns whatever Markdown code format is needed for the output .
* /
export class Ork {
/ *
Given a ` base ` directory and metadata object it will load code as needed . Keep
in mind this is tied to my setup where I have a private directory in a separate
repository that contains the code for the course . This let ' s you do this :
` ` ` javascript
let ork = new Ork ( "javascript_level_1" , { code : "exercise_1" } ) ;
let md _out = ork . js ( "code.js" ) ;
` ` `
This will load the file in ` javascript_level_1/exercise_1/code.js ` , wrap it with
Markdown code blocks .
+ ` base string ` -- The base directory where all of the code examples are .
+ ` metadata Object ` -- When a ` .md ` file is loaded it has metadata at the top , and one is ` code ` which says where code for that ` .md ` file should be loaded .
_ _ _BUG _ _ _ : ` base ` needs to go away , but for now it ' s where the root of the db is .
* /
constructor ( base , metadata ) {
constructor ( base , metadata ) {
// base needs to go away, but for now it's where the root of the db is
if ( metadata . code ) {
if ( metadata . code ) {
this . code _path = ` ${ base } ${ metadata . code } ` ;
this . code _path = ` ${ base } ${ metadata . code } ` ;
} else {
} else {
@ -23,11 +60,26 @@ class Ork {
}
}
}
}
/ *
Loads a JavaScript file and returns it wrapped in a Markdown code block tagged
with ` javascript ` .
+ ` file string ` -- The file to load relative to ` base/{metadata.code} ` from the constructor .
* /
js ( file = 'code.js' ) {
js ( file = 'code.js' ) {
assert ( this . code _path , "You are using ork but no code path set in metadata." ) ;
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 \` \` \` `
return ` \` \` \` javascript \n ${ fs . readFileSync ( ` ${ this . code _path } / ${ file } ` ) . toString ( ) . trim ( ) } \n \` \` \` `
}
}
/ *
Actually _ _executes _ _ the code file using the local ` node ` interpreter , and returns
the output , instead of the code . This lets you automatically generate the results
of code for your documents from one source . Otherwise you have to do this manually
then include it .
+ ` file string ` -- The file to _run _ relative to ` base/{metadata.code} ` from the constructor .
+ ` args string ` -- Arguments to the ` node ` command line .
* /
node ( file = 'code.js' , args = '' ) {
node ( file = 'code.js' , args = '' ) {
assert ( this . code _path , "You are using ork but no code path set." ) ;
assert ( this . code _path , "You are using ork but no code path set." ) ;
let cmd = ` node " ${ file } " ${ args } ` ;
let cmd = ` node " ${ file } " ${ args } ` ;
@ -36,23 +88,35 @@ class Ork {
}
}
}
}
const count _lines = ( str ) => {
/ *
To make CSS line numbers work you have to do this janky line counting
thing . Definitely not an efficient way to do it but it is a simple way .
* /
export const count _lines = ( str ) => {
let match = str . match ( /\n(?!$)/g ) ;
let match = str . match ( /\n(?!$)/g ) ;
return match ? match . length + 1 : 1 ;
return match ? match . length + 1 : 1 ;
}
}
const line _number _spans = ( str ) => {
/ *
Using ` count_lines ` this will generate a bunch of ` <span></span> ` tags
to match the count . This is the easiest way to create a strip of line
numbers to the left of a code block .
* /
export const line _number _spans = ( str ) => {
let count = count _lines ( str ) ;
let count = count _lines ( str ) ;
let spans = new Array ( count + 1 ) . join ( '<span></span>' ) ;
let spans = new Array ( count + 1 ) . join ( '<span></span>' ) ;
return ` <span aria-hidden="true" class="line-numbers-rows"> ${ spans } </span> ` ;
return ` <span aria-hidden="true" class="line-numbers-rows"> ${ spans } </span> ` ;
}
}
/ * *
/ *
* Runs Prism on the string , defaulting to the "markup"
Runs Prism on the string , defaulting to the "markup" language which will
* language which will encode most code correctly even
encode most code correctly even if you don ' t specify a language after the
* if you don ' t specify a language after the ` ` `
Markdown code block header .
* * /
const run _prism = ( str , lang = "markup" ) => {
+ ` str string ` -- Code to parse with Prism .
+ ` lang string ` -- Defaults to "markup" but can be anything Prism supports .
* /
export const run _prism = ( str , lang = "markup" ) => {
assert ( lang !== undefined , "Language can't be undefined." ) ;
assert ( lang !== undefined , "Language can't be undefined." ) ;
// do the check again since the language might not exist
// do the check again since the language might not exist
@ -64,17 +128,53 @@ const run_prism = (str, lang="markup") => {
}
}
}
}
/ *
Does a complete highlight of a block of code for the specified
` lang ` . It does the combination of ` run_prism ` + ` line_number_spans `
so that the output is immediately useable as code output with line numbers .
+ ` str string ` -- Code to parse with Prism .
+ ` lang string ` -- Can be anything Prism supports , like "javascript" or "shell" .
* /
export const highlight = ( str , lang ) => {
export const highlight = ( str , lang ) => {
return run _prism ( str , lang ) + line _number _spans ( str ) ;
return run _prism ( str , lang ) + line _number _spans ( str ) ;
}
}
/* A separate renderer just for the titles that doesn't need anything else. */
/ *
A separate renderer just for the titles that doesn ' t need anything else .
The main thing this does is turn of paragraphs by assigning :
` ` ` javascript
rem . renderer . rules . paragraph _open = pass ;
rem . renderer . rules . paragraph _close = pass ;
` ` `
The ` pass ` function is an empty function that removes the paragraphs from the
Remarkable output . That ' s all you need for title rendering .
* /
export const title _render = new Remarkable ( 'full' ) . use ( rem => {
export const title _render = new Remarkable ( 'full' ) . use ( rem => {
let pass = ( tokens , idx ) => '' ;
let pass = ( tokens , idx ) => '' ;
rem . renderer . rules . paragraph _open = pass ;
rem . renderer . rules . paragraph _open = pass ;
rem . renderer . rules . paragraph _close = pass ;
rem . renderer . rules . paragraph _close = pass ;
} ) ;
} ) ;
/ *
My Markdown files ` .md ` files with a format of :
` ` ` markup
{
metadata
}
-- -- --
Markdown
` ` `
This function splits the input at the 6 ` - ` characters , runs
` JSON.parse ` on the top part , then returns that metadata and body .
+ ` raw_md string ` -- The markdown to split .
+ _ _ _return ` Array[Object, string] ` -- An Array with the metadata object and the string body .
* /
export const split = ( raw _md ) => {
export const split = ( raw _md ) => {
let [ metadata , ... body ] = raw _md . split ( '------' ) ;
let [ metadata , ... body ] = raw _md . split ( '------' ) ;
metadata = JSON . parse ( metadata ) ;
metadata = JSON . parse ( metadata ) ;
@ -84,6 +184,15 @@ export const split = (raw_md) => {
const null _cb = ( metadata , body ) => body ;
const null _cb = ( metadata , body ) => body ;
/ *
Constructs the Remarkable render function that will do :
1. ` linkify ` any hostnames it finds .
2. Update the ` toc ` Array with new headings for a TOC output .
3. Fix up links , but I 'm not sure why I' m doing this .
_ _ _BUG _ _ _ : Why am I changing the link output .
* /
export const create _renderer = ( toc ) => {
export const create _renderer = ( toc ) => {
const renderer = new Remarkable ( 'full' , {
const renderer = new Remarkable ( 'full' , {
html : true ,
html : true ,
@ -122,6 +231,18 @@ export const create_renderer = (toc) => {
return renderer ;
return renderer ;
}
}
/ *
Performs a render of the ` raw_md ` that will :
1. Split the ` raw_md ` with ` split ` .
2. Render with a Remarkable renderer from ` create_renderer ` .
3. Go through each element of the TOC and run ` title_render.render ` on them .
+ ` raw_md string ` -- The Markdown string to parse .
+ ` cb function(metadata, body) ` -- An optional callback that gets the ` metadata, body ` from ` split ` .
+ _ _ _return _ _ _ ` {toc: Array, content: string, metadata: Object} `
* /
export const render = ( raw _md , cb = null _cb ) => {
export const render = ( raw _md , cb = null _cb ) => {
let toc = [ ] ;
let toc = [ ] ;
let [ metadata , body ] = split ( raw _md ) ;
let [ metadata , body ] = split ( raw _md ) ;
@ -144,6 +265,19 @@ export const render = (raw_md, cb=null_cb) => {
return { toc , content , metadata } ;
return { toc , content , metadata } ;
}
}
/ *
The tip of the iceberg , this does the actual render of a markdown file
with source code in it . It combines all of the functions so far into
one cohesive rendering function :
1. It uses ` Ork ` to load code into the markdown .
2. It uses ` render ` to create a Remarkable renderer .
3. Finally uses ` slugify ` to generate a slug for the metadata output .
+ ` source_dir string ` -- Source code directory for the ` Ork(source_dir, metadata) ` call .
+ ` md_file string ` -- Path to the markdown file to load .
+ _ _ _return _ _ _ ` {content: string, toc: Array, metadata: Object} `
* /
export const render _with _code = ( source _dir , md _file ) => {
export const render _with _code = ( source _dir , md _file ) => {
// get the file without path or extension
// get the file without path or extension
const tail _no _ext = PATH . basename ( md _file , PATH . extname ( md _file ) ) ;
const tail _no _ext = PATH . basename ( md _file , PATH . extname ( md _file ) ) ;