/**
 * Compiles fractive source projects into a playable distribution.
 */ require ( "source-map-support" ) . install ( ) ; import * as browserify from "browserify" ; import * as fs from "fs" ; import * as path from "path" ; import * as util from "util" ; import * as XRegExp from "xregexp" ; import * as commonmark from "commonmark" ; import * as beautifier from "js-beautify" ; import * as minifier from "html-minifier" ; import * as ajv from "ajv" ; import * as overrideJSON from "json-override" ; import { FractiveProject } from "./ProjectSchema" ; export let ProjectDefaults = { title : "Untitled" , author : "Anonymous" , description : "An interactive story written in Fractive" , website : "" , twitter : "" , markdown : [ "source/**/*.md" ] , javascript : [ "source/**/*.js" ] , assets : [ "assets/**" ] , ignore : [ ] , aliases : [ ] , template : "template.html" , output : "build" , outputFormat : "prettify" , linkTooltips : false , linkTags : { external : { html : "" , prepend : false } , inline : { html : "" , prepend : false } , section : { html : "" , prepend : false } , function : { html : "" , prepend : false } } , includeBackButton : true , backButtonHtml : "Back" , hardLineBreaks : true , smartPunctuation : true } ; import * as globby from "globby" ; import * as clc from "cli-color" ; export interface CompilerOptions { /**
     * Path to the project json file to compile.
     */ buildPath : string ; /**
     * Show usage guide for the Compiler.
     */ help ? : boolean ; /**
     * If true, log what would've been done but don't actually modify any files on disk
     */ dryRun ? : boolean ; /**
     * If true, log extra execution details
     */ verbose ? : boolean ; /**
     * If true, log additional debug info during build
     */ debug ? : boolean ; } export namespace Compiler { let project = null ; let projectPath = "" ; let nextInlineID = 0 ; let sectionCount = 0 ; let sections = { } ; let markdownReader = null ; let markdownWriter = null ; param html param javascript return function ApplyTemplate ( basePath , html , javascript ) { let templatePath = "" ; if ( project . template . indexOf ( "{examples}" ) == 0 ) { templatePath = project . template . replace ( "{examples}" , `${ __dirname }/../templates` ) ; } else { templatePath = path . resolve ( basePath , project . template ) ; } if ( ! fs . existsSync ( templatePath ) ) { console . log ( `Template file not found: "${ templatePath }"` ) ; process . exit ( 1 ) ; } if ( ! fs . lstatSync ( templatePath ) . isFile ( ) ) { console . log ( `Template "${ templatePath }" is not a file` ) ; process . exit ( 1 ) ; } let template = fs . readFileSync ( templatePath , "utf8" ) ; let scriptSection = `<script type='text/javascript'>${ javascript }</script>` ; template = InsertHtmlAtMark ( scriptSection , template , 'script' ) ; template = InsertHtmlAtMark ( html , template , 'story' ) ; if ( project . includeBackButton ) { let backButtonHtml = '<a href="javascript:Core.GotoPreviousSection();">' + project . backButtonHtml + '</a>' ; template = InsertHtmlAtMark ( backButtonHtml , template , 'backButton' , false ) ; } let openGraphHtml = `<meta property="og:title" content="${ project . title }"/>\n` ; openGraphHtml += `<meta property="og:description" content="${ project . description }"/>\n` ; openGraphHtml += `<meta name="twitter:card" content="summary"/>\n` ; if ( project . twitter . length > 0 ) { if ( project . twitter [ 0 ] !== '@' ) { project . twitter = `@${ project . twitter }` ; } openGraphHtml += `<meta name="twitter:creator" content="${ project . twitter }"/>\n` ; } template = InsertHtmlAtMark ( openGraphHtml , template , 'opengraph' , false ) ; template = InsertHtmlAtMark ( project . title , template , 'title' , false ) ; template += "<script>Core.BeginStory();</script>" ; if ( project . outputFormat === 'minify' ) { return minifier . minify ( template , { caseSensitive : true , collapseWhitespace : true , log : OnMinifierLog , minifyCSS : true , minifyJS : true , removeAttributeQuotes : true , removeComments : true , removeEmptyAttributes : true , removeEmptyElements : false , removeRedundantAttributes : true } ) ; } else if ( project . outputFormat === 'prettify' ) { return beautifier . html ( template ) ; } else { return template ; } } param targetPath param options function CleanDirectoryRecursive ( targetPath , options ) { if ( fs . lstatSync ( targetPath ) . isDirectory ( ) ) { let files = fs . readdirSync ( targetPath , "utf8" ) ; for ( let i = 0 ; i < files . length ; i ++ ) { CleanDirectoryRecursive ( path . resolve ( targetPath , files [ i ] ) , options ) ; } if ( ! options . dryRun ) { fs . rmdirSync ( targetPath ) ; } } else { if ( ! options . dryRun ) { fs . unlinkSync ( targetPath ) ; } } } param options export function Compile ( options ) { let buildPath = options . buildPath ; if ( fs . lstatSync ( buildPath ) . isDirectory ( ) ) { buildPath = path . join ( buildPath , "fractive.json" ) ; } if ( ! fs . existsSync ( buildPath ) ) { console . error ( clc . red ( `Couldn't find project config "${ buildPath }"` ) ) ; process . exit ( 1 ) ; } let projectPath = path . dirname ( buildPath ) ; let targetProject = JSON . parse ( fs . readFileSync ( buildPath , "utf8" ) ) ; let validator = new ajv ( ) ; let valid = validator . validate ( JSON . parse ( fs . readFileSync ( path . join ( __dirname , "../src/ProjectSchema.json" ) , "utf8" ) ) , targetProject ) ; if ( ! valid ) { LogError ( `  ${ buildPath }: Failed validating JSON` ) ; for ( let i = 0 ; i < validator . errors . length ; i ++ ) { LogError ( `  ${ validator . errors [ i ] . dataPath } ${ validator . errors [ i ] . message } ${ util . inspect ( validator . errors [ i ] . params ) }` ) ; } process . exit ( 1 ) ; } project = overrideJSON ( ProjectDefaults , targetProject , true ) ; if ( project . markdown . length < 1 ) { LogError ( "No Markdown input patterns were given (check the 'markdown' property in your fractive.json)" ) ; process . exit ( 1 ) ; } if ( project . output . length < 1 ) { LogError ( "No output directory was given (check the 'output' property in your fractive.json)" ) ; process . exit ( 1 ) ; } if ( options . dryRun ) { console . log ( clc . red ( "\n(This is a dry run. No output files will be written.)\n" ) ) ; } let cleanDir = path . resolve ( projectPath , project . output ) ; if ( ! fs . existsSync ( cleanDir ) ) { CreateDirectoryRecursive ( cleanDir ) ; } else { CleanDirectoryRecursive ( cleanDir , options ) ; } sections = { } ; markdownReader = new commonmark . Parser ( { smart : project . smartPunctuation } ) ; markdownWriter = new commonmark . HtmlRenderer ( { softbreak : ( project . hardLineBreaks ? "<br/>" : "\n" ) } ) ; let globOptions = { cwd : projectPath , expandDirectories : true , ignore : project . ignore . concat ( `${ project . output }/**` ) , matchBase : true , nodir : true , nomount : true } ; let targets = { markdownFiles : globby . sync ( project . markdown , globOptions ) , javascriptFiles : globby . sync ( project . javascript , globOptions ) , assetFiles : globby . sync ( project . assets , globOptions ) } ; let errorCount = 0 ; let html = "" ; for ( let i = 0 ; i < targets . markdownFiles . length ; i ++ ) { if ( options . verbose || options . dryRun ) { LogAction ( targets . markdownFiles [ i ] , "render" ) ; } var rendered = RenderFile ( path . resolve ( projectPath , targets . markdownFiles [ i ] ) , options ) ; if ( rendered === null ) { errorCount ++ ; } else { html += `<!-- ${ targets . markdownFiles [ i ] } -->\n${ rendered }\n` ; } } if ( errorCount > 0 ) { process . exit ( 1 ) ; } let outputDir = path . resolve ( projectPath , project . output ) ; if ( ! fs . existsSync ( outputDir ) ) { CreateDirectoryRecursive ( outputDir ) ; } let browserifyFiles = [ path . resolve ( __dirname , "Core.js" ) ] ; for ( let i = 0 ; i < targets . javascriptFiles . length ; i ++ ) { let scriptFile = path . resolve ( projectPath , targets . javascriptFiles [ i ] ) ; browserifyFiles [ i + 1 ] = scriptFile ; } if ( options . verbose || options . dryRun ) { LogAction ( 'javascript files' , 'running browserify' ) ; } let storyModulePaths = [ ] ; let storyModuleDir = path . resolve ( projectPath , 'node_modules' ) ; if ( fs . existsSync ( storyModuleDir ) ) { let isDirectory = directory => fs . lstatSync ( directory ) . isDirectory ( ) ; storyModulePaths = ( source => fs . readdirSync ( source ) . map ( name => path . join ( source , name ) ) . filter ( isDirectory ) ) ( storyModuleDir ) ; } browserify ( browserifyFiles , { standalone : 'Fractive' , paths : storyModulePaths } ) . bundle ( function ( error , buffer ) { if ( error ) { LogError ( error ) ; } else { let javascript = buffer ; html = ApplyTemplate ( projectPath , html , javascript ) ; for ( let i = 0 ; i < targets . assetFiles . length ; i ++ ) { if ( options . verbose || options . dryRun ) { LogAction ( targets . assetFiles [ i ] , "copy" ) ; } if ( ! options . dryRun ) { let sourcePath = path . resolve ( projectPath , targets . assetFiles [ i ] ) ; let destPath = path . resolve ( outputDir , targets . assetFiles [ i ] ) ; let destDir = path . dirname ( destPath ) ; if ( ! fs . existsSync ( destDir ) ) { CreateDirectoryRecursive ( destDir ) ; } fs . copyFileSync ( sourcePath , destPath ) ; } } let indexPath = path . resolve ( outputDir , "index.html" ) ; if ( options . verbose || options . dryRun ) { LogAction ( indexPath . split ( path . resolve ( projectPath ) ) . join ( "" ) , "output" ) ; } if ( ! options . dryRun ) { fs . writeFileSync ( indexPath , html , "utf8" ) ; } } } ) ; } param targetPath function CreateDirectoryRecursive ( targetPath ) { const separator = path . sep ; const initDir = ( path . isAbsolute ( targetPath ) ? separator : "" ) ; targetPath . split ( separator ) . reduce ( ( parentDir , childDir ) => { const currentPath = path . resolve ( parentDir , childDir ) ; if ( ! fs . existsSync ( currentPath ) ) { fs . mkdirSync ( currentPath ) ; } return currentPath ; } , initDir ) ; } param node return function GetLinkText ( node ) { if ( node . type !== "link" ) { console . log ( `GetLinkText received a node of type ${ node . type }, which is illegal and will be skipped` ) ; return null ; } let html = markdownWriter . render ( node ) ; for ( let i = 0 ; i < html . length ; i ++ ) { if ( html [ i ] === ">" ) { html = html . substring ( i + 1 , html . length - 4 ) ; break ; } } return html ; } param snippet param template param mark param required return function InsertHtmlAtMark ( snippet , template , mark , required = true ) { let markComment = `<!--{${ mark }}-->` ; if ( template . indexOf ( markComment ) === - 1 && required ) { LogError ( `Template file does not contain mark ${ markComment }` ) ; process . exit ( 1 ) ; } return template . split ( markComment ) . join ( snippet ) ; } param rootNode param startIndex param endIndex param dataAttrs returns function InsertHtmlIntoNode ( rootNode , startIndex , endIndex , dataAttrs ) { let preContent = rootNode . literal . substring ( 0 , startIndex ) ; let postContent = rootNode . literal . substring ( endIndex ) ; rootNode . literal = preContent ; var htmlNode = new commonmark . Node ( "html_inline" , rootNode . sourcepos ) ; let attrs = "" ; for ( let i = 0 ; i < dataAttrs . length ; i ++ ) { attrs += ` data-${ dataAttrs [ i ] . attr }="${ dataAttrs [ i ] . value }"` ; } switch ( rootNode . type ) { case "code" : { htmlNode . literal = `<code><span${ attrs }></span></code>` ; break ; } case "code_block" : { htmlNode . literal = `<pre><code><span${ attrs }></span></code></pre>` ; break ; } default : { htmlNode . literal = `<span${ attrs }></span>` ; break ; } } rootNode . insertAfter ( htmlNode ) ; if ( rootNode . literal === "" ) { rootNode . unlink ( ) ; } if ( postContent && postContent . length > 0 ) { var postNode = new commonmark . Node ( rootNode . type , rootNode . sourcepos ) ; postNode . literal = postContent ; htmlNode . insertAfter ( postNode ) ; } return htmlNode ; } param url function IsExternalLink ( url ) { let tokens = url . split ( "/" ) ; switch ( tokens [ 0 ] . toLowerCase ( ) ) { case "http:" : case "https:" : case "mailto:" : { return true ; } } return false ; } param filePath param action function LogAction ( filePath , action ) { console . log ( `  ${ clc . green ( action ) } ${ path . relative ( projectPath , filePath ) }` ) ; } param ast function LogAST ( ast ) { if ( ast === null ) { return ; } let indent = 0 ; let getIndent = function ( indent ) { let result = '' ; for ( let i = 0 ; i < indent ; i ++ ) { result += '  ' ; } return result ; } ; let walker = ast . walker ( ) ; var event ; while ( ( event = walker . next ( ) ) ) { if ( event . node . isContainer && ! event . entering ) { indent -- ; } if ( ! event . node . isContainer || event . entering ) { console . log ( clc . blue ( `${ getIndent ( indent ) }${ event . node . type }: ${ event . node . literal ? event . node . literal . split ( '\n' ) . join ( '\\n' ) : '' }` ) ) ; } if ( event . node . isContainer && event . entering ) { indent ++ ; } } } param text function LogError ( text ) { console . error ( clc . red ( text ) ) ; } param text param lineNumber param characterNumber function LogParseError ( text , filePath , node , lineOffset ? , columnOffset ? ) { if ( node && node . sourcepos ) { let line = node . sourcepos [ 0 ] [ 0 ] + ( lineOffset !== undefined ? lineOffset : 0 ) ; let column = node . sourcepos [ 0 ] [ 1 ] + ( columnOffset !== undefined ? columnOffset : 0 ) ; LogError ( `${ path . relative ( projectPath , filePath ) } (${ line },${ column }): ${ text }` ) ; } else { LogError ( `${ path . relative ( projectPath , filePath ) }: ${ text }` ) ; } } param data function OnMinifierLog ( data ) { switch ( typeof ( data ) ) { case "string" : { if ( data . indexOf ( "minified in:" ) < 0 ) { console . log ( clc . yellow ( "Minifier: " + data ) ) ; } break ; } case "object" : { if ( data . message ) { console . log ( clc . yellow ( `Minifier: ${ data . message }\nThis isn't fatal; it just means Javascript was not minified.\nTry running CLI uglifyjs on the .js file(s) to narrow down the error.` ) ) ; } else { console . log ( clc . yellow ( `"Minifier: Unrecognized object format in log... raw object follows:` ) ) ; console . log ( data ) ; } break ; } default : { console . log ( clc . yellow ( `"Minifier: Unhandled data type '${ typeof ( data ) }' in log... raw data follows:` ) ) ; console . log ( data ) ; break ; } } } param filepath param options return function RenderFile ( filepath , options ) { if ( ! fs . existsSync ( filepath ) ) { console . log ( "File not found: " + filepath ) ; process . exit ( 1 ) ; } let markdown = ReplaceAliases ( fs . readFileSync ( filepath , "utf8" ) ) ; let ast = markdownReader . parse ( markdown ) ; if ( options . debug ) { console . log ( "\nRAW AST\n" ) ; LogAST ( ast ) ; } let walker = ast . walker ( ) ; var event , node , prevNode ; while ( ( event = walker . next ( ) ) ) { node = event . node ; if ( node . type === "text" && prevNode && prevNode . type === "text" ) { if ( node . literal ) { prevNode . literal += node . literal ; } node . unlink ( ) ; } else { prevNode = node ; } } if ( options . debug ) { console . log ( "\nCONSOLIDATED AST\n" ) ; LogAST ( ast ) ; } sectionCount = 0 ; walker = ast . walker ( ) ; while ( ( event = walker . next ( ) ) ) { node = event . node ; switch ( node . type ) { case "link" : { if ( ! RenderLink ( walker , event , filepath ) ) { return null ; } break ; } case "text" : case "code" : case "code_block" : case "html_inline" : case "html_block" : { if ( ! RenderText ( walker , event , filepath ) ) { return null ; } break ; } case "image" : { if ( ! RenderImage ( walker , event , filepath ) ) { return null ; } break ; } } } walker = ast . walker ( ) ; let firstEvent = walker . next ( ) ; let closingNode = new commonmark . Node ( "html_inline" ) ; closingNode . literal = "</div>" ; firstEvent . node . appendChild ( closingNode ) ; if ( options . debug ) { console . log ( "\nFINAL AST\n" ) ; LogAST ( ast ) ; } return markdownWriter . render ( ast ) ; } param walker param event param filepath returns function RenderImage ( walker , event , filepath ) { if ( ! walker || ! event ) { LogError ( "RenderImage received an invalid state" ) ; return false ; } if ( event . node . type !== "image" ) { LogError ( `RenderImage was passed a ${ event . node . type } node, which is illegal` ) ; return false ; } let node = event . node ; let alt = "" ; if ( node . firstChild && node . firstChild . type == "text" ) { alt = node . firstChild . literal ; node . firstChild . unlink ( ) ; } let url = node . destination ; url = url . replace ( "%7B" , "{" ) . replace ( "%7D" , "}" ) ; if ( url [ 0 ] !== "{" ) { let newNode = new commonmark . Node ( "html_inline" ) ; newNode . literal = `<img src="${ url }" alt="${ alt }" title="${ alt }">` ; node . insertBefore ( newNode ) ; node . unlink ( ) ; walker . resumeAt ( newNode ) ; } else { if ( url [ url . length - 1 ] !== "}" ) { LogParseError ( `Unterminated macro ${ url } in image URL` , filepath , node ) ; return false ; } switch ( url [ 1 ] ) { case "@" : { LogParseError ( `Invalid macro ${ url } in image URL (section macros cannot be used as image sources)` , filepath , node ) ; return false ; } case "#" : case "$" : { let newNode = new commonmark . Node ( "html_inline" ) ; newNode . literal = `<img data-image-source-macro="${ url . substring ( 1 , url . length - 1 ) }" src="#" alt="${ alt }" title="${ alt }">` ; node . insertBefore ( newNode ) ; node . unlink ( ) ; walker . resumeAt ( newNode ) ; break ; } default : { LogParseError ( `Unknown macro ${ url } in image URL` , filepath , node ) ; return false ; } } } return true ; } param walker param event param filepath returns function RenderLink ( walker , event , filepath ) { if ( ! walker || ! event ) { LogError ( "RenderLink received an invalid state" ) ; return false ; } if ( event . node . type !== "link" ) { LogError ( `RenderLink received a ${ event . node . type } node, which is illegal` ) ; return false ; } let url = event . node . destination ; url = url . replace ( "%7B" , "{" ) . replace ( "%7D" , "}" ) ; if ( url [ 0 ] !== "{" ) { if ( IsExternalLink ( url ) ) { if ( event . entering ) { let newNode = new commonmark . Node ( "html_inline" , event . node . sourcepos ) ; newNode . literal = project . linkTags . external . html ; if ( project . linkTags . external . prepend ) { event . node . prependChild ( newNode ) ; walker . resumeAt ( newNode ) ; } else { event . node . appendChild ( newNode ) ; } return true ; } else { return RewriteLinkNode ( event . node , [ { "attr" : "target" , "value" : "_blank" } , { "attr" : "href" , "value" : event . node . destination } ] , null ) ; } } else { return true ; } } if ( url [ url . length - 1 ] !== "}" ) { LogParseError ( `Unterminated macro in link destination ${ url }` , filepath , event . node ) ; return false ; } let tokens = url . substring ( 1 , url . length - 1 ) . split ( ":" ) ; url = tokens [ 0 ] ; let modifier = ( tokens . length > 1 ? tokens [ 1 ] : "" ) ; switch ( modifier ) { case "inline" : { if ( event . entering ) { let newNode = new commonmark . Node ( "html_inline" , event . node . sourcepos ) ; newNode . literal = project . linkTags . inline . html ; if ( project . linkTags . inline . prepend ) { event . node . prependChild ( newNode ) ; walker . resumeAt ( newNode ) ; } else { event . node . appendChild ( newNode ) ; } return true ; } else { let attrs = [ { attr : "href" , value : "javascript:;" } , { attr : "data-replace-with" , value : url } ] ; return RewriteLinkNode ( event . node , attrs , `inline-${ nextInlineID ++ }` ) ; } } default : { switch ( url [ 0 ] ) { case "@" : { if ( event . entering ) { let newNode = new commonmark . Node ( "html_inline" , event . node . sourcepos ) ; newNode . literal = project . linkTags . section . html ; if ( project . linkTags . section . prepend ) { event . node . prependChild ( newNode ) ; walker . resumeAt ( newNode ) ; } else { event . node . appendChild ( newNode ) ; } return true ; } else { let attrs = [ { attr : "href" , value : "javascript:;" } , { attr : "data-goto-section" , value : url . substring ( 1 ) } ] ; return RewriteLinkNode ( event . node , attrs , null ) ; } } case "#" : { if ( event . entering ) { let newNode = new commonmark . Node ( "html_inline" , event . node . sourcepos ) ; newNode . literal = project . linkTags . function . html ; if ( project . linkTags . function . prepend ) { event . node . prependChild ( newNode ) ; walker . resumeAt ( newNode ) ; } else { event . node . appendChild ( newNode ) ; } return true ; } else { let attrs = [ { attr : "href" , value : "javascript:;" } , { attr : "data-call-function" , value : url . substring ( 1 ) } ] ; return RewriteLinkNode ( event . node , attrs , null ) ; } } case "$" : { LogParseError ( `Variable macros can't be used as link destinations: {${ url }}` , filepath , event . node ) ; return false ; } default : { LogParseError ( `Unrecognized macro in link destination: {${ url }}` , filepath , event . node ) ; return false ; } } } } } param walker param event param filepath returns function RenderText ( walker , event , filepath ) { if ( ! walker || ! event ) { LogError ( "RenderText received an invalid state" ) ; return false ; } let node = event . node ; let lineOffset = 0 ; let columnOffset = 0 ; for ( let i = 0 ; i < node . literal . length ; i ++ ) { if ( node . literal [ i ] === '\\' ) { i = SkipEscapedSubstring ( node . literal , i ) ; continue ; } else if ( node . literal [ i ] === '{' ) { let insertedNode = null ; let macro = null ; let braceCount = 1 ; for ( let j = i + 1 ; j < node . literal . length ; j ++ ) { if ( node . literal [ j ] === '{' ) { braceCount ++ ; } else if ( node . literal [ j ] === '}' ) { if ( -- braceCount == 0 ) { macro = node . literal . substring ( i , j + 1 ) ; break ; } } } if ( macro === null ) { LogParseError ( `Unterminated macro near "${ node . literal . substring ( i , i + 10 ) }" in text` , filepath , node , lineOffset , columnOffset ) ; return false ; } switch ( macro [ 1 ] ) { case "{" : { if ( node . parent ) { let macroContents = macro . substring ( 2 , macro . length - 2 ) ; let sectionName = macroContents ; let tags = [ ] ; if ( macroContents . indexOf ( ":" ) !== - 1 ) { sectionName = macroContents . substring ( 0 , macroContents . indexOf ( ":" ) ) . trim ( ) ; let tagDeclarations = macroContents . substring ( macroContents . indexOf ( ":" ) + 1 ) ; let tagTokens = tagDeclarations . split ( ',' ) ; for ( var j = 0 ; j < tagTokens . length ; ++ j ) { tags . push ( tagTokens [ j ] . trim ( ) ) ; } } if ( sections [ sectionName ] !== undefined ) { LogParseError ( `Section "${ sectionName }" was already defined in "${ sections [ sectionName ] . sourceFile }" on line "${ sections [ sectionName ] . lineNumber }"` , filepath , node , lineOffset , columnOffset ) ; return false ; } else { sections [ sectionName ] = { sourceFile : path . relative ( projectPath , filepath ) , lineNumber : lineOffset } ; } insertedNode = new commonmark . Node ( "html_inline" , node . sourcepos ) ; insertedNode . literal = `${ sectionCount > 0 ? "</div>\n" : "" }<div id="${ sectionName }" data-tags="${ tags . toString ( ) }" class="section" hidden="true">` ; if ( node . prev ) { LogParseError ( `Section macro "${ macro }" must be defined in its own paragraph/on its own line` , filepath , node , lineOffset , columnOffset ) ; return false ; } else if ( node . parent . type === "paragraph" ) { node . parent . insertAfter ( insertedNode ) ; let newParagraph = new commonmark . Node ( "paragraph" , node . sourcepos ) ; let nodesMoved = 0 ; let bSkippedFirstBreak = false ; while ( ( event = walker . next ( ) ) ) { if ( event . node . type === "paragraph" && ! event . entering ) { break ; } if ( event . node . type === "softbreak" && ! bSkippedFirstBreak ) { bSkippedFirstBreak = true ; continue ; } newParagraph . appendChild ( event . node ) ; ++ nodesMoved ; } let remainderNode = new commonmark . Node ( "text" , node . sourcepos ) ; remainderNode . literal = StripLeadingWhitespace ( node . literal . substring ( macro . length ) ) ; newParagraph . prependChild ( remainderNode ) ; if ( nodesMoved > 0 || remainderNode . literal . length > 0 ) { insertedNode . insertAfter ( newParagraph ) ; } node . parent . unlink ( ) ; } else { LogParseError ( `Section macro "${ macro }" cannot be defined inside another block element` , filepath , node , lineOffset , columnOffset ) ; return false ; } sectionCount ++ ; } else { LogParseError ( `Node for "${ macro }" has no parent` , filepath , node , lineOffset , columnOffset ) ; return false ; } break ; } case "@" : case "#" : case "$" : { insertedNode = InsertHtmlIntoNode ( node , i , i + macro . length , [ { attr : "expand-macro" , value : macro . substring ( 1 , macro . length - 1 ) } ] ) ; break ; } default : { LogParseError ( `Unrecognized macro "${ macro }" in text` , filepath , node . parent , lineOffset , columnOffset ) ; return false ; } } walker . resumeAt ( insertedNode ) ; break ; } else if ( node . literal [ i ] === '\n' ) { lineOffset ++ ; columnOffset = - 1 ; } columnOffset ++ ; } if ( node . literal ) { node . literal = node . literal . split ( '\\{' ) . join ( '{' ) ; } return true ; } param source returns function ReplaceAliases ( source ) { if ( project . aliases . length < 1 ) { return source ; } let markdown = source ; for ( let i = 0 ; i < markdown . length ; i ++ ) { if ( markdown [ i ] === '\\' ) { i = SkipEscapedSubstring ( markdown , i ) ; continue ; } else if ( markdown [ i ] === '{' ) { let bIsEnd = ( markdown [ i + 1 ] === '/' ) ; for ( let j = i + 1 ; j < markdown . length ; j ++ ) { if ( markdown [ j ] === '{' ) { i = j ; break ; } else if ( markdown [ j ] === '}' ) { let macro = markdown . substring ( i , j + 1 ) ; let macroName = macro . substring ( bIsEnd ? 2 : 1 , macro . length - 1 ) ; let replacement = null ; let regexp = null ; let regexpToReplace = null ; let regexpReplacement = null ; for ( let k = 0 ; k < project . aliases . length ; k ++ ) { let alias = project . aliases [ k ] ; if ( alias . hasOwnProperty ( 'alias' ) && macroName === alias . alias ) { replacement = ( bIsEnd ? alias . end : alias . replaceWith ) ; break ; } else if ( alias . hasOwnProperty ( 'regex' ) ) { regexp = XRegExp ( alias . regex ) ; if ( alias . debug ) { console . log ( `Checking macro ${ macroName } against regex ${ alias . regex }` ) ; } regexpToReplace = XRegExp ( '{' + ( bIsEnd ? '/' : '' ) + alias . regex + '}' ) ; if ( regexp . exec ( macroName ) ) { if ( alias . debug ) { console . log ( `Replacing macro ${ macroName } with ${ regexpReplacement }` ) ; } regexpReplacement = ( bIsEnd ? alias . end : alias . replaceWith ) ; replacement = XRegExp . replace ( macro , regexpToReplace , regexpReplacement , 'one' ) ; break ; } } } if ( replacement !== null ) { markdown = `${ markdown . substring ( 0 , i ) }${ replacement }${ markdown . substring ( j + 1 ) }` ; i += replacement . length - 1 ; } break ; } } } } return markdown ; } param node param attributes param linkText param id returns function RewriteLinkNode ( node , attributes , id ) { if ( node . type != "link" ) { console . log ( `RewriteLinkNode received a node of type ${ node . type }, which is illegal and will be skipped` ) ; return false ; } let newNode = new commonmark . Node ( "html_inline" , node . sourcepos ) ; let title = `title="${ ( project . linkTooltips ? node . destination . replace ( "%7B" , "{" ) . replace ( "%7D" , "}" ) : "" ) }"` ; let attrs = "" ; for ( let i = 0 ; i < attributes . length ; i ++ ) { attrs += ` ${ attributes [ i ] . attr }="${ attributes [ i ] . value }"` ; } newNode . literal = `<a ${ title }${ attrs }` ; if ( id !== null ) { newNode . literal += ` id="${ id }"` ; } newNode . literal += `>${ GetLinkText ( node ) }</a>` ; node . insertBefore ( newNode ) ; node . unlink ( ) ; return true ; } param s param startIndex returns function SkipEscapedSubstring ( s , startIndex ) { if ( s [ startIndex ] !== '\\' ) { return startIndex ; } if ( s [ startIndex + 1 ] === '\\' ) { return startIndex ; } if ( s [ startIndex + 1 ] !== '{' ) { return startIndex + 1 ; } let braceCount = 0 ; for ( let i = startIndex + 1 ; i < s . length ; i ++ ) { if ( s [ i ] === '{' ) { ++ braceCount ; } else if ( s [ i ] === '}' && -- braceCount === 0 ) { return i ; } } return startIndex + 1 ; } param s returns function StripLeadingWhitespace ( s ) { for ( let i = 0 ; i < s . length ; i ++ ) { if ( s [ i ] !== " " && s [ i ] !== "\t" && s [ i ] !== "\n" ) { return s . substring ( i ) ; } } return "" ; } }