/**
* Core functionality including section navigation and macro expansion.
*/ export namespace Core { export enum EGotoSectionReason { Goto , Back , Refresh } /**
    * Event listener to call just before the story begins, for user init code to run
    */ let OnBeginStory = [ ] ; param id param element param tags param reason let OnGotoSection = [ ] ; let currentSectionObserver = new MutationObserver ( OnCurrentSectionModified ) ; let currentSectionObserverConfig = { childList : true , attributes : true , characterData : true , subtree : true } ; param element export function ActivateElement ( element ) { if ( element . tagName && element . tagName . toLowerCase ( ) == "a" ) { for ( let i = 0 ; i < element . attributes . length ; i ++ ) { switch ( element . attributes [ i ] . name ) { case "data-goto-section" : { element . addEventListener ( "click" , function ( ) { Core . GotoSection ( element . attributes [ i ] . value ) ; } ) ; break ; } case "data-call-function" : { element . addEventListener ( "click" , RetrieveFromWindow ( element . attributes [ i ] . value , 'function' ) ) ; break ; } case "data-replace-with" : { element . addEventListener ( "click" , function ( ) { Core . ReplaceActiveElement ( element . id , ExpandMacro ( element . attributes [ i ] . value ) ) ; } ) ; break ; } } } } if ( element . id && element . id !== "__currentSection" ) { if ( element . id [ 0 ] !== '!' ) { element . id = `!${ element . id }` ; } } if ( element . children ) { for ( let i = 0 ; i < element . children . length ; i ++ ) { ActivateElement ( element . children [ i ] ) ; } } } param eventName param handler export function AddEventListener ( eventName , handler ) { switch ( eventName ) { case "OnBeginStory" : { OnBeginStory = OnBeginStory . concat ( handler ) ; break ; } case "OnGotoSection" : case "OnGoToSection" : { OnGotoSection = OnGotoSection . concat ( handler ) ; break ; } default : { console . error ( `Core.AddEventListener: "${ eventName }" is not a valid event` ) ; break ; } } } /**
    * Begins the story. Notifies user code and navigates to the "Start" section.
    */ export function BeginStory ( ) { for ( let i = 0 ; i < OnBeginStory . length ; i ++ ) { OnBeginStory [ i ] ( ) ; } let startSections = GetSectionsWithTag ( "Start" ) ; if ( startSections . length == 0 ) { console . error ( `BeginStory(): no section is defined with the Start tag.` ) ; } else if ( startSections . length > 1 ) { console . error ( `BeginStory(): multiple sections defined with the Start tag.` ) ; } else { GotoSection ( startSections [ 0 ] ) ; } } param html param context function CanBeInline ( html , context ) { let root = document . createElement ( "span" ) ; if ( context ) { context . appendChild ( root ) ; } else { document . appendChild ( root ) ; } root . innerHTML = html ; let scan = function ( e ) { if ( getComputedStyle ( e , "" ) . display === "block" ) { return false ; } for ( let i = 0 ; i < e . children . length ; i ++ ) { if ( scan ( e . children [ i ] ) === false ) { return false ; } } return true ; } ; let result = scan ( root ) ; if ( context ) { context . removeChild ( root ) ; } else { document . removeChild ( root ) ; } return result ; } /**
    * Disable any hyperlinks in the given section, while preserving their original link tag in case they need to be re-enabled
    */ function DisableLinks ( section ) { let links = section . getElementsByTagName ( "a" ) ; for ( let i = 0 ; i < links . length ; i ++ ) { let linkTag = links [ i ] . outerHTML . substring ( 0 , links [ i ] . outerHTML . indexOf ( ">" ) + 1 ) ; let contents = links [ i ] . outerHTML . substring ( links [ i ] . outerHTML . indexOf ( ">" ) + 1 , links [ i ] . outerHTML . indexOf ( "</a>" ) ) ; links [ i ] . outerHTML = `<span class="__disabledLink" data-link-tag='${ linkTag }'>${ contents }</span>` ; } } /**
    * Re-enable disabled hyperlinks in the given section
    */ function EnableLinks ( section ) { let links = section . getElementsByClassName ( "__disabledLink" ) ; for ( let i = 0 ; i < links . length ; ) { let linkTag = links [ i ] . getAttribute ( 'data-link-tag' ) ; let contents = links [ i ] . innerHTML ; links [ i ] . outerHTML = linkTag + contents + '</a>' ; } } param macro return export function ExpandMacro ( macro ) { switch ( macro [ 0 ] ) { case '@' : { let sectionName = macro . substring ( 1 ) ; if ( ! document . getElementById ( sectionName ) ) { return `{section "${ sectionName }" is not declared}` ; } else { return ExpandSection ( macro . substring ( 1 ) ) . innerHTML ; } } case '#' : { let functionName = macro . substring ( 1 ) ; let targetFunction = RetrieveFromWindow ( functionName , 'function' ) ; if ( targetFunction !== null && targetFunction !== undefined ) { let result = targetFunction ( ) ; return ( result ? result . toString ( ) : "" ) ; } else { return `{function "${ functionName }" is not defined}` ; } } case '$' : { let variableName = macro . substring ( 1 ) ; let targetVariable = RetrieveFromWindow ( variableName , 'variable' ) ; if ( targetVariable !== null && targetVariable !== undefined ) { return targetVariable . toString ( ) ; } else { return `{variable "${ variableName }" is not defined}` ; } } default : { return `{unknown metacharacter in macro "${ macro }"` ; } } } param id return function ExpandSection ( id ) { let source = document . getElementById ( id ) ; if ( source === null ) { console . log ( "Section " + id + " doesn't exist" ) ; return null ; } let sectionInstance = source . cloneNode ( true ) as Element ; sectionInstance . removeAttribute ( "hidden" ) ; let scan = function ( element ) { for ( let i = 0 ; i < element . attributes . length ; i ++ ) { let expanded = false ; switch ( element . attributes [ i ] . name ) { case "data-expand-macro" : { if ( element . parentElement ) { let newElement = document . createElement ( "span" ) ; newElement . innerHTML = ExpandMacro ( element . attributes [ i ] . value ) ; element . parentElement . replaceChild ( newElement , element ) ; expanded = true ; } break ; } case "data-image-source-macro" : { element . setAttribute ( "src" , ExpandMacro ( element . attributes [ i ] . value ) ) ; expanded = true ; } } if ( expanded ) { break ; } } if ( element . hasChildNodes ) { for ( let i = 0 ; i < element . children . length ; i ++ ) { scan ( element . children [ i ] ) ; } } } ; scan ( sectionInstance ) ; return sectionInstance ; } /**
    * Get a list of the tags the current section was declared with.
    */ export function GetCurrentSectionTags ( ) { return GetSectionTags ( "__currentSection" ) ; } param id export function GetSection ( id ) { let clone = ExpandSection ( id ) ; clone . setAttribute ( 'data-id' , id ) ; return clone ; } export function GetSectionsWithTag ( tag ) { let matchingSections = [ ] ; let sections = document . getElementsByClassName ( "section" ) ; for ( var i = 0 ; i < sections . length ; ++ i ) { let sectionId = sections [ i ] . getAttribute ( 'id' ) ; let sectionTags = GetSectionTags ( sectionId ) ; if ( sectionTags . indexOf ( tag ) !== - 1 ) { matchingSections . push ( sectionId ) ; } } return matchingSections ; } /**
    * Get a list of the tags a section was declared with.
    */ export function GetSectionTags ( id ) { let sectionDiv = document . getElementById ( id ) ; let tagDeclarations = sectionDiv . getAttribute ( "data-tags" ) ; return tagDeclarations . split ( ',' ) ; } /**
    * Navigate to the previous section as it was before transitioning to the current one.
    */ export function GotoPreviousSection ( ) { currentSectionObserver . disconnect ( ) ; let history = document . getElementById ( "__history" ) ; if ( history === null ) { console . error ( "History is not supported in this template (the __history element is missing)" ) ; return ; } let previousSections = history . getElementsByClassName ( '__previousSection' ) ; let previousSection = previousSections [ previousSections . length - 1 ] ; if ( ! previousSection ) { return ; } let id = previousSection . getAttribute ( 'data-id' ) ; let clone = previousSection . cloneNode ( true ) as Element ; EnableLinks ( clone ) ; SetElementAsCurrentSection ( clone ) ; for ( let i = 0 ; i < OnGotoSection . length ; i ++ ) { OnGotoSection [ i ] ( id , clone , GetSectionTags ( id ) , EGotoSectionReason . Back ) ; } history . removeChild ( previousSection ) ; } export function GoToPreviousSection ( ) { GotoPreviousSection ( ) ; } param id export function GotoSection ( id ) { currentSectionObserver . disconnect ( ) ; let currentSection = document . getElementById ( "__currentSection" ) ; DisableLinks ( currentSection ) ; let history = document . getElementById ( "__history" ) ; let previousSectionId = currentSection . getAttribute ( 'data-id' ) ; if ( previousSectionId !== null && history !== null ) { history . innerHTML += `<div class="__previousSection" data-id="${ previousSectionId }">${ currentSection . innerHTML }</div>` ; history . scrollTop = history . scrollHeight ; } let clone = GetSection ( id ) ; SetElementAsCurrentSection ( clone ) ; for ( let i = 0 ; i < OnGotoSection . length ; i ++ ) { OnGotoSection [ i ] ( id , clone , GetSectionTags ( id ) , EGotoSectionReason . Goto ) ; } } export function GoToSection ( id ) { GotoSection ( id ) ; } param mutations function OnCurrentSectionModified ( mutations ) { for ( let i = 0 ; i < mutations . length ; i ++ ) { for ( let j = 0 ; j < mutations [ i ] . addedNodes . length ; j ++ ) { let e = mutations [ i ] . addedNodes [ j ] as Element ; ActivateElement ( e ) ; } } } /**
    * Reloads the current section without creating a new history entry.
    */ export function RefreshCurrentSection ( ) { currentSectionObserver . disconnect ( ) ; let currentSection = document . getElementById ( "__currentSection" ) ; let id = currentSection . getAttribute ( "data-id" ) ; let clone = GetSection ( id ) ; SetElementAsCurrentSection ( clone ) ; for ( let i = 0 ; i < OnGotoSection . length ; i ++ ) { OnGotoSection [ i ] ( id , clone , GetSectionTags ( id ) , EGotoSectionReason . Refresh ) ; } } /**
    * Access a global variable dynamically from the window by splitting its name at any .'s in order to keep indexing recursively.
    * This is like what you'd expect window["foo.bar.baz"] would do... if that syntax were legal.
    */ function RetrieveFromWindow ( name , type ) { let targetObject = null ; let tokens = name . split ( '.' ) ; for ( let i = 0 ; i < tokens . length ; i ++ ) { if ( i === 0 ) { targetObject = window [ tokens [ 0 ] ] ; } else { targetObject = targetObject [ tokens [ i ] ] ; } } if ( targetObject === undefined ) { console . log ( `{${ type } "${ name }" is not declared}` ) ; return undefined ; } return targetObject ; } param id param html export function ReplaceActiveElement ( id , html ) { let element = document . getElementById ( id [ 0 ] === '!' ? id : `!${ id }` ) ; if ( ! element ) { return ; } let replacement = document . createElement ( CanBeInline ( html , element . parentElement ) ? "span" : "div" ) ; replacement . className = "__inlineMacro" ; replacement . innerHTML = html ; ActivateElement ( replacement ) ; element . parentNode . replaceChild ( replacement , element ) ; } param e function SetElementAsCurrentSection ( e ) { let currentSection = document . getElementById ( "__currentSection" ) ; e . scrollTop = 0 ; e . id = "__currentSection" ; ActivateElement ( e ) ; currentSection . parentElement . replaceChild ( e , currentSection ) ; currentSectionObserver . observe ( e , currentSectionObserverConfig ) ; } window [ 'Core' ] = Core ; }