2017-07-28 08:09:26 +00:00
/ * *
* Copyright 2017 Google Inc . All rights reserved .
*
* Licensed under the Apache License , Version 2.0 ( the "License" ) ;
* you may not use this file except in compliance with the License .
* You may obtain a copy of the License at
*
* http : //www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing , software
* distributed under the License is distributed on an "AS IS" BASIS ,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
* See the License for the specific language governing permissions and
* limitations under the License .
* /
2017-07-11 15:57:26 +00:00
const Documentation = require ( './Documentation' ) ;
const commonmark = require ( 'commonmark' ) ;
class MDOutline {
/ * *
2017-07-12 18:42:36 +00:00
* @ param { ! Page } page
2017-07-11 15:57:26 +00:00
* @ param { string } text
* @ return { ! MDOutline }
* /
2017-07-12 18:42:36 +00:00
static async create ( page , text ) {
2017-07-11 15:57:26 +00:00
// Render markdown as HTML.
const reader = new commonmark . Parser ( ) ;
const parsed = reader . parse ( text ) ;
const writer = new commonmark . HtmlRenderer ( ) ;
const html = writer . render ( parsed ) ;
// Extract headings.
await page . setContent ( html ) ;
2017-07-15 17:20:10 +00:00
const { classes , errors } = await page . evaluate ( ( ) => {
2017-08-21 23:39:04 +00:00
const classes = [ ] ;
2017-07-11 15:57:26 +00:00
let currentClass = { } ;
2017-07-12 15:01:21 +00:00
let member = { } ;
2017-08-21 23:39:04 +00:00
const errors = [ ] ;
for ( const element of document . body . querySelectorAll ( 'h3, h4, h4 + ul > li' ) ) {
2017-07-11 15:57:26 +00:00
if ( element . matches ( 'h3' ) ) {
currentClass = {
name : element . textContent ,
2017-07-12 15:01:21 +00:00
members : [ ] ,
2017-07-11 15:57:26 +00:00
} ;
classes . push ( currentClass ) ;
} else if ( element . matches ( 'h4' ) ) {
2017-07-12 15:01:21 +00:00
member = {
2017-07-11 15:57:26 +00:00
name : element . textContent ,
2017-07-13 22:54:37 +00:00
args : [ ] ,
2018-11-21 22:49:08 +00:00
returnType : null
2017-07-11 15:57:26 +00:00
} ;
2017-07-12 15:01:21 +00:00
currentClass . members . push ( member ) ;
2017-07-11 15:57:26 +00:00
} else if ( element . matches ( 'li' ) && element . firstChild . matches && element . firstChild . matches ( 'code' ) ) {
2018-11-21 22:49:08 +00:00
member . args . push ( parseProperty ( element ) ) ;
2018-02-09 03:59:46 +00:00
} else if ( element . matches ( 'li' ) && element . firstChild . nodeType === Element . TEXT _NODE && element . firstChild . textContent . toLowerCase ( ) . startsWith ( 'return' ) ) {
2018-11-21 22:49:08 +00:00
member . returnType = parseProperty ( element ) ;
2017-07-15 17:20:10 +00:00
const expectedText = 'returns: ' ;
let actualText = element . firstChild . textContent ;
let angleIndex = actualText . indexOf ( '<' ) ;
let spaceIndex = actualText . indexOf ( ' ' ) ;
2017-10-06 22:35:02 +00:00
angleIndex = angleIndex === - 1 ? actualText . length : angleIndex ;
spaceIndex = spaceIndex === - 1 ? actualText . length : spaceIndex + 1 ;
2017-07-15 17:20:10 +00:00
actualText = actualText . substring ( 0 , Math . min ( angleIndex , spaceIndex ) ) ;
if ( actualText !== expectedText )
errors . push ( ` ${ member . name } has mistyped 'return' type declaration: expected exactly ' ${ expectedText } ', found ' ${ actualText } '. ` ) ;
2017-07-11 15:57:26 +00:00
}
}
2017-07-15 17:20:10 +00:00
return { classes , errors } ;
2018-11-21 22:49:08 +00:00
function parseProperty ( element ) {
const str = element . textContent ;
const name = str . substring ( 0 , str . indexOf ( '<' ) ) . trim ( ) ;
const type = findType ( str ) ;
const properties = [ ] ;
// Strings have enum values instead of properties
if ( ! type . includes ( 'string' ) ) {
for ( const childElement of element . querySelectorAll ( ':scope > ul > li' ) )
properties . push ( parseProperty ( childElement ) ) ;
}
return {
name ,
type ,
properties
} ;
}
/ * *
* @ param { string } str
* @ return { string }
* /
function findType ( str ) {
const start = str . indexOf ( '<' ) + 1 ;
let count = 1 ;
for ( let i = start ; i < str . length ; i ++ ) {
if ( str [ i ] === '<' ) count ++ ;
if ( str [ i ] === '>' ) count -- ;
if ( ! count )
return str . substring ( start , i ) ;
}
return 'unknown' ;
}
2017-07-11 15:57:26 +00:00
} ) ;
2017-07-15 17:20:10 +00:00
return new MDOutline ( classes , errors ) ;
2017-07-11 15:57:26 +00:00
}
2017-07-15 17:20:10 +00:00
constructor ( classes , errors ) {
2017-07-11 15:57:26 +00:00
this . classes = [ ] ;
2017-07-15 17:20:10 +00:00
this . errors = errors ;
2017-07-11 15:57:26 +00:00
const classHeading = /^class: (\w+)$/ ;
const constructorRegex = /^new (\w+)\((.*)\)$/ ;
2017-07-18 01:56:56 +00:00
const methodRegex = /^(\w+)\.([\w$]+)\((.*)\)$/ ;
2017-07-12 15:01:21 +00:00
const propertyRegex = /^(\w+)\.(\w+)$/ ;
2017-07-14 20:03:21 +00:00
const eventRegex = /^event: '(\w+)'$/ ;
2017-07-11 15:57:26 +00:00
let currentClassName = null ;
2017-07-14 05:52:02 +00:00
let currentClassMembers = [ ] ;
2017-07-11 15:57:26 +00:00
for ( const cls of classes ) {
2017-08-21 23:39:04 +00:00
const match = cls . name . match ( classHeading ) ;
2017-07-12 18:42:36 +00:00
if ( ! match )
continue ;
2017-07-11 15:57:26 +00:00
currentClassName = match [ 1 ] ;
2017-08-21 23:39:04 +00:00
for ( const member of cls . members ) {
2017-07-12 15:01:21 +00:00
if ( constructorRegex . test ( member . name ) ) {
2017-08-21 23:39:04 +00:00
const match = member . name . match ( constructorRegex ) ;
2017-07-12 15:01:21 +00:00
handleMethod . call ( this , member , match [ 1 ] , 'constructor' , match [ 2 ] ) ;
} else if ( methodRegex . test ( member . name ) ) {
2017-08-21 23:39:04 +00:00
const match = member . name . match ( methodRegex ) ;
2017-07-12 15:01:21 +00:00
handleMethod . call ( this , member , match [ 1 ] , match [ 2 ] , match [ 3 ] ) ;
} else if ( propertyRegex . test ( member . name ) ) {
2017-08-21 23:39:04 +00:00
const match = member . name . match ( propertyRegex ) ;
2017-07-12 15:01:21 +00:00
handleProperty . call ( this , member , match [ 1 ] , match [ 2 ] ) ;
2017-07-14 20:03:21 +00:00
} else if ( eventRegex . test ( member . name ) ) {
2017-08-21 23:39:04 +00:00
const match = member . name . match ( eventRegex ) ;
2017-07-14 20:03:21 +00:00
handleEvent . call ( this , member , match [ 1 ] ) ;
2017-07-11 15:57:26 +00:00
}
}
flushClassIfNeeded . call ( this ) ;
}
2017-07-12 15:01:21 +00:00
function handleMethod ( member , className , methodName , parameters ) {
if ( ! currentClassName || ! className || ! methodName || className . toLowerCase ( ) !== currentClassName . toLowerCase ( ) ) {
this . errors . push ( ` Failed to process header as method: ${ member . name } ` ) ;
return ;
}
parameters = parameters . trim ( ) . replace ( /[\[\]]/g , '' ) ;
2018-11-21 22:49:08 +00:00
if ( parameters !== member . args . map ( arg => arg . name ) . join ( ', ' ) )
this . errors . push ( ` Heading arguments for " ${ member . name } " do not match described ones, i.e. " ${ parameters } " != " ${ member . args . map ( a => a . name ) . join ( ', ' ) } " ` ) ;
const args = member . args . map ( createPropertyFromJSON ) ;
let returnType = null ;
if ( member . returnType )
returnType = createPropertyFromJSON ( member . returnType ) . type ;
const method = Documentation . Member . createMethod ( methodName , args , returnType ) ;
2017-07-14 05:52:02 +00:00
currentClassMembers . push ( method ) ;
2017-07-12 15:01:21 +00:00
}
2018-11-21 22:49:08 +00:00
function createPropertyFromJSON ( payload ) {
const type = new Documentation . Type ( payload . type , payload . properties . map ( createPropertyFromJSON ) ) ;
return Documentation . Member . createProperty ( payload . name , type ) ;
}
2017-07-12 15:01:21 +00:00
function handleProperty ( member , className , propertyName ) {
if ( ! currentClassName || ! className || ! propertyName || className . toLowerCase ( ) !== currentClassName . toLowerCase ( ) ) {
this . errors . push ( ` Failed to process header as property: ${ member . name } ` ) ;
return ;
}
2017-07-14 05:52:02 +00:00
currentClassMembers . push ( Documentation . Member . createProperty ( propertyName ) ) ;
2017-07-12 15:01:21 +00:00
}
2017-07-14 20:03:21 +00:00
function handleEvent ( member , eventName ) {
if ( ! currentClassName || ! eventName ) {
this . errors . push ( ` Failed to process header as event: ${ member . name } ` ) ;
return ;
}
currentClassMembers . push ( Documentation . Member . createEvent ( eventName ) ) ;
}
2017-07-11 15:57:26 +00:00
function flushClassIfNeeded ( ) {
if ( currentClassName === null )
return ;
2017-07-14 05:52:02 +00:00
this . classes . push ( new Documentation . Class ( currentClassName , currentClassMembers ) ) ;
2017-07-11 15:57:26 +00:00
currentClassName = null ;
2017-07-14 05:52:02 +00:00
currentClassMembers = [ ] ;
2017-07-11 15:57:26 +00:00
}
}
}
/ * *
2017-07-12 18:42:36 +00:00
* @ param { ! Page } page
2017-07-31 04:49:04 +00:00
* @ param { ! Array < ! Source > } sources
2017-07-11 15:57:26 +00:00
* @ return { ! Promise < { documentation : ! Documentation , errors : ! Array < string > } > }
* /
2017-07-31 04:49:04 +00:00
module . exports = async function ( page , sources ) {
2017-08-21 23:39:04 +00:00
const classes = [ ] ;
const errors = [ ] ;
for ( const source of sources ) {
const outline = await MDOutline . create ( page , source . text ( ) ) ;
2017-07-11 15:57:26 +00:00
classes . push ( ... outline . classes ) ;
errors . push ( ... outline . errors ) ;
}
const documentation = new Documentation ( classes ) ;
return { documentation , errors } ;
} ;