namespace pxt . tutorial { const _h2Regex = /^##[^#](.*)$([\s\S]*?)(?=^##[^#]|$(?![\r\n]))/gmi ; const _h3Regex = /^###[^#](.*)$([\s\S]*?)(?=^###[^#]|$(?![\r\n]))/gmi ; export function parseTutorial ( tutorialmd ) { const { metadata , body } = parseTutorialMetadata ( tutorialmd ) ; const { steps , activities } = parseTutorialMarkdown ( body , metadata ) ; const title = parseTutorialTitle ( body ) ; if ( ! steps ) return undefined ; const { code , templateCode , editor , language , jres , assetJson , customTs , tutorialValidationRulesStr } = computeBodyMetadata ( body ) ; let tutorialValidationRules ; if ( metadata . tutorialCodeValidation ) { tutorialValidationRules = pxt . Util . jsonTryParse ( tutorialValidationRulesStr ) ; categorizingValidationRules ( tutorialValidationRules , title ) ; } if ( metadata . diffs === true || ( metadata . diffs !== false && metadata . noDiffs !== true && ( ( editor == pxt . BLOCKS_PROJECT_NAME && pxt . appTarget . appTheme . tutorialBlocksDiff ) || ( editor != pxt . BLOCKS_PROJECT_NAME && pxt . appTarget . appTheme . tutorialTextDiff ) ) ) ) { diffify ( steps , activities ) ; } const assetFiles = parseAssetJson ( assetJson ) ; steps . forEach ( step => { step . contentMd = stripHiddenSnippets ( step . contentMd ) ; step . headerContentMd = stripHiddenSnippets ( step . headerContentMd ) ; step . hintContentMd = stripHiddenSnippets ( step . hintContentMd ) ; step . requiredBlockMd = stripHiddenSnippets ( step . requiredBlockMd ) ; } ) ; return { editor , title , steps , activities , code , templateCode , metadata , language , jres , assetFiles , customTs , tutorialValidationRules } ; } export function getMetadataRegex ( ) { return /``` *(sim|block|blocks|filterblocks|spy|ghost|typescript|ts|js|javascript|template|python|jres|assetjson|customts|tutorialValidationRules|requiredTutorialBlock)\s*\n([\s\S]*?)\n```/gmi ; } function computeBodyMetadata ( body ) { let editor = undefined ; const regex = getMetadataRegex ( ) ; let jres ; let code = [ ] ; let templateCode ; let language ; let idx = 0 ; let assetJson ; let customTs ; let tutorialValidationRulesStr ; body . replace ( /((?!.)\s)+/g , "\n" ) . replace ( regex , function ( m0 , m1 , m2 ) { switch ( m1 ) { case "block" : case "blocks" : case "requiredTutorialBlock" : case "filterblocks" : if ( ! checkTutorialEditor ( pxt . BLOCKS_PROJECT_NAME ) ) return undefined ; break ; case "spy" : case "python" : if ( ! checkTutorialEditor ( pxt . PYTHON_PROJECT_NAME ) ) return undefined ; if ( m1 == "python" ) language = m1 ; break ; case "typescript" : case "ts" : case "javascript" : case "js" : if ( ! checkTutorialEditor ( pxt . JAVASCRIPT_PROJECT_NAME ) ) return undefined ; break ; case "template" : templateCode = m2 ; break ; case "jres" : jres = m2 ; break ; case "assetjson" : assetJson = m2 ; break ; case "customts" : customTs = m2 ; m2 = "" ; break ; case "tutorialValidationRules" : tutorialValidationRulesStr = m2 ; break ; } code . push ( m1 == "python" ? `\n${ m2 }\n` : `{\n${ m2 }\n}` ) ; idx ++ ; return "" ; } ) ; editor = editor || pxt . BLOCKS_PROJECT_NAME ; return { code , templateCode , editor , language , jres , assetJson , customTs , tutorialValidationRulesStr } ; function checkTutorialEditor ( expected ) { if ( editor && editor != expected ) { pxt . debug ( `tutorial ambiguous: contains snippets of different types` ) ; return false ; } else { editor = expected ; return true ; } } } function diffify ( steps , activities ) { let lastSrc = undefined ; steps . forEach ( ( step , stepi ) => { if ( step . resetDiff || ( activities && activities . find ( activity => activity . step == stepi ) ) ) lastSrc = undefined ; if ( step . hintContentMd ) { const s = convertSnippetToDiff ( step . hintContentMd ) ; if ( s && s != step . hintContentMd ) { step . hintContentMd = s ; return ; } } if ( step . headerContentMd ) { const s = convertSnippetToDiff ( step . headerContentMd ) ; if ( s && s != step . headerContentMd ) { step . headerContentMd = s ; return ; } } } ) ; function convertSnippetToDiff ( src ) { const diffClasses = { "typescript" : "diff" , "spy" : "diffspy" , "blocks" : "diffblocks" , "python" : "diff" } ; const highlightRx = /\s*(\/\/|#)\s*@highlight/gm ; if ( ! src ) return src ; return src . replace ( /```(typescript|spy|python|blocks|ghost|template)((?:.|[\r\n])+)```/ , function ( m , type , code ) { const fileA = lastSrc ; const hidden = /^(template|ghost)$/ . test ( type ) ; const hasHighlight = highlightRx . test ( code ) ; code = code . replace ( /^\n+/ , '' ) . replace ( /\n+$/ , '' ) ; if ( hasHighlight ) code = code . replace ( highlightRx , '' ) ; lastSrc = code ; if ( ! fileA || hasHighlight || hidden ) return m ; else return `\`\`\`${ diffClasses [ type ] }
${ fileA }
----------
${ code }
\`\`\`` ; } ) ; } } function parseTutorialTitle ( tutorialmd ) { let title = tutorialmd . match ( /^#[^#](.*)$/mi ) ; return title && title . length > 1 ? title [ 1 ] . trim ( ) : null ; } function parseTutorialMarkdown ( tutorialmd , metadata ) { if ( metadata && metadata . activities ) { return parseTutorialActivities ( tutorialmd , metadata ) ; } else { let steps = parseTutorialSteps ( tutorialmd , null , metadata ) ; if ( ! steps || steps . length < 1 ) steps = parseTutorialSteps ( tutorialmd , _h3Regex , metadata ) ; return { steps : steps , activities : null } ; } } function parseTutorialActivities ( markdown , metadata ) { let stepInfo = [ ] ; let activityInfo = [ ] ; markdown . replace ( _h2Regex , function ( match , name , activity ) { let i = activityInfo . length ; activityInfo . push ( { name : name || lf ( "Activity {0}" , i ) , step : stepInfo . length } ) ; let steps = parseTutorialSteps ( activity , _h3Regex , metadata ) ; steps = steps . map ( step => { step . activity = i ; return step ; } ) ; stepInfo = stepInfo . concat ( steps ) ; return "" ; } ) ; return { steps : stepInfo , activities : activityInfo } ; } function parseTutorialSteps ( markdown , regex ? , metadata ? ) { let stepRegex = regex || _h2Regex ; let stepInfo = [ ] ; markdown . replace ( stepRegex , function ( match , flags , step ) { step = step . trim ( ) ; let { header , hint , requiredBlocks } = parseTutorialHint ( step , metadata && metadata . explicitHints , metadata . tutorialCodeValidation ) ; const title = ! flags . match ( /^\{.*\}$/ ) ? flags . replace ( /@(fullscreen|unplugged|showdialog|showhint|tutorialCompleted|resetDiff)/gi , "" ) . trim ( ) : undefined ; let info = { title , contentMd : step , headerContentMd : header } ; if ( /@(fullscreen|unplugged|showdialog|showhint)/i . test ( flags ) ) info . showHint = true ; if ( /@(unplugged|showdialog)/i . test ( flags ) ) info . showDialog = true ; if ( /@tutorialCompleted/ . test ( flags ) ) info . tutorialCompleted = true ; if ( /@resetDiff/ . test ( flags ) ) info . resetDiff = true ; if ( hint ) info . hintContentMd = hint ; if ( metadata . tutorialCodeValidation && requiredBlocks ) info . requiredBlockMd = requiredBlocks ; stepInfo . push ( info ) ; return "" ; } ) ; if ( markdown . indexOf ( "# Not found" ) == 0 ) { pxt . debug ( `tutorial not found` ) ; return undefined ; } return stepInfo ; } function parseTutorialHint ( step , explicitHints ? , tutorialCodeValidationEnabled ? ) { step = stripHiddenSnippets ( step ) ; let header = step , hint ; let requiredBlocks ; if ( explicitHints ) { const hintTextRegex = /#+ ~ tutorialhint([\s\S]*)/i ; header = step . replace ( hintTextRegex , function ( f , m ) { hint = m ; return "" ; } ) ; } else { const hintTextRegex = /(^[\s\S]*?\S)\s*((```|\[?\!\[[\s\S]+?\]\(\S+?\)\]?)[\s\S]*)/mi ; let hintText = step . match ( hintTextRegex ) ; if ( hintText && hintText . length > 2 ) { header = hintText [ 1 ] . trim ( ) ; hint = hintText [ 2 ] . trim ( ) ; if ( tutorialCodeValidationEnabled ) { let hintSnippet = hintText [ 2 ] . trim ( ) ; hintSnippet = hintSnippet . replace ( /``` *(requiredTutorialBlock)\s*\n([\s\S]*?)\n```/gmi , function ( m0 , m1 , m2 ) { requiredBlocks = `{\n${ m2 }\n}` ; return "" ; } ) ; hint = hintSnippet ; } } } return { header , hint , requiredBlocks } ; } function categorizingValidationRules ( listOfRules , title ) { const ruleNames = Object . keys ( listOfRules ) ; for ( let i = 0 ; i < ruleNames . length ; i ++ ) { const setValidationRule = { ruleName : ruleNames [ i ] , enabled : listOfRules [ ruleNames [ i ] ] ? 'true' : 'false' , tutorial : title , } ; pxt . tickEvent ( 'tutorial.validation.setValidationRules' , setValidationRule ) ; } } function stripHiddenSnippets ( str ) { if ( ! str ) return str ; const hiddenSnippetRegex = /```(filterblocks|package|ghost|config|template|jres|assetjson|customts)\s*\n([\s\S]*?)\n```/gmi ; return str . replace ( hiddenSnippetRegex , '' ) . trim ( ) ; } function parseTutorialMetadata ( tutorialmd ) { const metadataRegex = /### @(\S+) ([ \S]+)/gi ; const m = { } ; const body = tutorialmd . replace ( metadataRegex , function ( f , k , v ) { try { m [ k ] = JSON . parse ( v ) ; } catch { m [ k ] = v ; } return "" ; } ) ; const metadata = ( m as TutorialMetadata ) ; if ( metadata . explicitHints !== undefined && pxt . appTarget . appTheme && pxt . appTarget . appTheme . tutorialExplicitHints ) metadata . explicitHints = true ; return { metadata , body } ; } export function highlight ( pre ) { let text = pre . textContent ; text = text . replace ( /img\s*\(\s*"{3}[\s\da-f.#tngrpoyw]*"{3}\s*\)/g , `img(""" """)` ) ; text = text . replace ( /img\s*`[\s\da-f.#tngrpoyw]*`\s*/g , "img` `" ) ; if ( ! /@highlight/ . test ( text ) ) { pre . textContent = text ; return ; } pre . textContent = "" ; const lines = text . split ( '\n' ) ; for ( let i = 0 ; i < lines . length ; ++ i ) { if ( i > 0 && i < lines . length ) pre . appendChild ( document . createTextNode ( "\n" ) ) ; let line = lines [ i ] ; if ( /@highlight/ . test ( line ) ) { line = lines [ ++ i ] ; if ( line !== undefined ) { const span = document . createElement ( "span" ) ; span . className = "highlight-line" ; span . textContent = line ; pre . appendChild ( span ) ; } } else { pre . appendChild ( document . createTextNode ( line ) ) ; } } } export function getTutorialOptions ( md , tutorialId , filename , reportId , recipe ) { const tutorialInfo = pxt . tutorial . parseTutorial ( md ) ; if ( ! tutorialInfo ) throw new Error ( lf ( "Invalid tutorial format" ) ) ; const tutorialOptions = { tutorial : tutorialId , tutorialName : tutorialInfo . title || filename , tutorialReportId : reportId , tutorialStep : 0 , tutorialReady : true , tutorialHintCounter : 0 , tutorialStepInfo : tutorialInfo . steps , tutorialActivityInfo : tutorialInfo . activities , tutorialMd : md , tutorialCode : tutorialInfo . code , tutorialRecipe : ! ! recipe , templateCode : tutorialInfo . templateCode , autoexpandStep : tutorialInfo . metadata ?. autoexpandOff ? false : true , metadata : tutorialInfo . metadata , language : tutorialInfo . language , jres : tutorialInfo . jres , assetFiles : tutorialInfo . assetFiles , customTs : tutorialInfo . customTs , tutorialValidationRules : tutorialInfo . tutorialValidationRules } ; return { options : tutorialOptions , editor : tutorialInfo . editor } ; } export function parseCachedTutorialInfo ( json , id ? ) { let cachedInfo = pxt . Util . jsonTryParse ( json ) as pxt . Map < pxt . BuiltTutorialInfo > ; if ( ! cachedInfo ) return Promise . resolve ( ) ; return pxt . BrowserUtils . tutorialInfoDbAsync ( ) . then ( db => { if ( id && cachedInfo [ id ] ) { const info = cachedInfo [ id ] ; if ( info . usedBlocks && info . hash ) db . setWithHashAsync ( id , info . snippetBlocks , info . hash ) ; } else { for ( let key of Object . keys ( cachedInfo ) ) { const info = cachedInfo [ key ] ; if ( info . usedBlocks && info . hash ) db . setWithHashAsync ( key , info . snippetBlocks , info . hash ) ; } } } ) . catch ( ( err ) => { } ) ; } export function resolveLocalizedMarkdown ( ghid , files , fileName ? ) { const mfn = ( fileName || ghid . fileName || "README" ) + ".md" ; let md = undefined ; const [ initialLang , baseLang , initialLangLowerCase ] = pxt . Util . normalizeLanguageCode ( pxt . Util . userLanguage ( ) ) ; if ( initialLang && baseLang && initialLangLowerCase ) { md = files [ `_locales/${ initialLang }/${ mfn }` ] || files [ `_locales/${ initialLangLowerCase }/${ mfn }` ] || files [ `_locales/${ baseLang }/${ mfn }` ] ; } else { md = files [ `_locales/${ initialLang }/${ mfn }` ] ; } md = md || files [ mfn ] ; return md ; } export function parseAssetJson ( json ) { if ( ! json ) return undefined ; const files = JSON . parse ( json ) ; return { [ pxt . TILEMAP_JRES ] : files [ pxt . TILEMAP_JRES ] , [ pxt . TILEMAP_CODE ] : files [ pxt . TILEMAP_CODE ] , [ pxt . IMAGES_JRES ] : files [ pxt . IMAGES_JRES ] , [ pxt . IMAGES_CODE ] : files [ pxt . IMAGES_CODE ] } ; } }