import { join } from 'path' ; import * as fs from 'fs-extra' ; /**
 * A group of Guide content.
 * Can contain other Pages or Categories.
 * Cannot contain content on its own except the title.
 */ export interface Category { type : 'category' ; /** Serves both as unique identifier and relative path to the directory. */ id : string ; /** List of locales this Category is available in. */ locales : string [ ] ; /** Titles keyed by locales. */ title : { [ key ] : string ; } ; /** Sub-categories and sub-pages. */ children : Node [ ] ; } /** A single unit of Guide content. */ export interface Page { type : 'page' ; id : string ; locales : string [ ] ; title : { [ key ] : string ; } ; } export type Node = Category | Page ; returns const isDirectory = ( path ) : boolean => { return fs . lstatSync ( path ) . isDirectory ( ) ; } ; returns const match = ( left , right ) : boolean => { return left . type === right . type && left . id === right . id ; } ; export class Parser { private source : string ; constructor ( source ) { this . source = source ; } /** Returns a title of given directory. */ private parseDirTitle ( path ) { if ( path . slice ( 0 , - 3 ) === this . source ) { return 'Suite Guide' ; } return this . parsePageTitle ( join ( path , 'README.md' ) ) ; } /** Returns a title of given page. */ private parsePageTitle ( path ) { const doc = fs . readFileSync ( path ) ; try { return doc . toString ( ) . match ( /^# (.+$)/m ) ! [ 1 ] . replace ( /[\\]/g , '' ) . trim ( ) ; } catch ( e ) { throw new Error ( `Could not parse title from ${ path }.` ) ; } } /** Converts given path to locale- and environment-agnostic id. */ private pathToId ( path ) { const id = path . slice ( this . source . length + 3 ) ; if ( id . length !== 0 ) { return id ; } return '/' ; } param path param locale returns private parseTree ( path , locale ) { if ( isDirectory ( path ) ) { const children = fs . readdirSync ( path ) . filter ( child => child !== 'README.md' ) . map ( child => this . parseTree ( join ( path , child ) , locale ) ) ; return { type : 'category' , id : this . pathToId ( path ) , locales : [ locale ] , title : { [ locale ] : this . parseDirTitle ( path ) } , children : children } ; } return { type : 'page' , id : this . pathToId ( path ) , locales : [ locale ] , title : { [ locale ] : this . parsePageTitle ( path ) } } ; } /**
     * Recursively traverses the left Node and tries to find
     * matching content in the right node.
     * Matching nodes are merged and the result is returned.
     *
     * Nodes in the right that are not in the left are discarded.
     */ private zip ( left , right ) { if ( ! match ( left , right ) ) { return left ; } const common = { ... left , locales : [ ... left . locales , ... right . locales ] , title : { ... left . title , ... right . title } } ; if ( left . type === 'page' ) { return common ; } return { ... ( common as Category ) , children : left . children . map ( leftChild => { const rightChild = ( right as Category ) . children . find ( candidate => { return match ( leftChild , candidate ) ; } ) ; if ( rightChild === undefined ) { console . warn ( `Missing ${ right . locales } alternative for ${ leftChild . id }` ) ; return leftChild ; } return this . zip ( leftChild , rightChild ) ; } ) } ; } returns private parseLocales ( ) { return fs . readdirSync ( this . source ) . filter ( it => { return ( it . length === 2 && isDirectory ( join ( this . source , it ) ) ) ; } ) ; } /**
     * Parses the GitBook content including all locales
     * and returns its object representation for subsequent processing.
     */ parse ( ) { const locales = this . parseLocales ( ) ; console . log ( `Detected ${ locales . length } locales: ${ locales }` ) ; const englishIndex = this . parseTree ( join ( this . source , 'en' ) , 'en' ) ; return ( locales . filter ( locale => locale !== 'en' ) . reduce ( ( englishIndex , locale ) => { const otherIndex = this . parseTree ( join ( this . source , locale ) , locale ) ; return this . zip ( englishIndex , otherIndex ) ; } , englishIndex ) ) ; } }