mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-14 13:45:36 +03:00
285 lines
8.9 KiB
JavaScript
285 lines
8.9 KiB
JavaScript
|
/**
|
||
|
* 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.startsWith('class-'))
|
||
|
bodyParts.push(fs.readFileSync(path.join(apiDir, name)).toString());
|
||
|
if (name === 'params.md')
|
||
|
paramsPath = path.join(apiDir, name);
|
||
|
}
|
||
|
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;
|
||
|
let langs = null;
|
||
|
const name = node.text.substring('class: '.length);
|
||
|
for (const member of node.children) {
|
||
|
if (member.type.startsWith('h'))
|
||
|
continue;
|
||
|
if (member.type === 'li' && member.text.startsWith('extends: [')) {
|
||
|
extendsName = member.text.substring('extends: ['.length, member.text.indexOf(']'));
|
||
|
continue;
|
||
|
}
|
||
|
if (member.type === 'li' && member.text.startsWith('langs: ')) {
|
||
|
langs = new Set(member.text.substring('langs: '.length).split(',').map(l => l.trim()));
|
||
|
continue;
|
||
|
}
|
||
|
}
|
||
|
const clazz = new Documentation.Class(langs, name, [], extendsName, extractComments(node));
|
||
|
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;
|
||
|
let langs = null;
|
||
|
|
||
|
for (const item of spec.children || []) {
|
||
|
if (item.type === 'li' && item.liType === 'default')
|
||
|
returnType = this.parseType(item);
|
||
|
if (item.type === 'li' && item.liType === 'bullet' && item.text.startsWith('langs: '))
|
||
|
langs = new Set(item.text.substring('langs: '.length).split(',').map(l => l.trim()));
|
||
|
}
|
||
|
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')
|
||
|
member = Documentation.Member.createEvent(langs, name, returnType, extractComments(spec));
|
||
|
if (match[1] === 'property')
|
||
|
member = Documentation.Member.createProperty(langs, name, returnType, extractComments(spec));
|
||
|
if (match[1] === 'method' || match[1] === 'async method')
|
||
|
member = Documentation.Member.createMethod(langs, name, [], returnType, extractComments(spec));
|
||
|
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) {
|
||
|
const param = spec.children[0];
|
||
|
const text = param.text;
|
||
|
const name = text.substring(0, text.indexOf('<')).replace(/\`/g, '').trim();
|
||
|
const comments = extractComments(spec);
|
||
|
return Documentation.Member.createProperty(null, name, this.parseType(param), comments, guessRequired(md.render(comments)));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {MarkdownNode=} spec
|
||
|
* @return {Documentation.Type}
|
||
|
*/
|
||
|
parseType(spec) {
|
||
|
const arg = parseArgument(spec.text);
|
||
|
const properties = [];
|
||
|
for (const child of spec.children || []) {
|
||
|
const { name, text } = parseArgument(child.text);
|
||
|
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 }}
|
||
|
*/
|
||
|
function parseArgument(line) {
|
||
|
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);
|
||
|
const { name: argName } = parseArgument(template.children[0].text);
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
module.exports = { parseApi };
|