import { dirname , sep } from '@stoplight/path' ; import { NodeType } from '@stoplight/types' ; import { escapeRegExp , flow , partial , sortBy } from 'lodash' ; import { Group , isDivider , isGroup , isItem , ITableOfContents , Item , NodeData , TableOfContentItem } from './types' ; type SchemaType = 'divider' | 'group' ; export function generateProjectToC ( searchResults ) { return flow ( ( ) => searchResults , groupNodesByType , ( { articles , models , httpServices , httpOperations } ) => { const toc = { items : [ ] } ; flow ( ( ) => articles , sortArticlesByTypeAndPath , appendArticlesToToC ( toc ) ) ( ) ; if ( httpServices . length > 0 ) { toc . items . push ( { type : 'divider' , title : 'APIS' } ) ; } flow ( ( ) => httpServices , httpServices => appendHttpServicesToToC ( toc ) ( { httpServices , models , httpOperations } ) , appendModelsToToc ( toc , 'divider' ) ) ( ) ; return toc ; } ) ( ) ; } export function generateTocSkeleton ( searchResults ) { return flow ( ( ) => searchResults , groupNodesByType , ( { articles , models , httpServices } ) => { const toc = { items : [ ] } ; flow ( ( ) => articles , sortArticlesByTypeAndPath , appendArticlesToToC ( toc ) ) ( ) ; toc . items . push ( { type : 'divider' , title : 'APIS' } ) ; flow ( ( ) => httpServices , appendHttpServicesItemsToToC ( toc ) ) ( ) ; flow ( ( ) => models , appendModelsToToc ( toc ) ) ( ) ; return toc ; } ) ( ) ; } function modifyEach ( items , apply , shouldReplace ? ) { for ( let i = items . length - 1 ; i > - 1 ; i -- ) { const item = items [ i ] ; if ( isItem ( item ) ) { if ( shouldReplace ?. ( item ) ) { items . splice ( i , 1 , ... apply ( item ) ) ; } else { items . splice ( i + 1 , 0 , ... apply ( item ) ) ; } } if ( isGroup ( item ) ) { modifyEach ( item . items , apply , shouldReplace ) ; } } } function cleanDividers ( group ) { group . items = group . items . filter ( item => ! isDivider ( item ) ) ; group . items . filter ( isGroup ) . forEach ( cleanDividers ) ; } function cleanToc ( toc ) { toc . items . filter ( isGroup ) . forEach ( cleanDividers ) ; } export function resolveHttpServices ( searchResults , toc ) { cleanToc ( toc ) ; flow ( ( ) => searchResults , groupNodesByType , ( { models , httpServices , httpOperations } ) => { modifyEach ( toc . items , item => { const httpService = httpServices . find ( matchesUri ( item . uri ) ) ; if ( ! httpService ) return [ ] ; const { hasTags , ... subNodes } = filterByUriRegexpAndCheckTags ( [ ] ) ( { httpOperations , models , regexp : new RegExp ( `^${ escapeRegExp ( httpService . uri ) }\/` , 'i' ) } ) ; const items = [ ] ; appendHttpServiceItemsToToC ( { items } ) ( subNodes , httpService . tags || [ ] ) ; return [ { type : 'group' , title : item . title , items , uri : httpService . uri } ] ; } , item => httpServices . some ( matchesUri ( item . uri ) ) ) ; } ) ( ) ; } function matchesUri ( uri ) { return ( item ) => item . uri . replace ( /^\// , '' ) === uri . replace ( /^\// , '' ) ; } export function groupNodesByType ( searchResults ) { return searchResults . reduce < { articles : Array < NodeData > ; models : Array < NodeData > ; httpServices : Array < NodeData > ; httpOperations : Array < NodeData > ; } > ( ( results , searchResult ) => { switch ( searchResult . type ) { case NodeType . Article : results . articles . push ( searchResult ) ; break ; case NodeType . HttpOperation : results . httpOperations . push ( searchResult ) ; break ; case NodeType . HttpService : results . httpServices . push ( searchResult ) ; break ; case NodeType . Model : results . models . push ( searchResult ) ; break ; } return results ; } , { articles : [ ] , models : [ ] , httpServices : [ ] , httpOperations : [ ] } ) ; } export function sortArticlesByTypeAndPath ( articles ) { return articles . sort ( ( a1 , a2 ) => { const rootDirs = [ '/' , '.' , '/docs' , 'docs' ] ; const a1DirPath = dirname ( a1 . uri ) ; const a2DirPath = dirname ( a2 . uri ) ; const dirOrder = rootDirs . includes ( a1DirPath ) || rootDirs . includes ( a2DirPath ) ? a1DirPath . localeCompare ( a2DirPath ) : 0 ; return dirOrder === 0 ? a1 . uri . localeCompare ( a2 . uri ) : dirOrder ; } ) ; } function findOrCreateArticleGroup ( toc ) { return ( dirs ) => { return dirs . reduce < ITableOfContents | Group > ( ( group , dir , i ) => { if ( i === 0 ) { const divider = group . items . filter ( isDivider ) . find ( item => item . title === dir ) ; if ( ! divider ) { group . items . push ( { type : 'divider' , title : dir } ) ; } return group ; } else { const childGroup = group . items . filter ( isGroup ) . find ( item => item . title === dir ) ; if ( ! childGroup ) { const newGroup = { type : 'group' , title : dir , items : [ ] } ; group . items . push ( newGroup ) ; return newGroup ; } return childGroup ; } } , toc ) ; } ; } export function appendArticlesToToC ( toc ) { return ( articles ) => { return articles . reduce < ITableOfContents > ( ( toc , article ) => flow ( partial ( getDirsFromUri , article . uri ) , findOrCreateArticleGroup ( toc ) , group => { group . items . push ( { type : 'item' , title : article . name , uri : article . uri } ) ; return toc ; } ) ( ) , toc ) ; } ; } function filterByUriRegexpAndCheckTags ( standaloneModels ) { return ( { regexp , httpOperations , models } ) => { let hasTags = false ; return { httpOperations : httpOperations . filter ( n => { const isRelevant = regexp . test ( n . uri ) ; if ( isRelevant && n . tags ?. length ) { hasTags = true ; } return isRelevant ; } ) , models : models . filter ( n => { const isRelevant = regexp . test ( n . uri ) ; if ( isRelevant ) { if ( n . tags ?. length ) { hasTags = true ; } standaloneModels . splice ( standaloneModels . findIndex ( m => m === n ) , 1 ) ; } return isRelevant ; } ) , hasTags } ; } ; } function appendHttpServiceItemsToToC ( toc ) { return ( { httpOperations , models } , serviceTagNames ) => { const operationsAndModels = [ ... httpOperations . map ( i => ( { ... i , nodeType : NodeType . HttpOperation } ) ) , ... models . map ( i => ( { ... i , nodeType : NodeType . Model } ) ) , ] ; const { groups , ungroupedModels , ungroupedOperations } = operationsAndModels . reduce < { groups : { [ key ] : Group ; } ; ungroupedOperations : Item [ ] ; ungroupedModels : NodeData [ ] ; } > ( ( result , subNode ) => { const [ tagName ] = subNode . tags || [ ] ; const item = { type : 'item' , title : subNode . name , uri : subNode . uri } ; if ( tagName ) { if ( result . groups [ tagName . toLowerCase ( ) ] ) { result . groups [ tagName . toLowerCase ( ) ] . items . push ( item ) ; } else { const serviceTagName = serviceTagNames . find ( tn => tn . toLowerCase ( ) === tagName . toLowerCase ( ) ) ; result . groups [ tagName . toLowerCase ( ) ] = { type : 'group' , title : serviceTagName || tagName , items : [ item ] } ; } } else { if ( subNode . nodeType === NodeType . HttpOperation ) { result . ungroupedOperations . push ( item ) ; } else { result . ungroupedModels . push ( subNode ) ; } } return result ; } , { groups : { } , ungroupedOperations : [ ] , ungroupedModels : [ ] } ) ; ungroupedOperations . forEach ( item => toc . items . push ( item ) ) ; const tagNamesLC = serviceTagNames . map ( tn => tn . toLowerCase ( ) ) ; Object . entries ( groups ) . sort ( ( [ g1 ] , [ g2 ] ) => { const g1LC = g1 . toLowerCase ( ) ; const g2LC = g2 . toLowerCase ( ) ; const g1Idx = tagNamesLC . findIndex ( tn => tn === g1LC ) ; const g2Idx = tagNamesLC . findIndex ( tn => tn === g2LC ) ; if ( g1Idx < 0 && g2Idx < 0 ) return 0 ; if ( g1Idx < 0 ) return 1 ; if ( g2Idx < 0 ) return - 1 ; return g1Idx - g2Idx ; } ) . forEach ( ( [ , group ] ) => toc . items . push ( group ) ) ; appendModelsToToc ( toc , 'group' ) ( ungroupedModels ) ; } ; } function appendHttpServicesItemsToToC ( toc ) { return ( httpServices ) => { if ( httpServices . length ) { httpServices . forEach ( httpService => toc . items . push ( { type : 'item' , title : httpService . name , uri : httpService . uri } ) ) ; } } ; } export function appendHttpServicesToToC ( toc ) { return ( { httpServices , httpOperations , models , } ) => { const standaloneModels = models . slice ( ) ; sortBy ( httpServices , 'name' ) . forEach ( httpService => { let tocNode ; tocNode = { type : 'group' , title : httpService . name , items : [ ] , uri : httpService . uri } ; toc . items . push ( tocNode ) ; flow ( ( ) => ( { httpOperations , models , regexp : new RegExp ( `^${ escapeRegExp ( httpService . uri ) }${ httpService . uri . endsWith ( '/' ) ? '' : '/' }` , 'i' ) } ) , filterByUriRegexpAndCheckTags ( standaloneModels ) , ( { hasTags , ... subNodes } ) => appendHttpServiceItemsToToC ( tocNode ) ( subNodes , httpService . tags || [ ] ) ) ( ) ; } ) ; return standaloneModels ; } ; } export function appendModelsToToc ( toc , schemaType = 'divider' ) { return ( models ) => { if ( models . length ) { const { others , groups } = models . reduce < { groups : { [ key ] : Group ; } ; others : Item [ ] ; } > ( ( result , model ) => { const [ tagName ] = model . tags || [ ] ; const item = { type : 'item' , title : model . name , uri : model . uri } ; if ( tagName ) { if ( result . groups [ tagName . toLowerCase ( ) ] ) { result . groups [ tagName . toLowerCase ( ) ] . items . push ( item ) ; } else { result . groups [ tagName . toLowerCase ( ) ] = { type : 'group' , title : tagName , items : [ item ] } ; } } else { result . others . push ( item ) ; } return result ; } , { groups : { } , others : [ ] } ) ; const childItems = [ ] ; Object . entries ( groups ) . forEach ( ( [ , group ] ) => childItems . push ( group ) ) ; others . forEach ( item => childItems . push ( item ) ) ; if ( schemaType === 'divider' ) { toc . items . push ( { type : 'divider' , title : 'Schemas' } ) ; toc . items . push ( ... childItems ) ; } else { toc . items . push ( { type : 'group' , title : 'Schemas' , items : childItems } ) ; } } } ; } function getDirsFromUri ( uri ) { const strippedUri = uri . replace ( /^\/?(?:docs\/)?/ , '' ) ; return strippedUri . split ( sep ) . slice ( 0 , - 1 ) ; }