| 
									
										
										
										
											2021-01-08 07:00:04 +08: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. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // @ts-check
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const fs = require('fs'); | 
					
						
							|  |  |  | const path = require('path'); | 
					
						
							|  |  |  | const md = require('../markdown'); | 
					
						
							|  |  |  | const Documentation = require('./documentation'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** @typedef {import('../markdown').MarkdownNode} MarkdownNode */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class ApiParser { | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @param {string} apiDir | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   constructor(apiDir) { | 
					
						
							|  |  |  |     let bodyParts = []; | 
					
						
							|  |  |  |     let paramsPath; | 
					
						
							|  |  |  |     for (const name of fs.readdirSync(apiDir)) { | 
					
						
							|  |  |  |       if (name === 'params.md') | 
					
						
							|  |  |  |         paramsPath = path.join(apiDir, name); | 
					
						
							| 
									
										
										
										
											2021-01-08 08:12:25 +08:00
										 |  |  |       else | 
					
						
							|  |  |  |         bodyParts.push(fs.readFileSync(path.join(apiDir, name)).toString()); | 
					
						
							| 
									
										
										
										
											2021-01-08 07:00:04 +08:00
										 |  |  |     } | 
					
						
							|  |  |  |     const body = md.parse(bodyParts.join('\n')); | 
					
						
							|  |  |  |     const params = paramsPath ? md.parse(fs.readFileSync(paramsPath).toString()) : null; | 
					
						
							|  |  |  |     const api = params ? applyTemplates(body, params) : body; | 
					
						
							|  |  |  |     /** @type {Map<string, Documentation.Class>} */ | 
					
						
							|  |  |  |     this.classes = new Map(); | 
					
						
							|  |  |  |     md.visitAll(api, node => { | 
					
						
							|  |  |  |       if (node.type === 'h1') | 
					
						
							|  |  |  |         this.parseClass(node); | 
					
						
							|  |  |  |       if (node.type === 'h2') | 
					
						
							|  |  |  |         this.parseMember(node); | 
					
						
							|  |  |  |       if (node.type === 'h3') | 
					
						
							|  |  |  |         this.parseArgument(node); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |     this.documentation = new Documentation([...this.classes.values()]); | 
					
						
							|  |  |  |     this.documentation.index(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @param {MarkdownNode} node | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   parseClass(node) { | 
					
						
							|  |  |  |     let extendsName = null; | 
					
						
							|  |  |  |     const name = node.text.substring('class: '.length); | 
					
						
							|  |  |  |     for (const member of node.children) { | 
					
						
							|  |  |  |       if (member.type.startsWith('h')) | 
					
						
							|  |  |  |         continue; | 
					
						
							| 
									
										
										
										
											2021-01-08 08:12:25 +08:00
										 |  |  |       if (member.type === 'li' && member.liType === 'bullet' && member.text.startsWith('extends: [')) { | 
					
						
							| 
									
										
										
										
											2021-01-08 07:00:04 +08:00
										 |  |  |         extendsName = member.text.substring('extends: ['.length, member.text.indexOf(']')); | 
					
						
							|  |  |  |         continue; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-01-08 08:12:25 +08:00
										 |  |  |     const clazz = new Documentation.Class(extractLangs(node), name, [], extendsName, extractComments(node)); | 
					
						
							| 
									
										
										
										
											2021-01-08 07:00:04 +08:00
										 |  |  |     this.classes.set(clazz.name, clazz); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @param {MarkdownNode} spec | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   parseMember(spec) { | 
					
						
							|  |  |  |     const match = spec.text.match(/(event|method|property|async method): ([^.]+)\.(.*)/); | 
					
						
							|  |  |  |     const name = match[3]; | 
					
						
							|  |  |  |     let returnType = null; | 
					
						
							|  |  |  |     for (const item of spec.children || []) { | 
					
						
							|  |  |  |       if (item.type === 'li' && item.liType === 'default') | 
					
						
							|  |  |  |         returnType = this.parseType(item); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     if (!returnType) | 
					
						
							|  |  |  |       returnType = new Documentation.Type('void'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (match[1] === 'async method') { | 
					
						
							|  |  |  |       const templates = [ returnType ]; | 
					
						
							|  |  |  |       returnType = new Documentation.Type('Promise'); | 
					
						
							|  |  |  |       returnType.templates = templates; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     let member; | 
					
						
							|  |  |  |     if (match[1] === 'event') | 
					
						
							| 
									
										
										
										
											2021-01-08 08:12:25 +08:00
										 |  |  |       member = Documentation.Member.createEvent(extractLangs(spec), name, returnType, extractComments(spec)); | 
					
						
							| 
									
										
										
										
											2021-01-08 07:00:04 +08:00
										 |  |  |     if (match[1] === 'property') | 
					
						
							| 
									
										
										
										
											2021-01-08 08:12:25 +08:00
										 |  |  |       member = Documentation.Member.createProperty(extractLangs(spec), name, returnType, extractComments(spec)); | 
					
						
							| 
									
										
										
										
											2021-01-08 07:00:04 +08:00
										 |  |  |     if (match[1] === 'method' || match[1] === 'async method') | 
					
						
							| 
									
										
										
										
											2021-01-08 08:12:25 +08:00
										 |  |  |       member = Documentation.Member.createMethod(extractLangs(spec), name, [], returnType, extractComments(spec)); | 
					
						
							| 
									
										
										
										
											2021-01-08 07:00:04 +08:00
										 |  |  |     const clazz = this.classes.get(match[2]); | 
					
						
							|  |  |  |     clazz.membersArray.push(member); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @param {MarkdownNode} spec | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   parseArgument(spec) { | 
					
						
							|  |  |  |     const match = spec.text.match(/(param|option): ([^.]+)\.([^.]+)\.(.*)/); | 
					
						
							|  |  |  |     const clazz = this.classes.get(match[2]); | 
					
						
							|  |  |  |     const method = clazz.membersArray.find(m => m.kind === 'method' && m.name === match[3]); | 
					
						
							|  |  |  |     if (match[1] === 'param') { | 
					
						
							|  |  |  |       method.argsArray.push(this.parseProperty(spec)); | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       let options = method.argsArray.find(o => o.name === 'options'); | 
					
						
							|  |  |  |       if (!options) { | 
					
						
							|  |  |  |         const type = new Documentation.Type('Object', []); | 
					
						
							|  |  |  |         options = Documentation.Member.createProperty(null, 'options', type, undefined, false); | 
					
						
							|  |  |  |         method.argsArray.push(options); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       const p = this.parseProperty(spec); | 
					
						
							|  |  |  |       p.required = false; | 
					
						
							|  |  |  |       options.type.properties.push(p); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @param {MarkdownNode} spec | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   parseProperty(spec) { | 
					
						
							| 
									
										
										
										
											2021-01-08 08:12:25 +08:00
										 |  |  |     const param = childrenWithoutProperties(spec)[0]; | 
					
						
							| 
									
										
										
										
											2021-01-08 07:00:04 +08:00
										 |  |  |     const text = param.text; | 
					
						
							|  |  |  |     const name = text.substring(0, text.indexOf('<')).replace(/\`/g, '').trim(); | 
					
						
							|  |  |  |     const comments = extractComments(spec); | 
					
						
							| 
									
										
										
										
											2021-01-08 08:12:25 +08:00
										 |  |  |     return Documentation.Member.createProperty(extractLangs(spec), name, this.parseType(param), comments, guessRequired(md.render(comments))); | 
					
						
							| 
									
										
										
										
											2021-01-08 07:00:04 +08:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @param {MarkdownNode=} spec | 
					
						
							|  |  |  |    * @return {Documentation.Type} | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   parseType(spec) { | 
					
						
							| 
									
										
										
										
											2021-01-08 08:12:25 +08:00
										 |  |  |     const arg = parseVariable(spec.text); | 
					
						
							| 
									
										
										
										
											2021-01-08 07:00:04 +08:00
										 |  |  |     const properties = []; | 
					
						
							|  |  |  |     for (const child of spec.children || []) { | 
					
						
							| 
									
										
										
										
											2021-01-08 08:12:25 +08:00
										 |  |  |       const { name, text } = parseVariable(child.text); | 
					
						
							| 
									
										
										
										
											2021-01-08 07:00:04 +08:00
										 |  |  |       const comments = /** @type {MarkdownNode[]} */ ([{ type: 'text', text }]); | 
					
						
							|  |  |  |       properties.push(Documentation.Member.createProperty(null, name, this.parseType(child), comments, guessRequired(text))); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return Documentation.Type.parse(arg.type, properties); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * @param {string} line  | 
					
						
							|  |  |  |  * @returns {{ name: string, type: string, text: string }} | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2021-01-08 08:12:25 +08:00
										 |  |  | function parseVariable(line) { | 
					
						
							| 
									
										
										
										
											2021-01-08 07:00:04 +08:00
										 |  |  |   let match = line.match(/^`([^`]+)` (.*)/); | 
					
						
							|  |  |  |   if (!match) | 
					
						
							|  |  |  |     match = line.match(/^(returns): (.*)/); | 
					
						
							|  |  |  |   if (!match) | 
					
						
							|  |  |  |     match = line.match(/^(type): (.*)/); | 
					
						
							|  |  |  |   if (!match) | 
					
						
							|  |  |  |     throw new Error('Invalid argument: ' + line); | 
					
						
							|  |  |  |   const name = match[1]; | 
					
						
							|  |  |  |   const remainder = match[2]; | 
					
						
							|  |  |  |   if (!remainder.startsWith('<')) | 
					
						
							|  |  |  |     throw new Error('Bad argument: ' + remainder); | 
					
						
							|  |  |  |   let depth = 0; | 
					
						
							|  |  |  |   for (let i = 0; i < remainder.length; ++i) { | 
					
						
							|  |  |  |     const c = remainder.charAt(i); | 
					
						
							|  |  |  |     if (c === '<') | 
					
						
							|  |  |  |       ++depth; | 
					
						
							|  |  |  |     if (c === '>') | 
					
						
							|  |  |  |       --depth; | 
					
						
							|  |  |  |     if (depth === 0) | 
					
						
							|  |  |  |       return { name, type: remainder.substring(1, i), text: remainder.substring(i + 2) }; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   throw new Error('Should not be reached'); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * @param {MarkdownNode[]} body | 
					
						
							|  |  |  |  * @param {MarkdownNode[]} params | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | function applyTemplates(body, params) { | 
					
						
							|  |  |  |   const paramsMap = new Map(); | 
					
						
							|  |  |  |   for (const node of params) | 
					
						
							|  |  |  |     paramsMap.set('%%-' + node.text + '-%%', node); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const visit = (node, parent) => { | 
					
						
							|  |  |  |     if (node.text && node.text.includes('-inline- = %%')) { | 
					
						
							|  |  |  |       const [name, key] = node.text.split('-inline- = '); | 
					
						
							|  |  |  |       const list = paramsMap.get(key); | 
					
						
							|  |  |  |       if (!list) | 
					
						
							|  |  |  |         throw new Error('Bad template: ' + key); | 
					
						
							|  |  |  |       for (const prop of list.children) { | 
					
						
							|  |  |  |         const template = paramsMap.get(prop.text); | 
					
						
							|  |  |  |         if (!template) | 
					
						
							|  |  |  |           throw new Error('Bad template: ' + prop.text); | 
					
						
							| 
									
										
										
										
											2021-01-08 08:12:25 +08:00
										 |  |  |         const children = childrenWithoutProperties(template); | 
					
						
							|  |  |  |         const { name: argName } = parseVariable(children[0].text); | 
					
						
							| 
									
										
										
										
											2021-01-08 07:00:04 +08:00
										 |  |  |         parent.children.push({ | 
					
						
							|  |  |  |           type: node.type, | 
					
						
							|  |  |  |           text: name + argName, | 
					
						
							|  |  |  |           children: template.children.map(c => md.clone(c)) | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } else if (node.text && node.text.includes(' = %%')) { | 
					
						
							|  |  |  |       const [name, key] = node.text.split(' = '); | 
					
						
							|  |  |  |       node.text = name; | 
					
						
							|  |  |  |       const template = paramsMap.get(key); | 
					
						
							|  |  |  |       if (!template) | 
					
						
							|  |  |  |         throw new Error('Bad template: ' + key); | 
					
						
							|  |  |  |       node.children.push(...template.children.map(c => md.clone(c))); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     for (const child of node.children || []) | 
					
						
							|  |  |  |       visit(child, node); | 
					
						
							|  |  |  |     if (node.children) | 
					
						
							|  |  |  |       node.children = node.children.filter(child => !child.text || !child.text.includes('-inline- = %%')); | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   for (const node of body) | 
					
						
							|  |  |  |     visit(node, null); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return body; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * @param {MarkdownNode} item | 
					
						
							|  |  |  |  * @returns {MarkdownNode[]} | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | function extractComments(item) { | 
					
						
							|  |  |  |   return (item.children || []).filter(c => { | 
					
						
							|  |  |  |     if (c.type.startsWith('h')) | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     if (c.type === 'li' && c.liType === 'default') | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     if (c.type === 'li' && c.text.startsWith('langs:')) | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     return true; | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * @param {string} comment | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | function guessRequired(comment) { | 
					
						
							|  |  |  |   let required = true; | 
					
						
							|  |  |  |   if (comment.toLowerCase().includes('defaults to ')) | 
					
						
							|  |  |  |     required = false; | 
					
						
							|  |  |  |   if (comment.startsWith('Optional')) | 
					
						
							|  |  |  |     required = false; | 
					
						
							|  |  |  |   if (comment.endsWith('Optional.')) | 
					
						
							|  |  |  |     required = false; | 
					
						
							|  |  |  |   if (comment.toLowerCase().includes('if set')) | 
					
						
							|  |  |  |     required = false; | 
					
						
							|  |  |  |   if (comment.toLowerCase().includes('if applicable')) | 
					
						
							|  |  |  |     required = false; | 
					
						
							|  |  |  |   if (comment.toLowerCase().includes('if available')) | 
					
						
							|  |  |  |     required = false; | 
					
						
							|  |  |  |   if (comment.includes('**required**')) | 
					
						
							|  |  |  |     required = true; | 
					
						
							|  |  |  |   return required; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * @param {string} apiDir | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | function parseApi(apiDir) { | 
					
						
							|  |  |  |   return new ApiParser(apiDir).documentation; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-08 08:12:25 +08:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @param {MarkdownNode} spec | 
					
						
							|  |  |  |  * @returns {?Set<string>} | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | function extractLangs(spec) { | 
					
						
							|  |  |  |   for (const child of spec.children) | 
					
						
							|  |  |  |   if (child.type === 'li' && child.liType === 'bullet' && child.text.startsWith('langs: ')) | 
					
						
							|  |  |  |     return new Set(child.text.substring('langs: '.length).split(',').map(l => l.trim())); | 
					
						
							|  |  |  |   return null; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * @param {MarkdownNode} spec | 
					
						
							|  |  |  |  * @returns {MarkdownNode[]} | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | function childrenWithoutProperties(spec) { | 
					
						
							|  |  |  |   return spec.children.filter(c => c.liType !== 'bullet' || !c.text.startsWith('langs')); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-08 07:00:04 +08:00
										 |  |  | module.exports = { parseApi }; |