참고소스 수정본
This commit is contained in:
497
참고/playwright-main/utils/doclint/api_parser.js
Normal file
497
참고/playwright-main/utils/doclint/api_parser.js
Normal file
@@ -0,0 +1,497 @@
|
||||
/**
|
||||
* 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 docs = require('./documentation');
|
||||
|
||||
/** @typedef {import('../markdown').MarkdownNode} MarkdownNode */
|
||||
/** @typedef {import('../markdown').MarkdownHeaderNode} MarkdownHeaderNode */
|
||||
/** @typedef {import('../markdown').MarkdownLiNode} MarkdownLiNode */
|
||||
/** @typedef {import('../markdown').MarkdownTextNode} MarkdownTextNode */
|
||||
|
||||
class ApiParser {
|
||||
/**
|
||||
* @param {string} apiDir
|
||||
* @param {string=} paramsPath
|
||||
*/
|
||||
constructor(apiDir, paramsPath) {
|
||||
let bodyParts = [];
|
||||
for (const name of fs.readdirSync(apiDir)) {
|
||||
if (!name.endsWith('.md'))
|
||||
continue;
|
||||
if (name === 'params.md')
|
||||
paramsPath = path.join(apiDir, name);
|
||||
else
|
||||
bodyParts.push(fs.readFileSync(path.join(apiDir, name)).toString());
|
||||
}
|
||||
const body = md.parse(bodyParts.join('\n'));
|
||||
const params = paramsPath ? md.parse(fs.readFileSync(paramsPath).toString()) : undefined;
|
||||
checkNoDuplicateParamEntries(params);
|
||||
const api = params ? applyTemplates(body, params) : body;
|
||||
/** @type {Map<string, docs.Class>} */
|
||||
this.classes = new Map();
|
||||
md.visitAll(api, node => {
|
||||
if (node.type === 'h1')
|
||||
this.parseClass(node);
|
||||
});
|
||||
md.visitAll(api, node => {
|
||||
if (node.type === 'h2')
|
||||
this.parseMember(node);
|
||||
});
|
||||
md.visitAll(api, node => {
|
||||
if (node.type === 'h3')
|
||||
this.parseArgument(node);
|
||||
});
|
||||
this.documentation = new docs.Documentation([...this.classes.values()]);
|
||||
this.documentation.index();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MarkdownHeaderNode} 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;
|
||||
if (member.type === 'li' && member.liType === 'bullet' && member.text.startsWith('extends: [')) {
|
||||
extendsName = member.text.substring('extends: ['.length, member.text.indexOf(']'));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const metainfo = extractMetainfo(node);
|
||||
const clazz = new docs.Class(metainfo, name, [], extendsName, extractComments(node));
|
||||
if (metainfo.hidden)
|
||||
return;
|
||||
this.classes.set(clazz.name, clazz);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {MarkdownHeaderNode} spec
|
||||
*/
|
||||
parseMember(spec) {
|
||||
const match = spec.text.match(/(event|method|property|async method|optional method|optional async method): ([^.]+)\.(.*)/);
|
||||
if (!match)
|
||||
throw new Error('Invalid member: ' + spec.text);
|
||||
const metainfo = extractMetainfo(spec);
|
||||
if (metainfo.hidden)
|
||||
return;
|
||||
|
||||
const name = match[3];
|
||||
let returnType = null;
|
||||
let optional = false;
|
||||
for (const item of spec.children || []) {
|
||||
if (item.type === 'li' && item.liType === 'default') {
|
||||
const parsed = this.parseType(item, metainfo.since ?? 'v1.0');
|
||||
returnType = parsed.type;
|
||||
optional = parsed.optional;
|
||||
}
|
||||
}
|
||||
if (!returnType)
|
||||
returnType = new docs.Type('void');
|
||||
|
||||
const comments = extractComments(spec);
|
||||
let member;
|
||||
if (match[1] === 'event')
|
||||
member = docs.Member.createEvent(metainfo, name, returnType, comments);
|
||||
if (match[1] === 'property')
|
||||
member = docs.Member.createProperty(metainfo, name, returnType, comments, !optional);
|
||||
if (['method', 'async method', 'optional method', 'optional async method'].includes(match[1])) {
|
||||
member = docs.Member.createMethod(metainfo, name, [], returnType, comments);
|
||||
if (match[1].includes('async'))
|
||||
member.async = true;
|
||||
if (match[1].includes('optional'))
|
||||
member.required = false;
|
||||
}
|
||||
if (!member)
|
||||
throw new Error('Unknown member: ' + spec.text);
|
||||
|
||||
const clazz = /** @type {docs.Class} */(this.classes.get(match[2]));
|
||||
if (!clazz)
|
||||
throw new Error(`Unknown class ${match[2]} for member: ` + spec.text);
|
||||
|
||||
const existingMember = clazz.membersArray.find(m => m.name === name && m.kind === member.kind);
|
||||
if (existingMember && isTypeOverride(existingMember, member)) {
|
||||
for (const lang of member?.langs?.only || []) {
|
||||
existingMember.langs.types = existingMember.langs.types || {};
|
||||
existingMember.langs.types[lang] = returnType;
|
||||
}
|
||||
} else {
|
||||
clazz.membersArray.push(member);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MarkdownHeaderNode} spec
|
||||
*/
|
||||
parseArgument(spec) {
|
||||
const match = spec.text.match(/(param|option): (.*)/);
|
||||
if (!match)
|
||||
throw `Something went wrong with matching ${spec.text}`;
|
||||
const metainfo = extractMetainfo(spec);
|
||||
if (metainfo.hidden)
|
||||
return null;
|
||||
|
||||
// For "test.describe.only.title":
|
||||
// - className is "test"
|
||||
// - methodName is "describe.only"
|
||||
// - argument name is "title"
|
||||
const parts = match[2].split('.');
|
||||
const className = parts[0];
|
||||
const name = parts[parts.length - 1];
|
||||
const methodName = parts.slice(1, parts.length - 1).join('.');
|
||||
|
||||
const clazz = this.classes.get(className);
|
||||
if (!clazz)
|
||||
throw new Error('Invalid class ' + className);
|
||||
const method = clazz.membersArray.find(m => m.kind === 'method' && m.name === methodName);
|
||||
if (!method)
|
||||
throw new Error(`Invalid method ${className}.${methodName} when parsing: ${match[0]}`);
|
||||
if (!name)
|
||||
throw new Error('Invalid member name ' + spec.text);
|
||||
if (match[1] === 'param') {
|
||||
const arg = this.parseProperty(spec, match[2]);
|
||||
if (!arg)
|
||||
return;
|
||||
arg.name = name;
|
||||
const existingArg = method.argsArray.find(m => m.name === arg.name);
|
||||
if (existingArg && isTypeOverride(existingArg, arg)) {
|
||||
if (!arg.langs || !arg.langs.only)
|
||||
throw new Error('Override does not have lang: ' + spec.text);
|
||||
for (const lang of arg.langs.only) {
|
||||
existingArg.langs.overrides = existingArg.langs.overrides || {};
|
||||
existingArg.langs.overrides[lang] = arg;
|
||||
}
|
||||
} else {
|
||||
method.argsArray.push(arg);
|
||||
}
|
||||
} else {
|
||||
// match[1] === 'option'
|
||||
const p = this.parseProperty(spec, match[2]);
|
||||
if (!p)
|
||||
return;
|
||||
let options = method.argsArray.find(o => o.name === 'options');
|
||||
if (!options) {
|
||||
const type = new docs.Type('Object', []);
|
||||
options = docs.Member.createProperty({ langs: {}, since: method.since, deprecated: undefined, discouraged: undefined }, 'options', type, undefined, false);
|
||||
method.argsArray.push(options);
|
||||
}
|
||||
p.required = false;
|
||||
options.type?.properties?.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MarkdownHeaderNode} spec
|
||||
* @param {string} memberName
|
||||
* @returns {docs.Member | null}
|
||||
*/
|
||||
parseProperty(spec, memberName) {
|
||||
const param = childrenWithoutProperties(spec)[0];
|
||||
const text = /** @type {string}*/(param.text);
|
||||
if (text.substring(text.lastIndexOf('>') + 1).trim())
|
||||
throw new Error(`Extra information after type while processing "${memberName}".\nYou probably need an extra empty line before the description.\n================\n${text}`);
|
||||
let typeStart = text.indexOf('<');
|
||||
while ('?e'.includes(text[typeStart - 1]))
|
||||
typeStart--;
|
||||
const name = text.substring(0, typeStart).replace(/\`/g, '').trim();
|
||||
const comments = extractComments(spec);
|
||||
const metainfo = extractMetainfo(spec);
|
||||
if (metainfo.hidden)
|
||||
return null;
|
||||
const { type, optional } = this.parseType(/** @type {MarkdownLiNode} */(param), metainfo.since ?? 'v1.0');
|
||||
return docs.Member.createProperty(metainfo, name, type, comments, !optional);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MarkdownLiNode} spec
|
||||
* @param {string} since
|
||||
* @return {{ type: docs.Type, optional: boolean }}
|
||||
*/
|
||||
parseType(spec, since) {
|
||||
const arg = parseVariable(spec.text);
|
||||
const properties = [];
|
||||
/** @type {Object<string, string>} */
|
||||
const langAliases = {};
|
||||
for (const child of /** @type {MarkdownLiNode[]} */ (spec.children) || []) {
|
||||
const childText = /** @type {string} */(child.text);
|
||||
const aliasMatch = childText.match(/^alias(?:-(\w+))?\s*:\s*(.*)$/);
|
||||
if (aliasMatch) {
|
||||
langAliases[aliasMatch[1] || 'default'] = aliasMatch[2].trim();
|
||||
continue;
|
||||
}
|
||||
const { name, text } = parseVariable(childText);
|
||||
const comments = /** @type {MarkdownNode[]} */ ([{ type: 'text', text }]);
|
||||
const childType = this.parseType(child, since);
|
||||
properties.push(docs.Member.createProperty({ langs: {}, since, deprecated: undefined, discouraged: undefined }, name, childType.type, comments, !childType.optional));
|
||||
}
|
||||
const type = docs.Type.parse(arg.type, properties, langAliases);
|
||||
return { type, optional: arg.optional };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} line
|
||||
* @returns {{ name: string, type: string, text: string, optional: boolean }}
|
||||
*/
|
||||
function parseVariable(line) {
|
||||
let match = line.match(/^`([^`]+)` (.*)/);
|
||||
if (!match)
|
||||
match = line.match(/^(returns): (.*)/);
|
||||
if (!match)
|
||||
match = line.match(/^(type): (.*)/);
|
||||
if (!match)
|
||||
match = line.match(/^(argument): (.*)/);
|
||||
if (!match)
|
||||
throw new Error('Invalid argument: ' + line);
|
||||
const name = match[1];
|
||||
let remainder = match[2];
|
||||
let optional = false;
|
||||
while ('?'.includes(remainder[0])) {
|
||||
if (remainder[0] === '?')
|
||||
optional = true;
|
||||
remainder = remainder.substring(1);
|
||||
}
|
||||
if (!remainder.startsWith('<'))
|
||||
throw new Error(`Bad argument: "${name}" in "${line}"`);
|
||||
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), optional };
|
||||
}
|
||||
throw new Error('Should not be reached, line: ' + line);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
const newChildren = [];
|
||||
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 children = childrenWithoutProperties(template);
|
||||
const { name: argName } = parseVariable(children[0].text || '');
|
||||
newChildren.push({
|
||||
type: node.type,
|
||||
text: name + argName,
|
||||
children: [...node.children, ...template.children.map(c => md.clone(c))]
|
||||
});
|
||||
}
|
||||
const nodeIndex = parent.children.indexOf(node);
|
||||
parent.children = [...parent.children.slice(0, nodeIndex), ...newChildren, ...parent.children.slice(nodeIndex + 1)];
|
||||
} 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);
|
||||
// Insert right after all metadata options like "* since",
|
||||
// keeping any additional text like **Usage** below the template.
|
||||
let index = node.children.findIndex(child => child.type !== 'li');
|
||||
if (index === -1)
|
||||
index = 0;
|
||||
node.children.splice(index, 0, ...template.children.map(c => md.clone(c)));
|
||||
} else if (node.text && node.text.includes('%%-template-')) {
|
||||
node.text.replace(/%%-template-[^%]+-%%/, templateName => {
|
||||
const template = paramsMap.get(templateName);
|
||||
if (!template)
|
||||
throw new Error('Bad template: ' + templateName);
|
||||
const nodeIndex = parent.children.indexOf(node);
|
||||
parent.children = [...parent.children.slice(0, nodeIndex), ...template.children, ...parent.children.slice(nodeIndex + 1)];
|
||||
});
|
||||
}
|
||||
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 {MarkdownHeaderNode} item
|
||||
* @returns {MarkdownNode[]}
|
||||
*/
|
||||
function extractComments(item) {
|
||||
return childrenWithoutProperties(item).filter(c => {
|
||||
if (c.type.startsWith('h'))
|
||||
return false;
|
||||
if (c.type === 'li' && c.liType === 'default')
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} apiDir
|
||||
* @param {string=} paramsPath
|
||||
*/
|
||||
function parseApi(apiDir, paramsPath) {
|
||||
return new ApiParser(apiDir, paramsPath).documentation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MarkdownHeaderNode} spec
|
||||
* @returns {import('./documentation').Metainfo & { hidden: boolean }}
|
||||
*/
|
||||
function extractMetainfo(spec) {
|
||||
return {
|
||||
langs: extractLangs(spec),
|
||||
since: extractSince(spec),
|
||||
deprecated: extractAttribute(spec, 'deprecated'),
|
||||
discouraged: extractAttribute(spec, 'discouraged'),
|
||||
hidden: extractHidden(spec),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MarkdownNode} spec
|
||||
* @returns {import('./documentation').Langs}
|
||||
*/
|
||||
function extractLangs(spec) {
|
||||
for (const child of spec.children || []) {
|
||||
if (child.type !== 'li' || child.liType !== 'bullet' || !child.text.startsWith('langs:'))
|
||||
continue;
|
||||
|
||||
const only = child.text.substring('langs:'.length).trim();
|
||||
/** @type {Object<string, string>} */
|
||||
const aliases = {};
|
||||
for (const p of child.children || []) {
|
||||
const match = /** @type {string}*/(p.text).match(/alias-(\w+)[\s]*:(.*)/);
|
||||
if (match)
|
||||
aliases[match[1].trim()] = match[2].trim();
|
||||
}
|
||||
return {
|
||||
only: only ? only.split(',').map(l => l.trim()) : undefined,
|
||||
aliases,
|
||||
types: {},
|
||||
overrides: {}
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MarkdownHeaderNode} spec
|
||||
* @returns {string}
|
||||
*/
|
||||
function extractSince(spec) {
|
||||
for (const child of spec.children) {
|
||||
if (child.type !== 'li' || child.liType !== 'bullet' || !child.text.startsWith('since:'))
|
||||
continue;
|
||||
return child.text.substring(child.text.indexOf(':') + 1).trim();
|
||||
}
|
||||
console.error('Missing since: v1.** declaration in node:');
|
||||
console.error(spec);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MarkdownHeaderNode} spec
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function extractHidden(spec) {
|
||||
for (const child of spec.children) {
|
||||
if (child.type === 'li' && child.liType === 'bullet' && child.text === 'hidden')
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MarkdownHeaderNode} spec
|
||||
* @param {string} name
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
function extractAttribute(spec, name) {
|
||||
for (const child of spec.children) {
|
||||
if (child.type !== 'li' || child.liType !== 'bullet' || !child.text.startsWith(name + ':'))
|
||||
continue;
|
||||
return child.text.substring(child.text.indexOf(':') + 1).trim() || undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MarkdownHeaderNode} spec
|
||||
* @returns {MarkdownNode[]}
|
||||
*/
|
||||
function childrenWithoutProperties(spec) {
|
||||
return (spec.children || []).filter(c => {
|
||||
const isProperty = c.type === 'li' && c.liType === 'bullet' && (c.text.startsWith('langs:') || c.text.startsWith('since:') || c.text.startsWith('deprecated:') || c.text.startsWith('discouraged:') || c.text === 'hidden');
|
||||
return !isProperty;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {docs.Member} existingMember
|
||||
* @param {docs.Member} member
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isTypeOverride(existingMember, member) {
|
||||
if (!existingMember.langs.only || !member.langs.only)
|
||||
return true;
|
||||
const existingOnly = existingMember.langs.only;
|
||||
if (member.langs.only.every(l => existingOnly.includes(l))) {
|
||||
return true;
|
||||
} else if (member.langs.only.some(l => existingOnly.includes(l))) {
|
||||
throw new Error(`Ambiguous language override for: ${member.name}`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MarkdownNode[]=} params
|
||||
*/
|
||||
function checkNoDuplicateParamEntries(params) {
|
||||
if (!params)
|
||||
return;
|
||||
const entries = new Set();
|
||||
for (const node of params) {
|
||||
if (entries.has(node.text))
|
||||
throw new Error('Duplicate param entry, for language-specific params use prefix (e.g. js-...): ' + node.text);
|
||||
entries.add(node.text);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { parseApi };
|
||||
312
참고/playwright-main/utils/doclint/cli.js
Normal file
312
참고/playwright-main/utils/doclint/cli.js
Normal file
@@ -0,0 +1,312 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 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 playwright = require('playwright-core');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { parseApi } = require('./api_parser');
|
||||
const md = require('../markdown');
|
||||
const docs = require('./documentation');
|
||||
const toKebabCase = require('lodash/kebabCase')
|
||||
|
||||
/** @typedef {import('./documentation').Type} Type */
|
||||
/** @typedef {import('../markdown').MarkdownNode} MarkdownNode */
|
||||
|
||||
const PROJECT_DIR = path.join(__dirname, '..', '..');
|
||||
|
||||
const dirtyFiles = new Set();
|
||||
|
||||
run().catch(e => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});;
|
||||
|
||||
function getAllMarkdownFiles(dirPath, filePaths = []) {
|
||||
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
|
||||
if (entry.isFile() && entry.name.toLowerCase().endsWith('.md'))
|
||||
filePaths.push(path.join(dirPath, entry.name));
|
||||
else if (entry.isDirectory())
|
||||
getAllMarkdownFiles(path.join(dirPath, entry.name), filePaths);
|
||||
}
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
async function run() {
|
||||
// Patch README.md
|
||||
const versions = await getBrowserVersions();
|
||||
{
|
||||
const params = new Map();
|
||||
const { chromium, firefox, webkit } = versions;
|
||||
params.set('chromium-version', chromium);
|
||||
params.set('firefox-version', firefox);
|
||||
params.set('webkit-version', webkit);
|
||||
params.set('chromium-version-badge', `[](https://www.chromium.org/Home)`);
|
||||
params.set('firefox-version-badge', `[](https://www.mozilla.org/en-US/firefox/new/)`);
|
||||
params.set('webkit-version-badge', `[](https://webkit.org/)`);
|
||||
|
||||
let content = fs.readFileSync(path.join(PROJECT_DIR, 'README.md')).toString();
|
||||
content = content.replace(/<!-- GEN:([^ ]+) -->([^<]*)<!-- GEN:stop -->/ig, (match, p1) => {
|
||||
if (!params.has(p1)) {
|
||||
console.log(`ERROR: Invalid generate parameter "${p1}" in "${match}"`);
|
||||
process.exit(1);
|
||||
}
|
||||
return `<!-- GEN:${p1} -->${params.get(p1)}<!-- GEN:stop -->`;
|
||||
});
|
||||
writeAssumeNoop(path.join(PROJECT_DIR, 'README.md'), content, dirtyFiles);
|
||||
}
|
||||
|
||||
let playwrightVersion = require(path.join(PROJECT_DIR, 'package.json')).version;
|
||||
if (playwrightVersion.endsWith('-next'))
|
||||
playwrightVersion = playwrightVersion.substring(0, playwrightVersion.indexOf('-next'));
|
||||
|
||||
// Ensure browser versions in browsers.json. This is most important for WebKit
|
||||
// since its version is hardcoded in Playwright library rather then in browser builds.
|
||||
// @see https://github.com/microsoft/playwright/issues/15702
|
||||
{
|
||||
const browsersJSONPath = path.join(__dirname, '..', '..', 'packages/playwright-core/browsers.json');
|
||||
const browsersJSON = JSON.parse(await fs.promises.readFile(browsersJSONPath, 'utf8'));
|
||||
for (const browser of browsersJSON.browsers) {
|
||||
if (versions[browser.name])
|
||||
browser.browserVersion = versions[browser.name];
|
||||
}
|
||||
writeAssumeNoop(browsersJSONPath, JSON.stringify(browsersJSON, null, 2) + '\n', dirtyFiles);
|
||||
}
|
||||
|
||||
// Update device descriptors
|
||||
{
|
||||
const devicesDescriptorsSourceFile = path.join(PROJECT_DIR, 'packages', 'playwright-core', 'src', 'server', 'deviceDescriptorsSource.json')
|
||||
const devicesDescriptors = require(devicesDescriptorsSourceFile)
|
||||
for (const deviceName of Object.keys(devicesDescriptors)) {
|
||||
switch (devicesDescriptors[deviceName].defaultBrowserType) {
|
||||
case 'chromium':
|
||||
devicesDescriptors[deviceName].userAgent = devicesDescriptors[deviceName].userAgent.replace(
|
||||
/(.*Chrome\/)(.*?)( .*)/,
|
||||
`$1${versions.chromium}$3`
|
||||
).replace(
|
||||
/(.*Edg\/)(.*?)$/,
|
||||
`$1${versions.chromium}`
|
||||
)
|
||||
break;
|
||||
case 'firefox':
|
||||
devicesDescriptors[deviceName].userAgent = devicesDescriptors[deviceName].userAgent.replace(
|
||||
/^(.*Firefox\/)(.*?)( .*?)?$/,
|
||||
`$1${versions.firefox}$3`
|
||||
).replace(/^(.*rv:)(.*)(\).*?)$/, `$1${versions.firefox}$3`)
|
||||
break;
|
||||
case 'webkit':
|
||||
devicesDescriptors[deviceName].userAgent = devicesDescriptors[deviceName].userAgent.replace(
|
||||
/(.*Version\/)(.*?)( .*)/,
|
||||
`$1${versions.webkit}$3`
|
||||
)
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
const invalidConfigurations = Object.entries(devicesDescriptors).filter(([_, deviceDescriptor]) => deviceDescriptor.isMobile && deviceDescriptor.defaultBrowserType === 'firefox').map(([deviceName, deviceDescriptor]) => deviceName);
|
||||
if (invalidConfigurations.length > 0)
|
||||
throw new Error(`Invalid Device Configurations. isMobile with Firefox not supported: ${invalidConfigurations.join(', ')}`);
|
||||
writeAssumeNoop(devicesDescriptorsSourceFile, JSON.stringify(devicesDescriptors, null, 2), dirtyFiles);
|
||||
}
|
||||
|
||||
// Validate links/code snippet langs
|
||||
{
|
||||
const langs = ['js', 'java', 'python', 'csharp'];
|
||||
const documentationRoot = path.join(PROJECT_DIR, 'docs', 'src');
|
||||
const apiRoot = path.join(documentationRoot, 'api');
|
||||
const testApiRoot = path.join(documentationRoot, 'test-api');
|
||||
const testReporterApiRoot = path.join(documentationRoot, 'test-reporter-api');
|
||||
const electronApiRoot = path.join(documentationRoot, 'electron-api');
|
||||
const mobileApiRoot = path.join(documentationRoot, 'mobile-api');
|
||||
for (const lang of langs) {
|
||||
try {
|
||||
let documentation = parseApi(apiRoot);
|
||||
if (lang === 'js') {
|
||||
documentation = documentation.mergeWith(
|
||||
parseApi(testApiRoot, path.join(documentationRoot, 'api', 'params.md'))
|
||||
).mergeWith(
|
||||
parseApi(testReporterApiRoot)
|
||||
).mergeWith(
|
||||
parseApi(electronApiRoot, path.join(documentationRoot, 'api', 'params.md'))
|
||||
).mergeWith(
|
||||
parseApi(mobileApiRoot, path.join(documentationRoot, 'api', 'params.md'))
|
||||
);
|
||||
}
|
||||
documentation.filterForLanguage(lang);
|
||||
|
||||
// This validates member links.
|
||||
documentation.setLinkRenderer(() => undefined);
|
||||
// This validates code snippet groups in comments.
|
||||
documentation.setCodeGroupsTransformer(lang, tabs => tabs.map(tab => tab.spec));
|
||||
documentation.generateSourceCodeComments();
|
||||
|
||||
const mdLinks = [];
|
||||
const mdSections = new Set();
|
||||
|
||||
for (const cls of documentation.classesArray) {
|
||||
const filePath = path.join(documentationRoot, 'api', 'class-' + cls.name.toLowerCase() + '.md');
|
||||
for (const member of cls.membersArray) {
|
||||
const memberHash = filePath + '#' + toKebabCase(cls.name).toLowerCase() + '-' + toKebabCase(member.name).toLowerCase()
|
||||
mdSections.add(memberHash);
|
||||
for (const arg of member.argsArray) {
|
||||
mdSections.add(memberHash + '-option-' + toKebabCase(arg.name).toLowerCase());
|
||||
if (arg.name === "options" && arg.type) {
|
||||
for (const option of arg.type.deepProperties())
|
||||
mdSections.add(memberHash + '-option-' + toKebabCase(option.name).toLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const event of cls.eventsArray)
|
||||
mdSections.add(filePath + '#' + toKebabCase(cls.name).toLowerCase() + '-event-' + toKebabCase(event.name).toLowerCase());
|
||||
}
|
||||
|
||||
for (const filePath of getAllMarkdownFiles(documentationRoot)) {
|
||||
if (!filePath.includes(`-${lang}`) && langs.some(other => other !== lang && filePath.includes(`-${other}`)))
|
||||
continue;
|
||||
|
||||
// Standardise naming and remove the filter in the file name
|
||||
// Also, Internally (playwright.dev generator) we merge test-api and test-reporter-api into api.
|
||||
const canonicalName = filePath.replace(/(-(js|python|csharp|java))+/, '').replace(/(\/|\\)(test-api|test-reporter-api|electron-api|mobile-api)(\/|\\)/, `${path.sep}api${path.sep}`);
|
||||
mdSections.add(canonicalName);
|
||||
|
||||
const data = fs.readFileSync(filePath, 'utf-8');
|
||||
let rootNode = md.filterNodesForLanguage(md.parse(data), lang);
|
||||
// Validates code snippet groups.
|
||||
rootNode = docs.processCodeGroups(rootNode, lang, tabs => tabs.map(tab => tab.spec));
|
||||
// Renders links.
|
||||
if (!filePath.startsWith(apiRoot) && !filePath.startsWith(testApiRoot) && !filePath.startsWith(testReporterApiRoot) && !filePath.startsWith(electronApiRoot) && !filePath.startsWith(mobileApiRoot))
|
||||
documentation.renderLinksInNodes(rootNode);
|
||||
// Validate links.
|
||||
{
|
||||
md.visitAll(rootNode, node => {
|
||||
if (node.type === 'code') {
|
||||
const allowedCodeLangs = new Set([
|
||||
'csharp',
|
||||
'java',
|
||||
'css',
|
||||
'js',
|
||||
'markdown',
|
||||
'ts',
|
||||
'python',
|
||||
'py',
|
||||
'java',
|
||||
'powershell',
|
||||
'batch',
|
||||
'ini',
|
||||
'txt',
|
||||
'html',
|
||||
'xml',
|
||||
'yml',
|
||||
'yaml',
|
||||
'json',
|
||||
'groovy',
|
||||
'html',
|
||||
'bash',
|
||||
'sh',
|
||||
'Dockerfile',
|
||||
]);
|
||||
if (!allowedCodeLangs.has(node.codeLang.split(' ')[0]))
|
||||
throw new Error(`${path.relative(PROJECT_DIR, filePath)} contains code block with invalid code block language "${node.codeLang}"`);
|
||||
}
|
||||
if (node.type.startsWith('h')) {
|
||||
const hash = mdSectionHash(node.text || '');
|
||||
mdSections.add(canonicalName + '#' + hash);
|
||||
}
|
||||
if (!node.text)
|
||||
return;
|
||||
// Match links in a lax way (.+), so they can include spaces, backticks etc.
|
||||
for (const [, mdLinkName, mdLink] of node.text.matchAll(/\[(.+)\]\((.*?)\)/g)) {
|
||||
const isExternal = mdLink.startsWith('http://') || mdLink.startsWith('https://');
|
||||
if (isExternal)
|
||||
continue;
|
||||
|
||||
const [beforeHash, hash] = mdLink.split('#');
|
||||
let linkWithoutHash = canonicalName;
|
||||
if (beforeHash) {
|
||||
// Not same-file link.
|
||||
linkWithoutHash = path.join(path.dirname(filePath), beforeHash);
|
||||
if (path.extname(linkWithoutHash) !== '.md')
|
||||
linkWithoutHash += '.md';
|
||||
}
|
||||
mdLinks.push({ filePath, linkTarget: linkWithoutHash + (hash ? '#' + hash : ''), name: mdLinkName });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const badLinks = [];
|
||||
for (const { filePath, linkTarget, name } of mdLinks) {
|
||||
if (linkTarget.startsWith(path.join(documentationRoot, 'images')))
|
||||
continue;
|
||||
if (!mdSections.has(linkTarget))
|
||||
badLinks.push(`${path.relative(PROJECT_DIR, filePath)} references to '${linkTarget}' as '${name}' which does not exist.`);
|
||||
}
|
||||
if (badLinks.length)
|
||||
throw new Error('Broken links found:\n' + badLinks.join('\n'));
|
||||
|
||||
} catch (e) {
|
||||
e.message = `While processing "${lang}"\n` + e.message;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dirtyFiles.size) {
|
||||
console.log('============================')
|
||||
console.log('ERROR: generated files have changed, this is only error if happens in CI:');
|
||||
[...dirtyFiles].forEach(f => console.log(f));
|
||||
console.log('============================')
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {string} content
|
||||
* @param {Set<string>} dirtyFiles
|
||||
*/
|
||||
function writeAssumeNoop(name, content, dirtyFiles) {
|
||||
fs.mkdirSync(path.dirname(name), { recursive: true });
|
||||
const oldContent = fs.existsSync(name) ? fs.readFileSync(name).toString() : '';
|
||||
if (oldContent !== content) {
|
||||
fs.writeFileSync(name, content);
|
||||
dirtyFiles.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
async function getBrowserVersions() {
|
||||
const names = ['chromium', 'firefox', 'webkit'];
|
||||
const browsers = await Promise.all(names.map(name => playwright[name].launch()));
|
||||
const result = {};
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
result[names[i]] = browsers[i].version();
|
||||
}
|
||||
await Promise.all(browsers.map(browser => browser.close()));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
function mdSectionHash(text) {
|
||||
return text.toLowerCase().replace(/\s/g, '-').replace(/[^-_a-z0-9]/g, '').replace(/^-+/, '');
|
||||
}
|
||||
983
참고/playwright-main/utils/doclint/documentation.js
Normal file
983
참고/playwright-main/utils/doclint/documentation.js
Normal file
@@ -0,0 +1,983 @@
|
||||
/**
|
||||
* 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 md = require('../markdown');
|
||||
|
||||
/** @typedef {import('../markdown').MarkdownNode} MarkdownNode */
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* name: string,
|
||||
* args: ParsedType | null,
|
||||
* retType: ParsedType | null,
|
||||
* template: ParsedType | null,
|
||||
* union: ParsedType | null,
|
||||
* unionName?: string,
|
||||
* next: ParsedType | null,
|
||||
* }} ParsedType
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* only?: string[],
|
||||
* aliases?: Object<string, string>,
|
||||
* types?: Object<string, Type>,
|
||||
* overrides?: Object<string, Member>,
|
||||
* }} Langs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {function({
|
||||
* clazz?: Class,
|
||||
* member?: Member,
|
||||
* param?: { name: string, alias: string },
|
||||
* option?: { name: string, alias: string },
|
||||
* href?: string,
|
||||
* }): string|undefined} Renderer
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* langs: Langs,
|
||||
* since: string,
|
||||
* deprecated?: string | undefined,
|
||||
* discouraged?: string | undefined,
|
||||
* }} Metainfo
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* csharpOptionOverloadsShortNotation?: boolean,
|
||||
* }} LanguageOptions
|
||||
*/
|
||||
|
||||
/** @typedef {{
|
||||
* value: string, groupId: string, spec: MarkdownNode
|
||||
* }} CodeGroup */
|
||||
|
||||
/** @typedef {function(CodeGroup[]): MarkdownNode[]} CodeGroupTransformer */
|
||||
|
||||
class Documentation {
|
||||
/**
|
||||
* @param {!Array<!Class>} classesArray
|
||||
*/
|
||||
constructor(classesArray) {
|
||||
this.classesArray = classesArray;
|
||||
/** @type {!Map<string, !Class>} */
|
||||
this.classes = new Map();
|
||||
this.index();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Documentation} documentation
|
||||
* @return {!Documentation}
|
||||
*/
|
||||
mergeWith(documentation) {
|
||||
return new Documentation([...this.classesArray, ...documentation.classesArray].map(cls => cls.clone()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} errors
|
||||
*/
|
||||
copyDocsFromSuperclasses(errors) {
|
||||
for (const [name, clazz] of this.classes.entries()) {
|
||||
clazz.sortMembers();
|
||||
|
||||
if (!clazz.extends || ['Error', 'Exception', 'RuntimeException'].includes(clazz.extends))
|
||||
continue;
|
||||
const superClass = this.classes.get(clazz.extends);
|
||||
if (!superClass) {
|
||||
errors.push(`Undefined superclass: ${superClass} in ${name}`);
|
||||
continue;
|
||||
}
|
||||
for (const memberName of clazz.members.keys()) {
|
||||
if (superClass.members.has(memberName))
|
||||
errors.push(`Member documentation overrides base: ${name}.${memberName} over ${clazz.extends}.${memberName}`);
|
||||
}
|
||||
|
||||
clazz.membersArray = [...clazz.membersArray, ...superClass.membersArray.map(c => c.clone())];
|
||||
clazz.index();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} lang
|
||||
* @param {LanguageOptions=} options
|
||||
*/
|
||||
filterForLanguage(lang, options = {}) {
|
||||
const classesArray = [];
|
||||
for (const clazz of this.classesArray) {
|
||||
if (clazz.langs.only && !clazz.langs.only.includes(lang))
|
||||
continue;
|
||||
clazz.filterForLanguage(lang, options);
|
||||
classesArray.push(clazz);
|
||||
}
|
||||
this.classesArray = classesArray;
|
||||
this.index();
|
||||
}
|
||||
|
||||
index() {
|
||||
for (const cls of this.classesArray) {
|
||||
this.classes.set(cls.name, cls);
|
||||
cls.index();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Renderer} linkRenderer
|
||||
*/
|
||||
setLinkRenderer(linkRenderer) {
|
||||
// @type {Map<string, Class>}
|
||||
const classesMap = new Map();
|
||||
const membersMap = new Map();
|
||||
for (const clazz of this.classesArray) {
|
||||
classesMap.set(clazz.name, clazz);
|
||||
for (const member of clazz.membersArray)
|
||||
membersMap.set(`${member.kind}: ${clazz.name}.${member.name}`, member);
|
||||
}
|
||||
/**
|
||||
* @param {Class|Member|undefined} classOrMember
|
||||
* @param {string} text
|
||||
*/
|
||||
this._patchLinksInText = (classOrMember, text) => patchLinksInText(classOrMember, text, classesMap, membersMap, linkRenderer);
|
||||
|
||||
for (const clazz of this.classesArray)
|
||||
clazz.visit(item => item.spec && this.renderLinksInNodes(item.spec, item));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MarkdownNode[]} nodes
|
||||
* @param {Class|Member=} classOrMember
|
||||
*/
|
||||
renderLinksInNodes(nodes, classOrMember) {
|
||||
if (classOrMember instanceof Member) {
|
||||
classOrMember.discouraged = classOrMember.discouraged ? this.renderLinksInText(classOrMember.discouraged, classOrMember) : undefined;
|
||||
classOrMember.deprecated = classOrMember.deprecated ? this.renderLinksInText(classOrMember.deprecated, classOrMember) : undefined;
|
||||
}
|
||||
md.visitAll(nodes, node => {
|
||||
if (!node.text)
|
||||
return;
|
||||
node.text = this.renderLinksInText(node.text, classOrMember);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {Class|Member=} classOrMember
|
||||
*/
|
||||
renderLinksInText(text, classOrMember) {
|
||||
return this._patchLinksInText?.(classOrMember, text);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} lang
|
||||
* @param {CodeGroupTransformer} transformer
|
||||
*/
|
||||
setCodeGroupsTransformer(lang, transformer) {
|
||||
this._codeGroupsTransformer = { lang, transformer };
|
||||
}
|
||||
|
||||
generateSourceCodeComments() {
|
||||
for (const clazz of this.classesArray) {
|
||||
clazz.visit(item => {
|
||||
let spec = item.spec;
|
||||
if (spec && this._codeGroupsTransformer)
|
||||
spec = processCodeGroups(spec, this._codeGroupsTransformer.lang, this._codeGroupsTransformer.transformer);
|
||||
item.comment = generateSourceCodeComment(spec);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new Documentation(this.classesArray.map(cls => cls.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
class Class {
|
||||
/**
|
||||
* @param {Metainfo} metainfo
|
||||
* @param {string} name
|
||||
* @param {!Array<!Member>} membersArray
|
||||
* @param {?string=} extendsName
|
||||
* @param {MarkdownNode[]=} spec
|
||||
*/
|
||||
constructor(metainfo, name, membersArray, extendsName = null, spec = undefined) {
|
||||
this.langs = metainfo.langs;
|
||||
this.since = metainfo.since;
|
||||
this.deprecated = metainfo.deprecated;
|
||||
this.discouraged = metainfo.discouraged;
|
||||
this.name = name;
|
||||
this.membersArray = membersArray;
|
||||
this.spec = spec;
|
||||
this.extends = extendsName;
|
||||
this.comment = '';
|
||||
this.index();
|
||||
const match = /** @type {string[]} */(name.match(/(API|JS|CDP|[A-Z])(.*)/));
|
||||
this.varName = match[1].toLowerCase() + match[2];
|
||||
/** @type {!Map<string, !Member>} */
|
||||
this.members = new Map();
|
||||
/** @type {!Map<string, !Member>} */
|
||||
this.properties = new Map();
|
||||
/** @type {!Array<!Member>} */
|
||||
this.propertiesArray = [];
|
||||
/** @type {!Map<string, !Member>} */
|
||||
this.methods = new Map();
|
||||
/** @type {!Array<!Member>} */
|
||||
this.methodsArray = [];
|
||||
/** @type {!Map<string, !Member>} */
|
||||
this.events = new Map();
|
||||
/** @type {!Array<!Member>} */
|
||||
this.eventsArray = [];
|
||||
}
|
||||
|
||||
index() {
|
||||
this.members = new Map();
|
||||
this.properties = new Map();
|
||||
this.propertiesArray = [];
|
||||
this.methods = new Map();
|
||||
this.methodsArray = [];
|
||||
this.events = new Map();
|
||||
this.eventsArray = [];
|
||||
|
||||
for (const member of this.membersArray) {
|
||||
this.members.set(member.name, member);
|
||||
if (member.kind === 'method') {
|
||||
this.methods.set(member.name, member);
|
||||
this.methodsArray.push(member);
|
||||
} else if (member.kind === 'property') {
|
||||
this.properties.set(member.name, member);
|
||||
this.propertiesArray.push(member);
|
||||
} else if (member.kind === 'event') {
|
||||
this.events.set(member.name, member);
|
||||
this.eventsArray.push(member);
|
||||
}
|
||||
member.clazz = this;
|
||||
member.index();
|
||||
}
|
||||
}
|
||||
|
||||
clone() {
|
||||
const cls = new Class({ langs: this.langs, since: this.since, deprecated: this.deprecated, discouraged: this.discouraged }, this.name, this.membersArray.map(m => m.clone()), this.extends, this.spec);
|
||||
cls.comment = this.comment;
|
||||
return cls;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} lang
|
||||
* @param {LanguageOptions=} options
|
||||
*/
|
||||
filterForLanguage(lang, options = {}) {
|
||||
const membersArray = [];
|
||||
for (const member of this.membersArray) {
|
||||
if (member.langs.only && !member.langs.only.includes(lang))
|
||||
continue;
|
||||
member.filterForLanguage(lang, options);
|
||||
membersArray.push(member);
|
||||
}
|
||||
this.membersArray = membersArray;
|
||||
}
|
||||
|
||||
sortMembers() {
|
||||
/**
|
||||
* @param {Member} member
|
||||
*/
|
||||
function sortKey(member) {
|
||||
return { 'event': 'a', 'method': 'b', 'property': 'c' }[member.kind] + member.alias;
|
||||
}
|
||||
|
||||
this.membersArray.sort((m1, m2) => {
|
||||
return sortKey(m1).localeCompare(sortKey(m2), 'en', { sensitivity: 'base' });
|
||||
});
|
||||
|
||||
// Ideally, we would automatically make options the last argument.
|
||||
// However, that breaks Java, since options are not always last in Java, for example
|
||||
// in page.waitForFileChooser(options, callback).
|
||||
// So, the order must be carefully setup in the md file!
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(Member|Class): void} visitor
|
||||
*/
|
||||
visit(visitor) {
|
||||
visitor(this);
|
||||
for (const p of this.propertiesArray)
|
||||
p.visit(visitor);
|
||||
for (const m of this.methodsArray)
|
||||
m.visit(visitor);
|
||||
for (const e of this.eventsArray)
|
||||
e.visit(visitor);
|
||||
}
|
||||
}
|
||||
|
||||
class Member {
|
||||
/**
|
||||
* @param {string} kind
|
||||
* @param {Metainfo} metainfo
|
||||
* @param {string} name
|
||||
* @param {?Type} type
|
||||
* @param {!Array<!Member>} argsArray
|
||||
* @param {MarkdownNode[]=} spec
|
||||
* @param {boolean=} required
|
||||
*/
|
||||
constructor(kind, metainfo, name, type, argsArray, spec = undefined, required = true) {
|
||||
this.kind = kind;
|
||||
this.langs = metainfo.langs;
|
||||
this.since = metainfo.since;
|
||||
this.deprecated = metainfo.deprecated;
|
||||
this.discouraged = metainfo.discouraged;
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.spec = spec;
|
||||
this.argsArray = argsArray;
|
||||
this.required = required;
|
||||
this.comment = '';
|
||||
/** @type {!Map<string, !Member>} */
|
||||
this.args = new Map();
|
||||
this.index();
|
||||
/** @type {!Class | null} */
|
||||
this.clazz = null;
|
||||
/** @type {Member=} */
|
||||
this.enclosingMethod = undefined;
|
||||
/** @type {Member=} */
|
||||
this.parent = undefined;
|
||||
this.async = false;
|
||||
this.alias = name;
|
||||
this.overloadIndex = 0;
|
||||
if (name.includes('#')) {
|
||||
const match = /** @type {string[]} */(name.match(/(.*)#(.*)/));
|
||||
this.alias = match[1];
|
||||
this.overloadIndex = (+match[2]) - 1;
|
||||
}
|
||||
}
|
||||
|
||||
index() {
|
||||
this.args = new Map();
|
||||
if (this.kind === 'method')
|
||||
this.enclosingMethod = this;
|
||||
const indexArg = (/** @type {Member} */ arg) => {
|
||||
arg.type?.deepProperties().forEach(p => {
|
||||
p.enclosingMethod = this;
|
||||
p.parent = arg;
|
||||
indexArg(p);
|
||||
});
|
||||
}
|
||||
for (const arg of this.argsArray) {
|
||||
this.args.set(arg.name, arg);
|
||||
arg.enclosingMethod = this;
|
||||
if (arg.name === 'options')
|
||||
arg.type?.properties?.sort((p1, p2) => p1.name.localeCompare(p2.name));
|
||||
indexArg(arg);
|
||||
}
|
||||
// Also index return type properties so they have enclosingMethod set.
|
||||
if (this.kind === 'method' || this.kind === 'property')
|
||||
indexArg(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} lang
|
||||
* @param {LanguageOptions=} options
|
||||
*/
|
||||
filterForLanguage(lang, options = {}) {
|
||||
if (!this.type)
|
||||
return;
|
||||
if (this.langs.aliases && this.langs.aliases[lang])
|
||||
this.alias = this.langs.aliases[lang];
|
||||
if (this.langs.types && this.langs.types[lang])
|
||||
this.type = this.langs.types[lang];
|
||||
this.type.filterForLanguage(lang, options);
|
||||
const argsArray = [];
|
||||
for (const arg of this.argsArray) {
|
||||
if (arg.langs.only && !arg.langs.only.includes(lang))
|
||||
continue;
|
||||
const overriddenArg = (arg.langs.overrides && arg.langs.overrides[lang]) || arg;
|
||||
overriddenArg.filterForLanguage(lang, options);
|
||||
if (overriddenArg.name === 'options' && !overriddenArg.type?.properties?.length)
|
||||
continue;
|
||||
overriddenArg.type?.filterForLanguage(lang, options);
|
||||
argsArray.push(overriddenArg);
|
||||
}
|
||||
this.argsArray = argsArray;
|
||||
|
||||
const optionsArg = this.argsArray.find(arg => arg.name === 'options');
|
||||
if (lang === 'csharp' && optionsArg) {
|
||||
try {
|
||||
patchCSharpOptionOverloads(optionsArg, options);
|
||||
} catch (e) {
|
||||
throw new Error(`Error processing csharp options in ${this.clazz?.name}.${this.name}: ` + e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clone() {
|
||||
const result = new Member(this.kind, { langs: this.langs, since: this.since, deprecated: this.deprecated, discouraged: this.discouraged }, this.name, this.type?.clone(), this.argsArray.map(arg => arg.clone()), this.spec, this.required);
|
||||
result.alias = this.alias;
|
||||
result.async = this.async;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Metainfo} metainfo
|
||||
* @param {string} name
|
||||
* @param {!Array<!Member>} argsArray
|
||||
* @param {?Type} returnType
|
||||
* @param {MarkdownNode[]=} spec
|
||||
* @return {!Member}
|
||||
*/
|
||||
static createMethod(metainfo, name, argsArray, returnType, spec) {
|
||||
return new Member('method', metainfo, name, returnType, argsArray, spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Metainfo} metainfo
|
||||
* @param {!string} name
|
||||
* @param {!Type} type
|
||||
* @param {!MarkdownNode[]=} spec
|
||||
* @param {boolean=} required
|
||||
* @return {!Member}
|
||||
*/
|
||||
static createProperty(metainfo, name, type, spec, required) {
|
||||
return new Member('property', metainfo, name, type, [], spec, required);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Metainfo} metainfo
|
||||
* @param {string} name
|
||||
* @param {?Type=} type
|
||||
* @param {MarkdownNode[]=} spec
|
||||
* @return {!Member}
|
||||
*/
|
||||
static createEvent(metainfo, name, type = null, spec) {
|
||||
return new Member('event', metainfo, name, type, [], spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(Member|Class): void} visitor
|
||||
*/
|
||||
visit(visitor) {
|
||||
visitor(this);
|
||||
if (this.type)
|
||||
this.type.visit(visitor);
|
||||
for (const arg of this.argsArray)
|
||||
arg.visit(visitor);
|
||||
for (const lang in this.langs.overrides || {})
|
||||
this.langs.overrides?.[lang].visit(visitor);
|
||||
}
|
||||
}
|
||||
|
||||
class Type {
|
||||
/**
|
||||
* @param {string} expression
|
||||
* @param {!Array<!Member>=} properties
|
||||
* @param {!Object<string, string>=} langAliases
|
||||
* @return {Type}
|
||||
*/
|
||||
static parse(expression, properties = [], langAliases = {}) {
|
||||
expression = expression.replace(/\\\(/g, '(').replace(/\\\)/g, ')');
|
||||
const type = Type.fromParsedType(parseTypeExpression(expression));
|
||||
type.expression = expression;
|
||||
if (type.name === 'number')
|
||||
throw new Error('Number types should be either int or float, not number in: ' + expression);
|
||||
const hasAliases = Object.keys(langAliases).length > 0;
|
||||
if (!properties.length && !hasAliases)
|
||||
return type;
|
||||
const types = [];
|
||||
type._collectAllTypes(types);
|
||||
let assignedToObject = false;
|
||||
for (const t of types) {
|
||||
if (t.name === 'Object') {
|
||||
if (properties.length)
|
||||
t.properties = properties;
|
||||
if (hasAliases)
|
||||
t.langAliases = { ...langAliases };
|
||||
assignedToObject = true;
|
||||
}
|
||||
}
|
||||
if (!assignedToObject) {
|
||||
if (properties.length)
|
||||
throw new Error('Nested properties given, but there are no objects in type expression: ' + expression);
|
||||
if (hasAliases)
|
||||
type.langAliases = { ...langAliases };
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ParsedType} parsedType
|
||||
* @return {Type}
|
||||
*/
|
||||
static fromParsedType(parsedType, inUnion = false) {
|
||||
if (!inUnion && !parsedType.unionName && isStringUnion(parsedType))
|
||||
throw new Error('Enum must have a name:\n' + JSON.stringify(parsedType, null, 2));
|
||||
|
||||
|
||||
if (!inUnion && (parsedType.union || parsedType.unionName)) {
|
||||
const type = new Type(parsedType.unionName || '');
|
||||
type.union = [];
|
||||
for (let /** @type {ParsedType | null} */ t = parsedType; t; t = t.union) {
|
||||
const nestedUnion = !!t.unionName && t !== parsedType;
|
||||
type.union.push(Type.fromParsedType(t, !nestedUnion));
|
||||
if (nestedUnion)
|
||||
break;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
if (parsedType.args || parsedType.retType) {
|
||||
const type = new Type('function');
|
||||
type.args = [];
|
||||
for (let t = parsedType.args; t; t = t.next)
|
||||
type.args.push(Type.fromParsedType(t));
|
||||
type.returnType = parsedType.retType ? Type.fromParsedType(parsedType.retType) : undefined;
|
||||
return type;
|
||||
}
|
||||
|
||||
if (parsedType.template) {
|
||||
const type = new Type(parsedType.name);
|
||||
type.templates = [];
|
||||
for (let /** @type {ParsedType | null} */ t = parsedType.template; t; t = t.next)
|
||||
type.templates.push(Type.fromParsedType(t));
|
||||
return type;
|
||||
}
|
||||
|
||||
return new Type(parsedType.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {!Array<!Member>=} properties
|
||||
*/
|
||||
constructor(name, properties) {
|
||||
this.name = name.replace(/^\[/, '').replace(/\]$/, '');
|
||||
/** @type {Member[] | undefined} */
|
||||
this.properties = this.name === 'Object' ? properties : undefined;
|
||||
/** @type {Type[] | undefined} */
|
||||
this.union = undefined;
|
||||
/** @type {Type[] | undefined} */
|
||||
this.args = undefined;
|
||||
/** @type {Type | undefined} */
|
||||
this.returnType = undefined;
|
||||
/** @type {Type[] | undefined} */
|
||||
this.templates = undefined;
|
||||
/** @type {string | undefined} */
|
||||
this.expression = undefined;
|
||||
/** @type {Object<string, string> | undefined} */
|
||||
this.langAliases = undefined;
|
||||
}
|
||||
|
||||
visit(visitor) {
|
||||
const types = [];
|
||||
this._collectAllTypes(types);
|
||||
for (const type of types) {
|
||||
for (const p of type.properties || [])
|
||||
p.visit(visitor);
|
||||
}
|
||||
}
|
||||
|
||||
clone() {
|
||||
const type = new Type(this.name, this.properties ? this.properties.map(prop => prop.clone()) : undefined);
|
||||
if (this.union)
|
||||
type.union = this.union.map(type => type.clone());
|
||||
if (this.args)
|
||||
type.args = this.args.map(type => type.clone());
|
||||
if (this.returnType)
|
||||
type.returnType = this.returnType.clone();
|
||||
if (this.templates)
|
||||
type.templates = this.templates.map(type => type.clone());
|
||||
type.expression = this.expression;
|
||||
if (this.langAliases)
|
||||
type.langAliases = { ...this.langAliases };
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Member[]}
|
||||
*/
|
||||
deepProperties() {
|
||||
const types = [];
|
||||
this._collectAllTypes(types);
|
||||
for (const type of types) {
|
||||
if (type.properties && type.properties.length)
|
||||
return type.properties;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} lang
|
||||
* @param {LanguageOptions=} options
|
||||
*/
|
||||
filterForLanguage(lang, options = {}) {
|
||||
if (!this.properties)
|
||||
return;
|
||||
const properties = [];
|
||||
for (const prop of this.properties) {
|
||||
if (prop.langs.only && !prop.langs.only.includes(lang))
|
||||
continue;
|
||||
prop.filterForLanguage(lang, options);
|
||||
properties.push(prop);
|
||||
}
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Type[]} result
|
||||
*/
|
||||
_collectAllTypes(result) {
|
||||
result.push(this);
|
||||
for (const t of this.union || [])
|
||||
t._collectAllTypes(result);
|
||||
for (const t of this.args || [])
|
||||
t._collectAllTypes(result);
|
||||
for (const t of this.templates || [])
|
||||
t._collectAllTypes(result);
|
||||
if (this.returnType)
|
||||
this.returnType._collectAllTypes(result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ParsedType | null} type
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isStringUnion(type) {
|
||||
while (type) {
|
||||
if (!type.name.startsWith('"') || !type.name.endsWith('"'))
|
||||
return false;
|
||||
type = type.union;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
* @returns {ParsedType}
|
||||
*/
|
||||
function parseTypeExpression(type) {
|
||||
type = type.trim();
|
||||
let name = type;
|
||||
let next = null;
|
||||
let template = null;
|
||||
let args = null;
|
||||
let retType = null;
|
||||
let firstTypeLength = type.length;
|
||||
|
||||
for (let i = 0; i < type.length; i++) {
|
||||
if (type[i] === '<') {
|
||||
name = type.substring(0, i);
|
||||
const matching = matchingBracket(type.substring(i), '<', '>');
|
||||
template = parseTypeExpression(type.substring(i + 1, i + matching - 1));
|
||||
firstTypeLength = i + matching;
|
||||
break;
|
||||
}
|
||||
if (type[i] === '(') {
|
||||
name = type.substring(0, i);
|
||||
const matching = matchingBracket(type.substring(i), '(', ')');
|
||||
const argsString = type.substring(i + 1, i + matching - 1);
|
||||
args = argsString ? parseTypeExpression(argsString) : null;
|
||||
i = i + matching;
|
||||
if (type[i] === ':') {
|
||||
retType = parseTypeExpression(type.substring(i + 1));
|
||||
next = retType.next;
|
||||
retType.next = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (type[i] === '|' || type[i] === ',') {
|
||||
name = type.substring(0, i);
|
||||
firstTypeLength = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let union = null;
|
||||
if (type[firstTypeLength] === '|')
|
||||
union = parseTypeExpression(type.substring(firstTypeLength + 1));
|
||||
else if (type[firstTypeLength] === ',')
|
||||
next = parseTypeExpression(type.substring(firstTypeLength + 1));
|
||||
|
||||
if (template && !template.unionName && isStringUnion(template)) {
|
||||
template.unionName = name;
|
||||
return template;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
args,
|
||||
retType,
|
||||
template,
|
||||
union,
|
||||
next
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @param {any} open
|
||||
* @param {any} close
|
||||
*/
|
||||
function matchingBracket(str, open, close) {
|
||||
let count = 1;
|
||||
let i = 1;
|
||||
for (; i < str.length && count; i++) {
|
||||
if (str[i] === open)
|
||||
count++;
|
||||
else if (str[i] === close)
|
||||
count--;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Class|Member|undefined} classOrMember
|
||||
* @param {string} text
|
||||
* @param {Map<string, Class>} classesMap
|
||||
* @param {Map<string, Member>} membersMap
|
||||
* @param {Renderer} linkRenderer
|
||||
*/
|
||||
function patchLinksInText(classOrMember, text, classesMap, membersMap, linkRenderer) {
|
||||
text = text.replace(/\[`(\w+): ([^\]]+)`\](?:\(([^)]*?)\))?/g, (match, p1, p2, href) => {
|
||||
if (['event', 'method', 'property'].includes(p1)) {
|
||||
const memberName = p1 + ': ' + p2;
|
||||
const member = membersMap.get(memberName);
|
||||
if (!member)
|
||||
throw new Error(`Undefined member reference: ${match}\n=========\n${text}`);
|
||||
return linkRenderer({ member, href }) || match;
|
||||
}
|
||||
if (p1 === 'param' || p1 === 'option') {
|
||||
let /** @type {string } */ name;
|
||||
let /** @type {Member} */ member;
|
||||
if (p2.includes('.')) {
|
||||
// fully-qualified name
|
||||
const [className, memberName, ...rest] = p2.split('.');
|
||||
const maybeMember = membersMap.get(`method: ${className}.${memberName}`);
|
||||
if (!maybeMember)
|
||||
throw new Error(`Undefined reference: ${match}\n=========\n${text}`);
|
||||
member = maybeMember;
|
||||
name = rest.join('.');
|
||||
} else {
|
||||
// non-fully-qualified param/option reference from the same method.
|
||||
if (!classOrMember || !(classOrMember instanceof Member)) {
|
||||
Error.stackTraceLimit = 100;
|
||||
throw new Error(`No parent method to find referenced ${match}\n=========\n${text}`);
|
||||
}
|
||||
const maybeMember = classOrMember.enclosingMethod;
|
||||
if (!maybeMember)
|
||||
throw new Error(`Undefined reference: ${match}\n=========\n${text}`);
|
||||
member = maybeMember;
|
||||
name = p2;
|
||||
}
|
||||
if (p1 === 'param') {
|
||||
const param = member.argsArray.find(a => a.name === name);
|
||||
if (!param)
|
||||
throw new Error(`Referenced parameter ${match} not found in the parent method ${member.name}\n=========\n${text}`);
|
||||
return linkRenderer({ member, param: { name, alias: param.alias }, href }) || match;
|
||||
} else {
|
||||
// p1 === 'option'
|
||||
const options = member.argsArray.find(a => a.name === 'options');
|
||||
const parts = name.split('.');
|
||||
const optionName = parts[0];
|
||||
const option = options?.type?.properties?.find(a => a.name === optionName);
|
||||
if (!option)
|
||||
throw new Error(`Referenced option ${match} not found in the parent method ${member.name}\n=========\n${text}`);
|
||||
parts[0] = option.alias;
|
||||
return linkRenderer({ member, option: { name: optionName, alias: parts.join('.') }, href }) || match;
|
||||
}
|
||||
}
|
||||
throw new Error(`Undefined link prefix, expected event|method|property|param|option, got: ` + match);
|
||||
});
|
||||
text = text.replace(/\[([\w]+)\](?:\(([^)]*?)\))?/g, (match, p1, href) => {
|
||||
const clazz = classesMap.get(p1);
|
||||
if (clazz)
|
||||
return linkRenderer({ clazz, href }) || match;
|
||||
return match;
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MarkdownNode[] | undefined} spec
|
||||
*/
|
||||
function generateSourceCodeComment(spec) {
|
||||
const comments = (spec || []).filter(n => !n.type.startsWith('h') && (n.type !== 'li' || n.liType !== 'default')).map(c => md.clone(c));
|
||||
md.visitAll(comments, node => {
|
||||
if (node.type === 'li' && node.liType === 'bullet')
|
||||
node.liType = 'default';
|
||||
if (node.type === 'code' && node.codeLang)
|
||||
node.codeLang = parseCodeLang(node.codeLang).highlighter;
|
||||
});
|
||||
// 5 is a typical member doc offset.
|
||||
return md.render(comments, { maxColumns: 120 - 5, omitLastCR: true, flattenText: true, noteMode: 'compact' });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Member} optionsArg
|
||||
* @param {LanguageOptions=} options
|
||||
*/
|
||||
function patchCSharpOptionOverloads(optionsArg, options = {}) {
|
||||
const props = optionsArg.type?.properties;
|
||||
if (!props)
|
||||
return;
|
||||
const propsToDelete = new Set();
|
||||
const propsToAdd = [];
|
||||
for (const prop of props) {
|
||||
const union = prop.type?.union;
|
||||
if (!union)
|
||||
continue;
|
||||
const isEnum = union[0].name.startsWith('"');
|
||||
const isNullable = union.length === 2 && union.some(type => type.name === 'null');
|
||||
if (isEnum || isNullable)
|
||||
continue;
|
||||
|
||||
const shortNotation = [];
|
||||
propsToDelete.add(prop);
|
||||
for (const type of union) {
|
||||
const suffix = csharpOptionOverloadSuffix(prop.name, type.name);
|
||||
if (options.csharpOptionOverloadsShortNotation) {
|
||||
if (type.name === 'string')
|
||||
shortNotation.push(prop.alias);
|
||||
else
|
||||
shortNotation.push(prop.alias + suffix);
|
||||
continue;
|
||||
}
|
||||
|
||||
const newProp = prop.clone();
|
||||
newProp.name = prop.name + suffix;
|
||||
newProp.alias = prop.alias + suffix;
|
||||
newProp.type = type;
|
||||
propsToAdd.push(newProp);
|
||||
|
||||
if (type.name === 'string') {
|
||||
const stringProp = prop.clone();
|
||||
stringProp.type = type;
|
||||
propsToAdd.push(stringProp);
|
||||
}
|
||||
}
|
||||
if (options.csharpOptionOverloadsShortNotation) {
|
||||
const newProp = prop.clone();
|
||||
newProp.name = prop.name;
|
||||
newProp.alias = shortNotation.join('|');
|
||||
propsToAdd.push(newProp);
|
||||
}
|
||||
}
|
||||
for (const prop of propsToDelete)
|
||||
props.splice(props.indexOf(prop), 1);
|
||||
props.push(...propsToAdd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} option
|
||||
* @param {string} type
|
||||
*/
|
||||
function csharpOptionOverloadSuffix(option, type) {
|
||||
switch (type) {
|
||||
case 'string': return 'String';
|
||||
case 'RegExp': return 'Regex';
|
||||
case 'function': return 'Func';
|
||||
case 'Buffer': return 'Byte';
|
||||
case 'Serializable': return 'Object';
|
||||
case 'int': return 'Int';
|
||||
case 'long': return 'Int64';
|
||||
case 'Date': return 'Date';
|
||||
}
|
||||
throw new Error(`CSharp option "${option}" has unsupported type overload "${type}"`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MarkdownNode[]} spec
|
||||
* @param {string} language
|
||||
* @param {CodeGroupTransformer} transformer
|
||||
* @returns {MarkdownNode[]}
|
||||
*/
|
||||
function processCodeGroups(spec, language, transformer) {
|
||||
/** @type {MarkdownNode[]} */
|
||||
const newSpec = [];
|
||||
for (let i = 0; i < spec.length; ++i) {
|
||||
/** @type {{value: string, groupId: string, spec: MarkdownNode}[]} */
|
||||
const tabs = [];
|
||||
for (;i < spec.length; i++) {
|
||||
const codeLang = spec[i].codeLang;
|
||||
if (!codeLang)
|
||||
break;
|
||||
let parsed;
|
||||
try {
|
||||
parsed = parseCodeLang(codeLang);
|
||||
} catch (e) {
|
||||
throw new Error(e.message + '\n while processing:\n' + md.render([spec[i]]));
|
||||
}
|
||||
if (!parsed.codeGroup)
|
||||
break;
|
||||
if (parsed.language && parsed.language !== language)
|
||||
continue;
|
||||
const [groupId, value] = parsed.codeGroup.split('-');
|
||||
const clone = md.clone(spec[i]);
|
||||
clone.codeLang = parsed.highlighter;
|
||||
tabs.push({ groupId, value, spec: clone });
|
||||
}
|
||||
if (tabs.length) {
|
||||
if (tabs.length === 1)
|
||||
throw new Error(`Lonely tab "${tabs[0].spec.codeLang}". Make sure there are at least two tabs in the group.\n` + md.render([tabs[0].spec]));
|
||||
|
||||
// Validate group consistency.
|
||||
const groupId = tabs[0].groupId;
|
||||
const values = new Set();
|
||||
for (const tab of tabs) {
|
||||
if (tab.groupId !== groupId)
|
||||
throw new Error('Mixed group ids: ' + md.render(spec));
|
||||
if (values.has(tab.value))
|
||||
throw new Error(`Duplicated tab "${tab.value}"\n` + md.render(tabs.map(tab => tab.spec)));
|
||||
values.add(tab.value);
|
||||
}
|
||||
|
||||
// Append transformed nodes.
|
||||
newSpec.push(...transformer(tabs));
|
||||
}
|
||||
if (i < spec.length)
|
||||
newSpec.push(spec[i]);
|
||||
}
|
||||
return newSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} codeLang
|
||||
* @return {{ highlighter: string, language: string|undefined, codeGroup: string|undefined}}
|
||||
*/
|
||||
function parseCodeLang(codeLang) {
|
||||
if (codeLang === 'python async')
|
||||
return { highlighter: 'py', codeGroup: 'python-async', language: 'python' };
|
||||
if (codeLang === 'python sync')
|
||||
return { highlighter: 'py', codeGroup: 'python-sync', language: 'python' };
|
||||
|
||||
const [highlighter] = codeLang.split(' ');
|
||||
if (!highlighter)
|
||||
throw new Error(`Cannot parse code block lang: "${codeLang}"`);
|
||||
|
||||
const languageMatch = codeLang.match(/ lang=([\w\d]+)/);
|
||||
let language = languageMatch ? languageMatch[1] : undefined;
|
||||
if (!language) {
|
||||
if (highlighter === 'ts')
|
||||
language = 'js';
|
||||
else if (highlighter === 'py')
|
||||
language = 'python';
|
||||
else if (['js', 'python', 'csharp', 'java'].includes(highlighter))
|
||||
language = highlighter;
|
||||
}
|
||||
|
||||
const tabMatch = codeLang.match(/ tab=([\w\d-]+)/);
|
||||
return { highlighter, language, codeGroup: tabMatch ? tabMatch[1] : '' };
|
||||
}
|
||||
|
||||
module.exports = { Documentation, Class, Member, Type, processCodeGroups, parseCodeLang };
|
||||
169
참고/playwright-main/utils/doclint/dotnetXmlDocumentation.js
Normal file
169
참고/playwright-main/utils/doclint/dotnetXmlDocumentation.js
Normal file
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* 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 Documentation = require('./documentation');
|
||||
const { visitAll, render } = require('../markdown');
|
||||
/**
|
||||
* @param {Documentation.MarkdownNode[]} nodes
|
||||
* @param {number} maxColumns
|
||||
*/
|
||||
function renderXmlDoc(nodes, maxColumns = 80, prefix = '/// ') {
|
||||
if (!nodes)
|
||||
return [];
|
||||
|
||||
const renderResult = _innerRenderNodes(nodes, maxColumns);
|
||||
|
||||
const doc = [];
|
||||
_wrapInNode('summary', renderResult.summary, doc);
|
||||
_wrapInNode('remarks', renderResult.remarks, doc);
|
||||
return doc.map(x => `${prefix}${x}`);
|
||||
}
|
||||
|
||||
function _innerRenderNodes(nodes, maxColumns = 80, wrapParagraphs = true) {
|
||||
const summary = [];
|
||||
const remarks = [];
|
||||
const handleListItem = (lastNode, node) => {
|
||||
if (node && node.type === 'li' && (!lastNode || lastNode.type !== 'li'))
|
||||
summary.push(`<list type="${node.liType}">`);
|
||||
else if (lastNode && lastNode.type === 'li' && (!node || node.type !== 'li'))
|
||||
summary.push('</list>');
|
||||
|
||||
};
|
||||
|
||||
let lastNode;
|
||||
visitAll(nodes, node => {
|
||||
// handle special cases first
|
||||
if (_nodeShouldBeIgnored(node))
|
||||
return;
|
||||
if (node.text && node.text.startsWith('extends: ')) {
|
||||
remarks.push('Inherits from ' + node.text.replace('extends: ', ''));
|
||||
return;
|
||||
}
|
||||
handleListItem(lastNode, node);
|
||||
if (node.type === 'text') {
|
||||
if (wrapParagraphs)
|
||||
_wrapInNode('para', _wrapAndEscape(node, maxColumns), summary);
|
||||
else
|
||||
summary.push(..._wrapAndEscape(node, maxColumns));
|
||||
} else if (node.type === 'code' && node.codeLang === 'csharp') {
|
||||
_wrapInNode('code', _wrapCode(node.lines), summary);
|
||||
} else if (node.type === 'li') {
|
||||
_wrapInNode('item><description', _wrapAndEscape(node, maxColumns), summary, '/description></item');
|
||||
} else if (node.type === 'note') {
|
||||
_wrapInNode('para', _wrapAndEscape({
|
||||
type: 'text',
|
||||
text: render(node.children ?? []).replaceAll('\n', '↵'),
|
||||
}, maxColumns), remarks);
|
||||
}
|
||||
lastNode = node;
|
||||
});
|
||||
handleListItem(lastNode, null);
|
||||
|
||||
return { summary, remarks };
|
||||
}
|
||||
|
||||
function _wrapCode(lines) {
|
||||
let i = 0;
|
||||
const out = [];
|
||||
for (let line of lines) {
|
||||
line = line.replace(/[&]/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
if (i < lines.length - 1)
|
||||
line = line + '<br/>';
|
||||
out.push(line);
|
||||
i++;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function _wrapInNode(tag, nodes, target, closingTag = null) {
|
||||
if (nodes.length === 0)
|
||||
return;
|
||||
|
||||
if (!closingTag)
|
||||
closingTag = `/${tag}`;
|
||||
|
||||
if (nodes.length === 1) {
|
||||
target.push(`<${tag}>${nodes[0]}<${closingTag}>`);
|
||||
return;
|
||||
}
|
||||
|
||||
target.push(`<${tag}>`);
|
||||
target.push(...nodes);
|
||||
target.push(`<${closingTag}>`);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Documentation.MarkdownNode} node
|
||||
*/
|
||||
function _wrapAndEscape(node, maxColumns = 0) {
|
||||
const lines = [];
|
||||
const pushLine = text => {
|
||||
if (text === '')
|
||||
return;
|
||||
text = text.trim();
|
||||
lines.push(text);
|
||||
};
|
||||
|
||||
|
||||
let text = (node.text || '').replace(/↵/g, ' ');
|
||||
text = text.replace(/\[([^\]]*)\]\((.*?)\)/g, (match, linkName, linkUrl) => {
|
||||
const isInternal = !linkUrl.startsWith('http://') && !linkUrl.startsWith('https://');
|
||||
if (isInternal)
|
||||
linkUrl = new URL(linkUrl.replace('.md', ''), 'https://playwright.dev/dotnet/docs/api/').toString();
|
||||
return `<a href="${linkUrl}">${linkName}</a>`;
|
||||
});
|
||||
text = text.replace(/(?<!`)\[(.*?)\]/g, (match, link) => `<see cref="${link}"/>`);
|
||||
text = text.replace(/`([^`]*)`/g, (match, code) => `<c>${code.replace(/</g, '<').replace(/>/g, '>')}</c>`);
|
||||
text = text.replace(/ITimeoutError/, 'TimeoutException');
|
||||
text = text.replace(/Promise/, 'Task');
|
||||
|
||||
const words = text.split(' ');
|
||||
let line = '';
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
line = line + ' ' + words[i];
|
||||
if (line.length >= maxColumns) {
|
||||
pushLine(line);
|
||||
line = '';
|
||||
}
|
||||
}
|
||||
|
||||
pushLine(line);
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Documentation.MarkdownNode} node
|
||||
*/
|
||||
function _nodeShouldBeIgnored(node) {
|
||||
if (!node
|
||||
|| (node.text === 'extends: [EventEmitter]'))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Documentation.MarkdownNode[]} nodes
|
||||
*/
|
||||
function renderTextOnly(nodes, maxColumns = 80) {
|
||||
const result = _innerRenderNodes(nodes, maxColumns, false);
|
||||
return result.summary;
|
||||
}
|
||||
|
||||
module.exports = { renderXmlDoc, renderTextOnly };
|
||||
135
참고/playwright-main/utils/doclint/generateApiJson.js
Normal file
135
참고/playwright-main/utils/doclint/generateApiJson.js
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* 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 path = require('path');
|
||||
const { parseApi } = require('./api_parser');
|
||||
const PROJECT_DIR = path.join(__dirname, '..', '..');
|
||||
|
||||
{
|
||||
const documentation = parseApi(path.join(PROJECT_DIR, 'docs', 'src', 'api'));
|
||||
documentation.setLinkRenderer(item => {
|
||||
const { clazz, param, option } = item;
|
||||
if (param)
|
||||
return `\`${param.alias}\``;
|
||||
if (option)
|
||||
return `\`${option.alias}\``;
|
||||
if (clazz)
|
||||
return `\`${clazz.name}\``;
|
||||
});
|
||||
documentation.generateSourceCodeComments();
|
||||
const result = serialize(documentation);
|
||||
console.log(JSON.stringify(result));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('./documentation').Documentation} documentation
|
||||
*/
|
||||
function serialize(documentation) {
|
||||
return documentation.classesArray.map(serializeClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('./documentation').Class} clazz
|
||||
*/
|
||||
function serializeClass(clazz) {
|
||||
const result = { name: clazz.name, spec: clazz.spec };
|
||||
if (clazz.extends)
|
||||
result.extends = clazz.extends;
|
||||
serializeLangs(clazz, result);
|
||||
if (clazz.comment)
|
||||
result.comment = clazz.comment;
|
||||
if (clazz.since)
|
||||
result.since = clazz.since;
|
||||
result.members = clazz.membersArray.map(serializeMember);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('./documentation').Member} member
|
||||
*/
|
||||
function serializeMember(member) {
|
||||
const result = /** @type {any} */ ({ ...member });
|
||||
sanitize(result);
|
||||
result.args = member.argsArray.map(serializeProperty);
|
||||
if (member.type)
|
||||
result.type = serializeType(member.type);
|
||||
serializeLangs(member, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('./documentation').Member | import('./documentation').Class} from
|
||||
* @param {any} to
|
||||
*/
|
||||
function serializeLangs(from, to) {
|
||||
if (!from.langs)
|
||||
return;
|
||||
to.langs = { ...from.langs };
|
||||
sanitize(to.langs);
|
||||
if (from.langs.overrides) {
|
||||
for (const key in from.langs.overrides)
|
||||
to.langs.overrides[key] = serializeMember(from.langs.overrides[key]);
|
||||
}
|
||||
if (from.langs.types) {
|
||||
for (const key in from.langs.types)
|
||||
to.langs.types[key] = serializeType(from.langs.types[key]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('./documentation').Member} arg
|
||||
*/
|
||||
function serializeProperty(arg) {
|
||||
const result = { ...arg };
|
||||
sanitize(result);
|
||||
if (arg.type)
|
||||
result.type = serializeType(arg.type);
|
||||
serializeLangs(arg, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} result
|
||||
*/
|
||||
function sanitize(result) {
|
||||
delete result.args;
|
||||
delete result.argsArray;
|
||||
delete result.clazz;
|
||||
delete result.enclosingMethod;
|
||||
delete result.parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('./documentation').Type} type
|
||||
*/
|
||||
function serializeType(type) {
|
||||
/** @type {any} */
|
||||
const result = { ...type };
|
||||
sanitize(result);
|
||||
if (type.properties)
|
||||
result.properties = type.properties.map(serializeProperty);
|
||||
if (type.union)
|
||||
result.union = type.union.map(type => serializeType(type));
|
||||
if (type.templates)
|
||||
result.templates = type.templates.map(type => serializeType(type));
|
||||
if (type.args)
|
||||
result.args = type.args.map(type => serializeType(type));
|
||||
if (type.returnType)
|
||||
result.returnType = serializeType(type.returnType);
|
||||
return result;
|
||||
}
|
||||
891
참고/playwright-main/utils/doclint/generateDotnetApi.js
Normal file
891
참고/playwright-main/utils/doclint/generateDotnetApi.js
Normal file
@@ -0,0 +1,891 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* 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 path = require('path');
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const Documentation = require('./documentation');
|
||||
const XmlDoc = require('./dotnetXmlDocumentation');
|
||||
const PROJECT_DIR = path.join(__dirname, '..', '..');
|
||||
const fs = require('fs');
|
||||
const { parseApi } = require('./api_parser');
|
||||
const { Type } = require('./documentation');
|
||||
const { EOL } = require('os');
|
||||
|
||||
const maxDocumentationColumnWidth = 80;
|
||||
Error.stackTraceLimit = 100;
|
||||
|
||||
/** @type {Map<string, Documentation.Type>} */
|
||||
const modelTypes = new Map(); // this will hold types that we discover, because of .NET specifics, like results
|
||||
/** @type {Map<string, string>} */
|
||||
const documentedResults = new Map(); // will hold documentation for new types
|
||||
/** @type {Map<string, string[]>} */
|
||||
const enumTypes = new Map();
|
||||
/** @type {Map<string, Documentation.Type>} */
|
||||
const optionTypes = new Map();
|
||||
const customTypeNames = new Map([
|
||||
['domcontentloaded', 'DOMContentLoaded'],
|
||||
['networkidle', 'NetworkIdle'],
|
||||
]);
|
||||
|
||||
const outputDir = process.argv[2] || path.join(__dirname, 'generate_types', 'csharp');
|
||||
const apiDir = path.join(outputDir, 'API', 'Generated');
|
||||
const optionsDir = path.join(outputDir, 'API', 'Generated', 'Options');
|
||||
const enumsDir = path.join(outputDir, 'API', 'Generated', 'Enums');
|
||||
const typesDir = path.join(outputDir, 'API', 'Generated', 'Types');
|
||||
|
||||
for (const dir of [apiDir, optionsDir, enumsDir, typesDir])
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
const documentation = parseApi(path.join(PROJECT_DIR, 'docs', 'src', 'api'));
|
||||
documentation.filterForLanguage('csharp');
|
||||
|
||||
documentation.setLinkRenderer(item => {
|
||||
const asyncSuffix = item.member && item.member.async ? 'Async' : '';
|
||||
if (item.clazz)
|
||||
return `<see cref="I${toTitleCase(item.clazz.name)}"/>`;
|
||||
else if (item.member)
|
||||
return `<see cref="I${toTitleCase(item.member.clazz.name)}.${toMemberName(item.member)}${asyncSuffix}"/>`;
|
||||
else if (item.option)
|
||||
return `<paramref name="${item.option.name}"/>`;
|
||||
else if (item.param)
|
||||
return `<paramref name="${item.param.name}"/>`;
|
||||
else
|
||||
throw new Error('Unknown link format.');
|
||||
});
|
||||
|
||||
// get the template for a class
|
||||
const template = fs.readFileSync(path.join(__dirname, 'templates', 'interface.cs'), 'utf-8');
|
||||
|
||||
// map the name to a C# friendly one (we prepend an I to denote an interface)
|
||||
const classNameMap = new Map(documentation.classesArray.map(x => [x.name, `I${toTitleCase(x.name)}`]));
|
||||
|
||||
// map some types that we know of
|
||||
classNameMap.set('Error', 'Exception');
|
||||
classNameMap.set('TimeoutError', 'TimeoutException');
|
||||
classNameMap.set('EvaluationArgument', 'object');
|
||||
classNameMap.set('boolean', 'bool');
|
||||
classNameMap.set('any', 'object');
|
||||
classNameMap.set('Buffer', 'byte[]');
|
||||
classNameMap.set('path', 'string');
|
||||
classNameMap.set('Date', 'DateTime');
|
||||
classNameMap.set('URL', 'string');
|
||||
classNameMap.set('RegExp', 'Regex');
|
||||
classNameMap.set('Readable', 'Stream');
|
||||
classNameMap.set('Disposable', 'IAsyncDisposable');
|
||||
classNameMap.set('Promise', 'Task');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} kind
|
||||
* @param {string} name
|
||||
* @param {Documentation.MarkdownNode[]|null} spec
|
||||
* @param {string[]} body
|
||||
* @param {string} folder
|
||||
* @param {string|null} extendsName
|
||||
*/
|
||||
function writeFile(kind, name, spec, body, folder, extendsName = null) {
|
||||
const out = [];
|
||||
// console.log(`Generating ${name}`);
|
||||
|
||||
if (spec) {
|
||||
out.push(...XmlDoc.renderXmlDoc(spec, maxDocumentationColumnWidth));
|
||||
} else {
|
||||
const ownDocumentation = documentedResults.get(name);
|
||||
if (ownDocumentation) {
|
||||
out.push('/// <summary>');
|
||||
out.push(`/// ${ownDocumentation}`);
|
||||
out.push('/// </summary>');
|
||||
}
|
||||
}
|
||||
|
||||
if (extendsName === 'IEventEmitter')
|
||||
extendsName = null;
|
||||
|
||||
if (body[0] === '')
|
||||
body = body.slice(1);
|
||||
|
||||
out.push(`${kind} ${name}${extendsName ? ` : ${extendsName}` : ''}`);
|
||||
out.push('{');
|
||||
out.push(...body);
|
||||
out.push('}');
|
||||
|
||||
const content = template.replace('[CONTENT]', out.join(EOL));
|
||||
fs.writeFileSync(path.join(folder, name + '.cs'), content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Documentation.Class} clazz
|
||||
*/
|
||||
function renderClass(clazz) {
|
||||
const name = classNameMap.get(clazz.name);
|
||||
if (name === 'TimeoutException')
|
||||
return;
|
||||
if (name === 'IAsyncDisposable')
|
||||
return;
|
||||
|
||||
const body = [];
|
||||
for (const member of clazz.membersArray) {
|
||||
// Classes inherit it from IAsyncDisposable
|
||||
if (member.name === 'dispose')
|
||||
continue;
|
||||
if (member.alias.startsWith('RunAnd'))
|
||||
renderMember(member, clazz, { trimRunAndPrefix: true }, body);
|
||||
renderMember(member, clazz, {}, body);
|
||||
}
|
||||
|
||||
/** @type {Documentation.MarkdownNode[]} */
|
||||
const spec = [];
|
||||
if (clazz.deprecated)
|
||||
spec.push({ type: 'text', text: '**DEPRECATED** ' + clazz.deprecated });
|
||||
if (clazz.discouraged)
|
||||
spec.push({ type: 'text', text: clazz.discouraged });
|
||||
if (clazz.spec)
|
||||
spec.push(...clazz.spec);
|
||||
|
||||
writeFile(
|
||||
'public partial interface',
|
||||
name,
|
||||
spec,
|
||||
body,
|
||||
apiDir,
|
||||
clazz.extends ? `I${toTitleCase(clazz.extends)}` : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {Documentation.Type} type
|
||||
*/
|
||||
function renderModelType(name, type) {
|
||||
const body = [];
|
||||
// TODO: consider how this could be merged with the `translateType` check
|
||||
if (type.union
|
||||
&& type.union[0].name === 'null'
|
||||
&& type.union.length === 2)
|
||||
type = type.union[1];
|
||||
|
||||
|
||||
if (type.name === 'Array') {
|
||||
throw new Error('Array at this stage is unexpected.');
|
||||
} else if (type.properties) {
|
||||
for (const member of type.properties) {
|
||||
const fakeType = new Type(name, null);
|
||||
renderMember(member, fakeType, {}, body);
|
||||
}
|
||||
} else {
|
||||
console.log(type);
|
||||
throw new Error(`Not sure what to do in this case.`);
|
||||
}
|
||||
writeFile('public partial class', name, null, body, typesDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {string[]} literals
|
||||
*/
|
||||
function renderEnum(name, literals) {
|
||||
const body = [];
|
||||
for (let literal of literals) {
|
||||
// strip out the quotes
|
||||
literal = literal.replace(/[\"]/g, ``);
|
||||
const escapedName = literal.replace(/[-]/g, ' ')
|
||||
.split(' ')
|
||||
.map(word => customTypeNames.get(word) || word[0].toUpperCase() + word.substring(1)).join('');
|
||||
|
||||
body.push(`[EnumMember(Value = "${literal}")]`);
|
||||
body.push(`${escapedName},`);
|
||||
}
|
||||
writeFile('public enum', name, null, body, enumsDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {Documentation.Type} type
|
||||
*/
|
||||
function renderOptionType(name, type) {
|
||||
const body = [];
|
||||
|
||||
renderConstructors(name, type, body);
|
||||
|
||||
for (const member of type.properties)
|
||||
renderMember(member, member.type, {}, body);
|
||||
writeFile('public class', name, null, body, optionsDir);
|
||||
}
|
||||
|
||||
for (const element of documentation.classesArray)
|
||||
renderClass(element);
|
||||
|
||||
|
||||
for (const [name, type] of optionTypes)
|
||||
renderOptionType(name, type);
|
||||
|
||||
for (const [name, type] of modelTypes)
|
||||
renderModelType(name, type);
|
||||
|
||||
for (const [name, literals] of enumTypes)
|
||||
renderEnum(name, literals);
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
function toArgumentName(name) {
|
||||
return name === 'event' ? `@${name}` : name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Documentation.Member} member
|
||||
*/
|
||||
function toMemberName(member, makeAsync = false) {
|
||||
const assumedName = toTitleCase(member.alias || member.name);
|
||||
if (member.kind === 'interface')
|
||||
return `I${assumedName}`;
|
||||
if (makeAsync && member.async)
|
||||
return assumedName + 'Async';
|
||||
if (!makeAsync && assumedName.endsWith('Async'))
|
||||
return assumedName.substring(0, assumedName.length - 'Async'.length);
|
||||
return assumedName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {string}
|
||||
*/
|
||||
function toTitleCase(name) {
|
||||
return name.charAt(0).toUpperCase() + name.substring(1);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {Documentation.Type} type
|
||||
* @param {string[]} out
|
||||
*/
|
||||
function renderConstructors(name, type, out) {
|
||||
out.push(`public ${name}(){}`);
|
||||
out.push('');
|
||||
out.push(`public ${name}(${name} clone) {`);
|
||||
out.push(`if(clone == null) return;`);
|
||||
|
||||
type.properties.forEach(p => {
|
||||
const propType = translateType(p.type, type, t => generateNameDefault(p, name, t, type));
|
||||
const propName = toMemberName(p);
|
||||
const overloads = getPropertyOverloads(propType, p, propName, p.type);
|
||||
for (const { name } of overloads)
|
||||
out.push(`${name} = clone.${name};`);
|
||||
});
|
||||
out.push(`}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Documentation.Member} member
|
||||
* @param {string[]} out
|
||||
*/
|
||||
function renderMemberDoc(member, out) {
|
||||
/** @type {Documentation.MarkdownNode[]} */
|
||||
const nodes = [];
|
||||
if (member.deprecated)
|
||||
nodes.push({ type: 'text', text: '**DEPRECATED** ' + member.deprecated });
|
||||
if (member.discouraged)
|
||||
nodes.push({ type: 'text', text: member.discouraged });
|
||||
if (member.spec)
|
||||
nodes.push(...member.spec);
|
||||
out.push(...XmlDoc.renderXmlDoc(nodes, maxDocumentationColumnWidth));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Documentation.Member} member
|
||||
* @param {Documentation.Class|Documentation.Type} parent
|
||||
* @param {{nojson?: boolean, trimRunAndPrefix?: boolean}} options
|
||||
* @param {string[]} out
|
||||
*/
|
||||
function renderMember(member, parent, options, out) {
|
||||
const name = toMemberName(member);
|
||||
if (member.kind === 'method') {
|
||||
renderMethod(member, parent, name, { trimRunAndPrefix: options.trimRunAndPrefix }, out);
|
||||
return;
|
||||
}
|
||||
|
||||
let type = translateType(member.type, parent, t => generateNameDefault(member, name, t, parent));
|
||||
if (member.kind === 'event') {
|
||||
if (!member.type)
|
||||
throw new Error(`No Event Type for ${name} in ${parent.name}`);
|
||||
out.push('');
|
||||
renderMemberDoc(member, out);
|
||||
if (member.deprecated)
|
||||
out.push(`[System.Obsolete]`);
|
||||
if (type === 'void')
|
||||
out.push(`event EventHandler ${name};`);
|
||||
else
|
||||
out.push(`event EventHandler<${type}> ${name};`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (member.kind === 'property') {
|
||||
if (parent && member && member.name === 'children') { // this is a special hack for Accessibility
|
||||
console.warn(`children property found in ${parent.name}, assuming array.`);
|
||||
type = `IEnumerable<${parent.name}>`;
|
||||
}
|
||||
const overloads = getPropertyOverloads(type, member, name, parent);
|
||||
for (const overload of overloads) {
|
||||
const { name, jsonName } = overload;
|
||||
let { type } = overload;
|
||||
out.push('');
|
||||
renderMemberDoc(member, out);
|
||||
if (!member.clazz)
|
||||
out.push(`${member.required ? '[Required]\n' : ''}[JsonPropertyName("${jsonName}")]`);
|
||||
if (member.deprecated)
|
||||
out.push(`[System.Obsolete]`);
|
||||
if (!type.endsWith('?') && !member.required)
|
||||
type = `${type}?`;
|
||||
const requiredSuffix = type.endsWith('?') ? '' : ' = default!;';
|
||||
if (member.clazz)
|
||||
out.push(`public ${type} ${name} { get; }`);
|
||||
else
|
||||
out.push(`public ${type} ${name} { get; set; }${requiredSuffix}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
throw new Error(`Problem rendering a member: ${type} - ${name} (${member.kind})`);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} type
|
||||
* @param {Documentation.Member} member
|
||||
* @param {string} name
|
||||
* @param {Documentation.Class|Documentation.Type} parent
|
||||
* @returns [{ type: string; name: string; jsonName: string; }]
|
||||
*/
|
||||
function getPropertyOverloads(type, member, name, parent) {
|
||||
const overloads = [];
|
||||
if (type) {
|
||||
let jsonName = member.name;
|
||||
if (member.type.expression === '[string]|[float]')
|
||||
jsonName = `${member.name}String`;
|
||||
overloads.push({ type, name, jsonName });
|
||||
}
|
||||
return overloads;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Documentation.Member} member
|
||||
* @param {string} name
|
||||
* @param {Documentation.Type} t
|
||||
* @param {*} parent
|
||||
*/
|
||||
function generateNameDefault(member, name, t, parent) {
|
||||
const structName = t.langAliases?.csharp || t.langAliases?.default;
|
||||
if (structName) {
|
||||
registerModelType(structName, t);
|
||||
return structName;
|
||||
}
|
||||
|
||||
if (!t.properties
|
||||
&& !t.templates
|
||||
&& !t.union
|
||||
&& t.expression === '[Object]')
|
||||
return 'object';
|
||||
|
||||
// we'd get this call for enums, primarily
|
||||
const enumName = generateEnumNameIfApplicable(t);
|
||||
if (!enumName && member) {
|
||||
if (member.kind === 'method' || member.kind === 'property') {
|
||||
const names = [
|
||||
parent.alias || parent.name,
|
||||
toTitleCase(member.alias || member.name),
|
||||
toTitleCase(name),
|
||||
];
|
||||
if (names[2] === names[1])
|
||||
names.pop(); // get rid of duplicates, cheaply
|
||||
let attemptedName = names.pop();
|
||||
const typesDiffer = function(/** @type {Documentation.Type} */ left, /** @type {Documentation.Type} */ right) {
|
||||
if (left.expression && right.expression)
|
||||
return left.expression !== right.expression;
|
||||
const toExpression = (/** @type {Documentation.Member} */ t) => t.name + t.type?.expression;
|
||||
const leftOverRightProperties = new Set(left.properties?.map(toExpression) ?? []);
|
||||
for (const prop of right.properties ?? []) {
|
||||
const expression = toExpression(prop);
|
||||
if (!leftOverRightProperties.has(expression))
|
||||
return true;
|
||||
leftOverRightProperties.delete(expression);
|
||||
}
|
||||
return leftOverRightProperties.size > 0;
|
||||
};
|
||||
while (true) {
|
||||
// crude attempt at removing plurality
|
||||
if (attemptedName.endsWith('s')
|
||||
&& !['properties', 'httpcredentials'].includes(attemptedName.toLowerCase()))
|
||||
attemptedName = attemptedName.substring(0, attemptedName.length - 1);
|
||||
|
||||
const probableType = modelTypes.get(attemptedName);
|
||||
if ((probableType && typesDiffer(t, probableType))
|
||||
|| (['Value'].includes(attemptedName))) {
|
||||
if (!names.length)
|
||||
throw new Error(`Ran out of possible names: ${attemptedName}`);
|
||||
attemptedName = `${names.pop()}${attemptedName}`;
|
||||
continue;
|
||||
} else {
|
||||
registerModelType(attemptedName, t);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return attemptedName;
|
||||
}
|
||||
|
||||
if (member.kind === 'event')
|
||||
return `${name}Payload`;
|
||||
|
||||
}
|
||||
|
||||
return enumName || t.name;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Documentation.Type} type
|
||||
* @returns
|
||||
*/
|
||||
function generateEnumNameIfApplicable(type) {
|
||||
if (!type.union)
|
||||
return null;
|
||||
|
||||
const potentialValues = type.union.filter(u => u.name.startsWith('"'));
|
||||
if ((potentialValues.length !== type.union.length)
|
||||
&& !(type.union[0].name === 'null' && potentialValues.length === type.union.length - 1))
|
||||
return null; // this isn't an enum, so we don't care, we let the caller generate the name
|
||||
|
||||
return type.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendering a method is so _special_, with so many weird edge cases, that it
|
||||
* makes sense to put it separate from the other logic.
|
||||
* @param {Documentation.Member} member
|
||||
* @param {Documentation.Class | Documentation.Type} parent
|
||||
* @param {string} name
|
||||
* @param {{
|
||||
* nodocs?: boolean,
|
||||
* abstract?: boolean,
|
||||
* public?: boolean,
|
||||
* trimRunAndPrefix?: boolean,
|
||||
* }} options
|
||||
* @param {string[]} out
|
||||
*/
|
||||
function renderMethod(member, parent, name, options, out) {
|
||||
out.push('');
|
||||
|
||||
if (options.trimRunAndPrefix)
|
||||
name = name.substring('RunAnd'.length);
|
||||
|
||||
/** @type {Map<string, string[]>} */
|
||||
const paramDocs = new Map();
|
||||
const addParamsDoc = (paramName, docs) => {
|
||||
if (paramName.startsWith('@'))
|
||||
paramName = paramName.substring(1);
|
||||
if (paramDocs.get(paramName) && paramDocs.get(paramName) !== docs)
|
||||
throw new Error(`Parameter ${paramName} already exists in the docs.`);
|
||||
paramDocs.set(paramName, docs);
|
||||
};
|
||||
|
||||
let type = translateType(member.type, parent, t => generateNameDefault(member, name, t, parent), false, true);
|
||||
|
||||
// TODO: this is something that will probably go into the docs
|
||||
// translate simple getters into read-only properties, and simple
|
||||
// set-only methods to settable properties
|
||||
if (member.args.size === 0
|
||||
&& type !== 'void'
|
||||
&& !name.startsWith('Get')
|
||||
&& name !== 'CreateFormData'
|
||||
&& !name.startsWith('PostDataJSON')
|
||||
&& !name.startsWith('As')
|
||||
&& name !== 'ConnectToServer') {
|
||||
if (!member.async) {
|
||||
if (member.spec && !options.nodocs)
|
||||
out.push(...XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth));
|
||||
if (member.deprecated)
|
||||
out.push(`[System.Obsolete]`);
|
||||
out.push(`${type} ${name} { get; }`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// HACK: special case for generics handling!
|
||||
if (type === 'T')
|
||||
name = `${name}<T>`;
|
||||
|
||||
|
||||
// adjust the return type for async methods
|
||||
if (member.async) {
|
||||
if (type === 'void')
|
||||
type = `Task`;
|
||||
else
|
||||
type = `Task<${type}>`;
|
||||
}
|
||||
|
||||
// render args
|
||||
/** @type {string[]} */
|
||||
const args = [];
|
||||
/** @type {string[]} */
|
||||
const explodedArgs = [];
|
||||
/** @type {Map<string, string>} */
|
||||
const argTypeMap = new Map([]);
|
||||
/**
|
||||
*
|
||||
* @param {string} innerArgType
|
||||
* @param {string} innerArgName
|
||||
* @param {Documentation.Member} argument
|
||||
* @param {boolean} isExploded
|
||||
*/
|
||||
function pushArg(innerArgType, innerArgName, argument, isExploded = false) {
|
||||
if (innerArgType === 'null')
|
||||
return;
|
||||
const requiredPrefix = (argument.required || isExploded) ? '' : '?';
|
||||
const requiredSuffix = (argument.required || isExploded) ? '' : ' = default';
|
||||
const push = `${innerArgType}${requiredPrefix} ${innerArgName}${requiredSuffix}`;
|
||||
if (isExploded)
|
||||
explodedArgs.push(push);
|
||||
else
|
||||
args.push(push);
|
||||
argTypeMap.set(push, innerArgName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Documentation.Member} arg
|
||||
*/
|
||||
function processArg(arg) {
|
||||
if (options.trimRunAndPrefix && arg.name === 'action')
|
||||
return;
|
||||
|
||||
if (arg.name === 'options') {
|
||||
const optionsType = rewriteSuggestedOptionsName(member.clazz.name + name.replace('<T>', '') + 'Options');
|
||||
if (!optionTypes.has(optionsType) || arg.type.properties.length > optionTypes.get(optionsType).properties.length)
|
||||
optionTypes.set(optionsType, arg.type);
|
||||
args.push(`${optionsType}? options = default`);
|
||||
argTypeMap.set(`${optionsType}? options = default`, 'options');
|
||||
addParamsDoc('options', ['Call options']);
|
||||
return;
|
||||
}
|
||||
|
||||
if (arg.type.expression === '[string]|[path]') {
|
||||
const argName = toArgumentName(arg.name);
|
||||
pushArg('string?', `${argName} = default`, arg);
|
||||
pushArg('string?', `${argName}Path = default`, arg);
|
||||
if (arg.spec) {
|
||||
addParamsDoc(argName, XmlDoc.renderTextOnly(arg.spec, maxDocumentationColumnWidth));
|
||||
addParamsDoc(`${argName}Path`, [`Instead of specifying <paramref name="${argName}"/>, gives the file name to load from.`]);
|
||||
}
|
||||
return;
|
||||
} else if (arg.type.expression === '[boolean]|[Array]<[string]>') {
|
||||
// HACK: this hurts my brain too
|
||||
// we split this into two args, one boolean, with the logical name
|
||||
const argName = toArgumentName(arg.name);
|
||||
const leftArgType = translateType(arg.type.union[0], parent, t => { throw new Error('Not supported'); });
|
||||
const rightArgType = translateType(arg.type.union[1], parent, t => { throw new Error('Not supported'); });
|
||||
|
||||
pushArg(leftArgType, argName, arg);
|
||||
pushArg(rightArgType, `${argName}Values`, arg);
|
||||
|
||||
addParamsDoc(argName, XmlDoc.renderTextOnly(arg.spec, maxDocumentationColumnWidth));
|
||||
addParamsDoc(`${argName}Values`, [`The values to take into account when <paramref name="${argName}"/> is <code>true</code>.`]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const argName = toArgumentName(arg.alias || arg.name);
|
||||
const argType = translateType(arg.type, parent, t => generateNameDefault(member, argName, t, parent));
|
||||
|
||||
if (argType === null && arg.type.union) {
|
||||
// we might have to split this into multiple arguments
|
||||
const translatedArguments = arg.type.union.map(t => translateType(t, parent, x => generateNameDefault(member, argName, x, parent)));
|
||||
if (translatedArguments.includes(null))
|
||||
throw new Error('Unexpected null in translated argument types. Aborting.');
|
||||
|
||||
const argDocumentation = XmlDoc.renderTextOnly(arg.spec, maxDocumentationColumnWidth);
|
||||
for (const newArg of translatedArguments) {
|
||||
pushArg(newArg, argName, arg, true); // push the exploded arg
|
||||
addParamsDoc(argName, argDocumentation);
|
||||
}
|
||||
args.push(arg.required ? 'EXPLODED_ARG' : 'OPTIONAL_EXPLODED_ARG');
|
||||
return;
|
||||
}
|
||||
|
||||
addParamsDoc(argName, XmlDoc.renderTextOnly(arg.spec, maxDocumentationColumnWidth));
|
||||
|
||||
if (argName === 'timeout' && argType === 'decimal') {
|
||||
args.push(`int timeout = 0`); // a special argument, we ignore our convention
|
||||
return;
|
||||
}
|
||||
|
||||
pushArg(argType, argName, arg);
|
||||
}
|
||||
|
||||
let modifiers = '';
|
||||
if (options.abstract)
|
||||
modifiers = 'protected abstract ';
|
||||
if (options.public)
|
||||
modifiers = 'public ';
|
||||
|
||||
member.argsArray
|
||||
.sort((a, b) => b.alias === 'options' ? -1 : 0) // move options to the back to the arguments list
|
||||
.forEach(processArg);
|
||||
|
||||
if (!explodedArgs.length) {
|
||||
if (!options.nodocs) {
|
||||
renderMemberDoc(member, out);
|
||||
paramDocs.forEach((value, i) => printArgDoc(i, value, out));
|
||||
}
|
||||
if (member.deprecated)
|
||||
out.push(`[System.Obsolete]`);
|
||||
out.push(`${modifiers}${type} ${toAsync(name, member.async)}(${args.join(', ')});`);
|
||||
} else {
|
||||
let containsOptionalExplodedArgs = false;
|
||||
explodedArgs.forEach((explodedArg, argIndex) => {
|
||||
if (!options.nodocs)
|
||||
renderMemberDoc(member, out);
|
||||
const overloadedArgs = [];
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (arg === 'EXPLODED_ARG' || arg === 'OPTIONAL_EXPLODED_ARG') {
|
||||
containsOptionalExplodedArgs = arg === 'OPTIONAL_EXPLODED_ARG';
|
||||
const argType = argTypeMap.get(explodedArg);
|
||||
if (!options.nodocs)
|
||||
printArgDoc(argType, paramDocs.get(argType), out);
|
||||
overloadedArgs.push(explodedArg);
|
||||
} else {
|
||||
const argType = argTypeMap.get(arg);
|
||||
if (!options.nodocs)
|
||||
printArgDoc(argType, paramDocs.get(argType), out);
|
||||
overloadedArgs.push(arg);
|
||||
}
|
||||
}
|
||||
out.push(`${modifiers}${type} ${toAsync(name, member.async)}(${overloadedArgs.join(', ')});`);
|
||||
if (argIndex < explodedArgs.length - 1)
|
||||
out.push(''); // output a special blank line
|
||||
});
|
||||
|
||||
// If the exploded union arguments are optional, we also output a special
|
||||
// signature, to help prevent compilation errors with ambiguous overloads.
|
||||
// That particular overload only contains the required arguments, or rather
|
||||
// contains all the arguments *except* the exploded ones.
|
||||
if (containsOptionalExplodedArgs) {
|
||||
const filteredArgs = args.filter(x => x !== 'OPTIONAL_EXPLODED_ARG');
|
||||
if (!options.nodocs)
|
||||
renderMemberDoc(member, out);
|
||||
filteredArgs.forEach(arg => {
|
||||
if (arg === 'EXPLODED_ARG')
|
||||
throw new Error(`Unsupported required union arg combined an optional union inside ${member.name}`);
|
||||
const argType = argTypeMap.get(arg);
|
||||
if (!options.nodocs)
|
||||
printArgDoc(argType, paramDocs.get(argType), out);
|
||||
});
|
||||
out.push(`${type} ${name}(${filteredArgs.join(', ')});`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Documentation.Type} type
|
||||
* @param {Documentation.Class|Documentation.Type} parent
|
||||
* @param {function(Documentation.Type): string} generateNameCallback
|
||||
* @param {boolean=} optional
|
||||
* @returns {string}
|
||||
*/
|
||||
function translateType(type, parent, generateNameCallback = t => t.name, optional = false, isReturnType = false) {
|
||||
// a few special cases we can fix automatically
|
||||
if (type.expression === '[null]|[Error]')
|
||||
return 'void';
|
||||
|
||||
if (type.name === 'Promise' && type.templates?.[0].name === 'any')
|
||||
return 'Task';
|
||||
|
||||
if (type.union) {
|
||||
if (type.union[0].name === 'null' && type.union.length === 2)
|
||||
return translateType(type.union[1], parent, generateNameCallback, true, isReturnType);
|
||||
|
||||
if (type.expression === '[string]|[Buffer]')
|
||||
return `byte[]`; // TODO: make sure we implement extension methods for this!
|
||||
if (type.expression === '[string]|[float]' || type.expression === '[string]|[float]|[boolean]') {
|
||||
console.warn(`${type.name} should be a 'string', but was a ${type.expression}`);
|
||||
return `string`;
|
||||
}
|
||||
if (type.expression === '[float]|"raf"')
|
||||
return `Polling`; // hardcoded because there's no other way to denote this
|
||||
|
||||
// Regular primitive enums are named in the markdown.
|
||||
if (type.name) {
|
||||
enumTypes.set(type.name, type.union.map(t => t.name));
|
||||
return optional ? type.name + '?' : type.name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (type.name === 'Array') {
|
||||
if (type.templates.length !== 1)
|
||||
throw new Error(`Array (${type.name} from ${parent.name}) has more than 1 dimension. Panic.`);
|
||||
|
||||
const innerType = translateType(type.templates[0], parent, generateNameCallback, false, isReturnType);
|
||||
return isReturnType ? `IReadOnlyList<${innerType}>` : `IEnumerable<${innerType}>`;
|
||||
}
|
||||
|
||||
if (type.name === 'Object') {
|
||||
// take care of some common cases
|
||||
// TODO: this can be genericized
|
||||
if (type.templates && type.templates.length === 2) {
|
||||
// get the inner types of both templates, and if they're strings, it's a keyvaluepair string, string,
|
||||
const keyType = translateType(type.templates[0], parent, generateNameCallback, false, isReturnType);
|
||||
const valueType = translateType(type.templates[1], parent, generateNameCallback, false, isReturnType);
|
||||
if (['Request', 'Response', 'APIResponse'].includes(parent.name))
|
||||
return `Dictionary<${keyType}, ${valueType}>`;
|
||||
return `IEnumerable<KeyValuePair<${keyType}, ${valueType}>>`;
|
||||
}
|
||||
|
||||
if ((type.name === 'Object')
|
||||
&& !type.properties
|
||||
&& !type.union)
|
||||
return 'object';
|
||||
|
||||
// this is an additional type that we need to generate
|
||||
const objectName = generateNameCallback(type);
|
||||
if (objectName === 'Object')
|
||||
throw new Error('Object unexpected');
|
||||
else if (type.name === 'Object')
|
||||
registerModelType(objectName, type);
|
||||
|
||||
return `${objectName}${optional ? '?' : ''}`;
|
||||
}
|
||||
|
||||
if (type.name === 'Map') {
|
||||
if (type.templates && type.templates.length === 2) {
|
||||
// we map to a dictionary
|
||||
const keyType = translateType(type.templates[0], parent, generateNameCallback, false, isReturnType);
|
||||
const valueType = translateType(type.templates[1], parent, generateNameCallback, false, isReturnType);
|
||||
return `Dictionary<${keyType}, ${valueType}>`;
|
||||
} else {
|
||||
throw 'Map has invalid number of templates.';
|
||||
}
|
||||
}
|
||||
|
||||
if (type.name === 'function') {
|
||||
if (type.expression === '[function]' || !type.args)
|
||||
return 'Action'; // super simple mapping
|
||||
|
||||
let argsList = '';
|
||||
if (type.args) {
|
||||
const translatedCallbackArguments = type.args.map(t => translateType(t, parent, generateNameCallback, false, isReturnType));
|
||||
if (translatedCallbackArguments.includes(null))
|
||||
throw new Error('There was an argument we could not parse. Aborting.');
|
||||
|
||||
argsList = translatedCallbackArguments.join(', ');
|
||||
}
|
||||
|
||||
if (!type.returnType) {
|
||||
// this is an Action
|
||||
return `Action<${argsList}>`;
|
||||
} else {
|
||||
const returnType = translateType(type.returnType, parent, generateNameCallback, false, isReturnType);
|
||||
if (returnType === null)
|
||||
throw new Error('Unexpected null as return type.');
|
||||
|
||||
if (!argsList)
|
||||
return `Func<${returnType}>`;
|
||||
return `Func<${argsList}, ${returnType}>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (type.templates) {
|
||||
// this should mean we have a generic type and we can translate that
|
||||
/** @type {string[]} */
|
||||
const types = type.templates.map(template => translateType(template, parent));
|
||||
return `${type.name}<${types.join(', ')}>`;
|
||||
}
|
||||
|
||||
if (type.name === 'Serializable')
|
||||
return isReturnType ? 'T' : 'object';
|
||||
|
||||
// there's a chance this is a name we've already seen before, so check
|
||||
// this is also where we map known types, like boolean -> bool, etc.
|
||||
const name = classNameMap.get(type.name) || type.name;
|
||||
return `${name}${optional ? '?' : ''}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} typeName
|
||||
* @param {Documentation.Type} type
|
||||
*/
|
||||
function registerModelType(typeName, type) {
|
||||
if (['object', 'string', 'int', 'long'].includes(typeName))
|
||||
return;
|
||||
|
||||
if (typeName.endsWith('Option'))
|
||||
return;
|
||||
|
||||
const potentialType = modelTypes.get(typeName);
|
||||
if (potentialType) {
|
||||
// console.log(`Type ${typeName} already exists, so skipping...`);
|
||||
return;
|
||||
}
|
||||
|
||||
modelTypes.set(typeName, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {string[]} value
|
||||
* @param {string[]} out
|
||||
*/
|
||||
function printArgDoc(name, value, out) {
|
||||
if (value.length === 1) {
|
||||
out.push(`/// <param name="${name}">${value}</param>`);
|
||||
} else {
|
||||
out.push(`/// <param name="${name}">`);
|
||||
out.push(...value.map(l => `/// ${l}`));
|
||||
out.push(`/// </param>`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {boolean} convert
|
||||
*/
|
||||
function toAsync(name, convert) {
|
||||
if (!convert)
|
||||
return name;
|
||||
if (name.includes('<'))
|
||||
return name.replace('<', 'Async<');
|
||||
return name + 'Async';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} suggestedName
|
||||
* @returns {string}
|
||||
*/
|
||||
function rewriteSuggestedOptionsName(suggestedName) {
|
||||
if ([
|
||||
'APIRequestContextDeleteOptions',
|
||||
'APIRequestContextFetchOptions',
|
||||
'APIRequestContextGetOptions',
|
||||
'APIRequestContextHeadOptions',
|
||||
'APIRequestContextPatchOptions',
|
||||
'APIRequestContextPostOptions',
|
||||
'APIRequestContextPutOptions',
|
||||
].includes(suggestedName))
|
||||
return 'APIRequestContextOptions';
|
||||
return suggestedName;
|
||||
}
|
||||
|
||||
170
참고/playwright-main/utils/doclint/linkUtils.js
Normal file
170
참고/playwright-main/utils/doclint/linkUtils.js
Normal file
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/** @typedef {'Types'|'ReleaseNotesMd'} OutputType */
|
||||
|
||||
// @ts-check
|
||||
const toKebabCase = require('lodash/kebabCase.js')
|
||||
const Documentation = require('./documentation');
|
||||
|
||||
/**
|
||||
* @param {string} languagePath
|
||||
* @param {Documentation.Member} member
|
||||
* @param {string} text
|
||||
* @param {string=} paramOrOption
|
||||
* @returns {string}
|
||||
*/
|
||||
function createMarkdownLink(languagePath, member, text, paramOrOption) {
|
||||
if (!member.clazz)
|
||||
throw new Error('Member without a class!');
|
||||
const className = toKebabCase(member.clazz.name);
|
||||
const memberName = toKebabCase(member.name);
|
||||
let hash = null;
|
||||
if (member.kind === 'property' || member.kind === 'method')
|
||||
hash = `${className}-${memberName}`.toLowerCase();
|
||||
else if (member.kind === 'event')
|
||||
hash = `${className}-event-${memberName}`.toLowerCase();
|
||||
if (paramOrOption)
|
||||
hash += '-option-' + toKebabCase(paramOrOption).toLowerCase();
|
||||
return `[${text}](https://playwright.dev${languagePath}/docs/api/class-${member.clazz.name.toLowerCase()}#${hash})`;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} languagePath
|
||||
* @param {Documentation.Class} clazz
|
||||
* @returns {string}
|
||||
*/
|
||||
function createClassMarkdownLink(languagePath, clazz) {
|
||||
return `[${clazz.name}](https://playwright.dev${languagePath}/docs/api/class-${clazz.name.toLowerCase()})`;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} language
|
||||
* @param {OutputType} outputType
|
||||
* @returns {Documentation.Renderer}
|
||||
*/
|
||||
function docsLinkRendererForLanguage(language, outputType) {
|
||||
const languagePath = languageToRelativeDocsPath(language);
|
||||
return ({ clazz, member, param, option }) => {
|
||||
if (clazz)
|
||||
return createClassMarkdownLink(languagePath, clazz);
|
||||
if (!member || !member.clazz)
|
||||
throw new Error('Internal error');
|
||||
if (param)
|
||||
return createMarkdownLink(languagePath, member, `\`${param.alias}\``, param.name);
|
||||
if (option)
|
||||
return createMarkdownLink(languagePath, member, `\`${option.alias}\``, option.name);
|
||||
const className = member.clazz.varName === 'playwrightAssertions' ? '' : member.clazz.varName + '.';
|
||||
if (member.kind === 'method') {
|
||||
const args = outputType === 'ReleaseNotesMd' ? '' : renderJSSignature(member.argsArray);
|
||||
return createMarkdownLink(languagePath, member, `${formatClassName(className, language)}${member.alias}(${args})`);
|
||||
}
|
||||
if (member.kind === 'event')
|
||||
return createMarkdownLink(languagePath, member, `${className}on('${member.alias.toLowerCase()}')`);
|
||||
if (member.kind === 'property')
|
||||
return createMarkdownLink(languagePath, member, `${className}${member.alias}`);
|
||||
throw new Error('Unknown member kind ' + member.kind);
|
||||
}
|
||||
}
|
||||
|
||||
function languageToRelativeDocsPath(language) {
|
||||
if (language === 'js')
|
||||
return '';
|
||||
if (language === 'csharp')
|
||||
return '/dotnet';
|
||||
if (language === 'python')
|
||||
return '/python';
|
||||
if (language === 'java')
|
||||
return '/java';
|
||||
throw new Error('Unexpected language ' + language);
|
||||
}
|
||||
|
||||
function formatClassName(className, language) {
|
||||
if (!className.endsWith('Assertions.'))
|
||||
return className;
|
||||
className = className.substring(0, className.length - 1)
|
||||
if (language === 'js')
|
||||
return `expect(${assertionArgument(className)}).`;
|
||||
else if (language === 'csharp')
|
||||
return `Expect(${assertionArgument(className)}).`;
|
||||
else if (language === 'python')
|
||||
return `expect(${assertionArgument(className)}).`;
|
||||
else if (language === 'java')
|
||||
return `assertThat(${assertionArgument(className)}).`;
|
||||
throw new Error('Unexpected language ' + language);
|
||||
}
|
||||
|
||||
function assertionArgument(className) {
|
||||
switch (className.toLowerCase()) {
|
||||
case 'locatorassertions': return 'locator';
|
||||
case 'pageassertions': return 'page';
|
||||
case 'genericassertions': return 'value';
|
||||
case 'snapshotassertions': return 'value';
|
||||
case 'apiresponseassertions': return 'response';
|
||||
}
|
||||
throw new Error(`Unexpected assertion class: ${className}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Documentation.Member[]} args
|
||||
*/
|
||||
function renderJSSignature(args) {
|
||||
const tokens = [];
|
||||
let hasOptional = false;
|
||||
for (const arg of args) {
|
||||
const name = arg.alias;
|
||||
const optional = !arg.required;
|
||||
if (tokens.length) {
|
||||
if (optional && !hasOptional)
|
||||
tokens.push(`[, ${name}`);
|
||||
else
|
||||
tokens.push(`, ${name}`);
|
||||
} else {
|
||||
if (optional && !hasOptional)
|
||||
tokens.push(`[${name}`);
|
||||
else
|
||||
tokens.push(`${name}`);
|
||||
}
|
||||
hasOptional = hasOptional || optional;
|
||||
}
|
||||
if (hasOptional)
|
||||
tokens.push(']');
|
||||
return tokens.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @param {string} languagePath
|
||||
* @param {string} relativePath
|
||||
* @param {boolean} releaseNotesMode
|
||||
* @returns {string}
|
||||
*/
|
||||
function renderPlaywrightDevLinks(content, languagePath, relativePath, releaseNotesMode = false) {
|
||||
return content.replace(/\[([^\]]+)\]\((\.[^\)]+)\)/g, (match, p1, p2) => {
|
||||
if (releaseNotesMode && p2.includes('/images/')) {
|
||||
const url = new URL(p2, `https://github.com/microsoft/playwright/blob/main/docs/src/`);
|
||||
url.searchParams.set('raw', 'true');
|
||||
return `[${p1}](${url.toString()})`;
|
||||
}
|
||||
return `[${p1}](${new URL(p2.replace('.md', ''), `https://playwright.dev${languagePath}/docs${relativePath}/`).toString()})`;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
docsLinkRendererForLanguage,
|
||||
renderPlaywrightDevLinks,
|
||||
languageToRelativeDocsPath,
|
||||
}
|
||||
351
참고/playwright-main/utils/doclint/linting-code-snippets/cli.js
Normal file
351
참고/playwright-main/utils/doclint/linting-code-snippets/cli.js
Normal file
@@ -0,0 +1,351 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* 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 debug = require('debug')
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { parseApi } = require('../api_parser');
|
||||
const md = require('../../markdown');
|
||||
const { ESLint } = require('eslint')
|
||||
const child_process = require('child_process');
|
||||
const os = require('os');
|
||||
|
||||
const { codeFrameColumns } = require('@babel/code-frame');
|
||||
|
||||
/** @typedef {import('../documentation').Type} Type */
|
||||
/** @typedef {import('../../markdown').MarkdownNode} MarkdownNode */
|
||||
|
||||
const PROJECT_DIR = path.join(__dirname, '..', '..', '..');
|
||||
|
||||
function getAllMarkdownFiles(dirPath, filePaths = []) {
|
||||
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
|
||||
if (entry.isFile() && entry.name.toLowerCase().endsWith('.md'))
|
||||
filePaths.push(path.join(dirPath, entry.name));
|
||||
else if (entry.isDirectory())
|
||||
getAllMarkdownFiles(path.join(dirPath, entry.name), filePaths);
|
||||
}
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
const jsOnly = process.argv.includes('--js-only');
|
||||
const lintingServiceFactory = new LintingServiceFactory(jsOnly);
|
||||
const documentationRoot = path.join(PROJECT_DIR, 'docs', 'src');
|
||||
let documentation = parseApi(path.join(documentationRoot, 'api'));
|
||||
|
||||
/** @type {CodeSnippet[]} */
|
||||
const codeSnippets = [];
|
||||
for (const filePath of getAllMarkdownFiles(documentationRoot)) {
|
||||
const data = fs.readFileSync(filePath, 'utf-8');
|
||||
let rootNode = md.parse(data);
|
||||
// Renders links.
|
||||
documentation.renderLinksInNodes(rootNode);
|
||||
documentation.generateSourceCodeComments();
|
||||
md.visitAll(rootNode, node => {
|
||||
if (node.type !== 'code')
|
||||
return;
|
||||
const codeLang = node.codeLang.split(' ')[0];
|
||||
const code = node.lines.join('\n');
|
||||
codeSnippets.push({
|
||||
filePath,
|
||||
codeLang,
|
||||
code,
|
||||
})
|
||||
});
|
||||
}
|
||||
await lintingServiceFactory.lintAndReport(codeSnippets);
|
||||
if (jsOnly)
|
||||
return;
|
||||
const { hasErrors } = lintingServiceFactory.reportMetrics();
|
||||
if (hasErrors)
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
/** @typedef {{ codeLang: string, code: string, filePath: string }} CodeSnippet */
|
||||
/** @typedef {{ status: 'ok' | 'updated' | 'error' | 'unsupported', error?: string }} LintResult */
|
||||
|
||||
class LintingService {
|
||||
/**
|
||||
* @param {string} codeLang
|
||||
* @returns {boolean}
|
||||
*/
|
||||
supports(codeLang) {
|
||||
throw new Error('supports() is not implemented');
|
||||
}
|
||||
|
||||
async _writeTempSnippetsFile(snippets) {
|
||||
const tempFile = path.join(os.tmpdir(), `snippet-${Date.now()}.json`);
|
||||
await fs.promises.writeFile(tempFile, JSON.stringify(snippets, undefined, 2));
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} command
|
||||
* @param {string[]} args
|
||||
* @param {CodeSnippet[]} snippets
|
||||
* @param {string} cwd
|
||||
* @returns {Promise<LintResult[]>}
|
||||
*/
|
||||
async spawnAsync(command, args, snippets, cwd) {
|
||||
const tempFile = await this._writeTempSnippetsFile(snippets);
|
||||
return await new Promise((fulfill, reject) => {
|
||||
const child = child_process.spawn(command, [...args, tempFile], { cwd });
|
||||
let stdout = '';
|
||||
child.on('error', reject);
|
||||
child.stdout.on('data', data => stdout += data.toString());
|
||||
child.stderr.pipe(process.stderr);
|
||||
child.on('exit', code => {
|
||||
if (code)
|
||||
reject(new Error(`${command} exited with code ${code}`));
|
||||
else
|
||||
fulfill(JSON.parse(stdout));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CodeSnippet[]} snippets
|
||||
* @returns {Promise<LintResult[]>}
|
||||
*/
|
||||
async lint(snippets) {
|
||||
throw new Error('lint() is not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class JSLintingService extends LintingService {
|
||||
_knownBadSnippets = [
|
||||
'mount(',
|
||||
'render(',
|
||||
'vue-router',
|
||||
'experimental-ct',
|
||||
];
|
||||
|
||||
async _init() {
|
||||
if (this._eslint)
|
||||
return this._eslint;
|
||||
|
||||
const { fixupConfigRules } = await import('@eslint/compat');
|
||||
const { FlatCompat } = await import('@eslint/eslintrc');
|
||||
// @ts-ignore
|
||||
const js = (await import('@eslint/js')).default;
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
// @ts-ignore
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all
|
||||
});
|
||||
const baseConfig = fixupConfigRules(compat.extends('plugin:react/recommended', 'plugin:@typescript-eslint/disable-type-checked'));
|
||||
const { baseRules }= await import('../../../eslint.config.mjs');
|
||||
|
||||
this._eslint = new ESLint({
|
||||
baseConfig,
|
||||
plugins: /** @type {any}*/({
|
||||
'@stylistic': (await import('@stylistic/eslint-plugin')).default,
|
||||
}),
|
||||
ignore: false,
|
||||
overrideConfig: {
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
settings: {
|
||||
react: { version: 'detect' },
|
||||
},
|
||||
languageOptions: {
|
||||
// @ts-ignore
|
||||
parser: await import('@typescript-eslint/parser'),
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
rules: /** @type {any}*/({
|
||||
...baseRules,
|
||||
'notice/notice': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'max-len': ['error', { code: 100 }],
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'eol-last': 'off',
|
||||
'@typescript-eslint/consistent-type-imports': 'off',
|
||||
}),
|
||||
}
|
||||
});
|
||||
return this._eslint;
|
||||
}
|
||||
|
||||
supports(codeLang) {
|
||||
return codeLang === 'js' || codeLang === 'ts';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CodeSnippet} snippet
|
||||
* @returns {Promise<LintResult>}
|
||||
*/
|
||||
async _lintSnippet(snippet) {
|
||||
const eslint = await this._init();
|
||||
if (this._knownBadSnippets.some(s => snippet.code.includes(s)))
|
||||
return { status: 'ok' };
|
||||
const results = await eslint.lintText(snippet.code, { filePath: path.join(__dirname, 'file.tsx') });
|
||||
if (!results || !results.length || !results[0].messages.length)
|
||||
return { status: 'ok' };
|
||||
const result = results[0];
|
||||
const error = result.source ? results[0].messages[0].message + '\n\n' + codeFrameColumns(result.source, { start: result.messages[0] }, { highlightCode: true }) : results[0].messages[0].message;
|
||||
return { status: 'error', error };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CodeSnippet[]} snippets
|
||||
* @returns {Promise<LintResult[]>}
|
||||
*/
|
||||
async lint(snippets) {
|
||||
const result = [];
|
||||
for (let i = 0; i < snippets.length; ++i)
|
||||
result.push(await this._lintSnippet(snippets[i]));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class PythonLintingService extends LintingService {
|
||||
supports(codeLang) {
|
||||
return codeLang === 'python' || codeLang === 'py';
|
||||
}
|
||||
|
||||
async lint(snippets) {
|
||||
const result = await this.spawnAsync('python', [path.join(__dirname, 'python', 'main.py')], snippets, path.join(__dirname, 'python'))
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class CSharpLintingService extends LintingService {
|
||||
supports(codeLang) {
|
||||
return codeLang === 'csharp';
|
||||
}
|
||||
|
||||
async lint(snippets) {
|
||||
return await this.spawnAsync('dotnet', ['run', '--project', path.join(__dirname, 'csharp')], snippets, path.join(__dirname, 'csharp'))
|
||||
}
|
||||
}
|
||||
|
||||
class JavaLintingService extends LintingService {
|
||||
supports(codeLang) {
|
||||
return codeLang === 'java';
|
||||
}
|
||||
|
||||
async lint(snippets) {
|
||||
return await this.spawnAsync('java', ['-jar', path.join(__dirname, 'java', 'target', 'java-syntax-checker-1.0-SNAPSHOT.jar')], snippets, path.join(__dirname, 'java'))
|
||||
}
|
||||
}
|
||||
|
||||
class LintingServiceFactory {
|
||||
constructor(jsOnly) {
|
||||
/** @type {LintingService[]} */
|
||||
this.services = [
|
||||
new JSLintingService(),
|
||||
]
|
||||
if (!jsOnly) {
|
||||
this.services.push(
|
||||
new PythonLintingService(),
|
||||
new CSharpLintingService(),
|
||||
new JavaLintingService(),
|
||||
);
|
||||
}
|
||||
this._metrics = {};
|
||||
this._log = debug('linting-service');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CodeSnippet[]} allSnippets
|
||||
*/
|
||||
async lintAndReport(allSnippets) {
|
||||
/** @type {Record<string, CodeSnippet[]>} */
|
||||
const groupedByLanguage = allSnippets.reduce((acc, snippet) => {
|
||||
if (!acc[snippet.codeLang])
|
||||
acc[snippet.codeLang] = [];
|
||||
acc[snippet.codeLang].push(snippet);
|
||||
return acc;
|
||||
}, {});
|
||||
for (const language in groupedByLanguage) {
|
||||
const service = this.services.find(service => service.supports(language));
|
||||
if (!service) {
|
||||
this._collectMetrics(language, {
|
||||
status: 'unsupported',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const languageSnippets = groupedByLanguage[language];
|
||||
const results = await service.lint(languageSnippets);
|
||||
if (results.length !== languageSnippets.length)
|
||||
throw new Error('Linting service returned wrong number of results');
|
||||
|
||||
for (const [{ code, codeLang, filePath }, result] of /** @type {[[CodeSnippet, LintResult]]} */ (results.map((result, index) => [languageSnippets[index], result]))) {
|
||||
const { status, error } = result;
|
||||
this._collectMetrics(codeLang, result);
|
||||
if (status === 'error') {
|
||||
console.log(`${codeLang} linting error!`);
|
||||
console.log(`ERROR: ${error}`);
|
||||
console.log(`File: ${filePath}`);
|
||||
console.log(code);
|
||||
console.log('-'.repeat(80));
|
||||
if (process.env.GITHUB_ACTION) {
|
||||
const actions = await import('@actions/core');
|
||||
actions.warning(`Error: ${error}\nUnable to lint:\n${code}`, {
|
||||
title: `${codeLang} linting error`,
|
||||
file: filePath,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {{ hasErrors: boolean }}
|
||||
*/
|
||||
reportMetrics() {
|
||||
console.log('Metrics:');
|
||||
const renderMetric = (metric, name) => {
|
||||
if (!metric[name])
|
||||
return '';
|
||||
return `${name}: ${metric[name]}`;
|
||||
}
|
||||
let hasErrors = false;
|
||||
const languagesOrderedByOk = Object.entries(this._metrics).sort(([langA], [langB]) => {
|
||||
return this._metrics[langB].ok - this._metrics[langA].ok
|
||||
})
|
||||
for (const [language, metrics] of languagesOrderedByOk) {
|
||||
if (metrics.error)
|
||||
hasErrors = true;
|
||||
console.log(` ${language}: ${['ok', 'updated', 'error', 'unsupported'].map(name => renderMetric(metrics, name)).filter(Boolean).join(', ')}`)
|
||||
}
|
||||
return { hasErrors }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} language
|
||||
* @param {LintResult} result
|
||||
*/
|
||||
_collectMetrics(language, result) {
|
||||
if (!this._metrics[language])
|
||||
this._metrics[language] = { ok: 0, updated: 0, error: 0, unsupported: 0 };
|
||||
this._metrics[language][result.status]++;
|
||||
}
|
||||
}
|
||||
|
||||
run().catch(e => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
2
참고/playwright-main/utils/doclint/linting-code-snippets/csharp/.gitignore
vendored
Normal file
2
참고/playwright-main/utils/doclint/linting-code-snippets/csharp/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
obj/
|
||||
bin/
|
||||
@@ -0,0 +1,43 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
var codeSnippetsPath = args[args.Length - 1];
|
||||
var codeSnippets = JsonSerializer.Deserialize<List<CodeSnippet>>(File.ReadAllText(codeSnippetsPath));
|
||||
if (codeSnippets == null)
|
||||
{
|
||||
Console.WriteLine("Error: codeSnippets is null");
|
||||
return;
|
||||
}
|
||||
var output = new List<object>();
|
||||
|
||||
foreach (var codeSnippet in codeSnippets)
|
||||
{
|
||||
var tree = CSharpSyntaxTree.ParseText(codeSnippet.code);
|
||||
var syntaxErrors = tree.GetDiagnostics()
|
||||
.Where(diag => diag.Severity == DiagnosticSeverity.Error)
|
||||
.ToList();
|
||||
if (syntaxErrors.Any())
|
||||
{
|
||||
output.Add(new
|
||||
{
|
||||
status = "error",
|
||||
error = string.Join("\n", syntaxErrors.Select(diag => diag.GetMessage()))
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
output.Add(new
|
||||
{
|
||||
status = "ok"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine(JsonSerializer.Serialize(output));
|
||||
|
||||
record CodeSnippet(string filePath, string codeLang, string code);
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RootNamespace>csharp</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis" Version="4.10.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
2
참고/playwright-main/utils/doclint/linting-code-snippets/java/.gitignore
vendored
Normal file
2
참고/playwright-main/utils/doclint/linting-code-snippets/java/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
target/
|
||||
dependency-reduced-pom.xml
|
||||
@@ -0,0 +1,53 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>java-syntax-checker</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>15</maven.compiler.source>
|
||||
<maven.compiler.target>15</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.github.javaparser</groupId>
|
||||
<artifactId>javaparser-core</artifactId>
|
||||
<version>3.26.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.11.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>JavaSyntaxChecker</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,110 @@
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.github.javaparser.JavaParser;
|
||||
import com.github.javaparser.Problem;
|
||||
import com.github.javaparser.ParseResult;
|
||||
import com.github.javaparser.ast.CompilationUnit;
|
||||
import com.github.javaparser.ParserConfiguration;
|
||||
import com.github.javaparser.StaticJavaParser;
|
||||
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class JavaSyntaxChecker {
|
||||
public static void main(String[] args) {
|
||||
if (args.length == 0) {
|
||||
System.out.println("Error: Please provide the path to the JSON file");
|
||||
return;
|
||||
}
|
||||
|
||||
String codeSnippetsPath = args[args.length - 1];
|
||||
List<CodeSnippet> codeSnippets = readCodeSnippets(codeSnippetsPath);
|
||||
if (codeSnippets == null) {
|
||||
System.out.println("Error: codeSnippets is null");
|
||||
return;
|
||||
}
|
||||
|
||||
List<Map<String, Object>> output = new ArrayList<>();
|
||||
|
||||
ParserConfiguration config = new ParserConfiguration();
|
||||
config.setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_17);
|
||||
|
||||
for (CodeSnippet codeSnippet : codeSnippets) {
|
||||
String cleanedCode = cleanSnippet(codeSnippet.code);
|
||||
ParseResult<CompilationUnit> parseResult = new JavaParser(config).parse(cleanedCode);
|
||||
List<Problem> syntaxErrors = parseResult.getProblems();
|
||||
|
||||
if (!syntaxErrors.isEmpty()) {
|
||||
output.add(Map.of(
|
||||
"status", "error",
|
||||
"error", String.join("\n", syntaxErrors.stream()
|
||||
.map(Problem::getMessage)
|
||||
.collect(Collectors.toList()))
|
||||
));
|
||||
} else {
|
||||
output.add(Map.of("status", "ok"));
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println(new Gson().toJson(output));
|
||||
}
|
||||
|
||||
private static String removeImports(String code) {
|
||||
// Remove import statements
|
||||
return Pattern.compile("^import.*;$", Pattern.MULTILINE)
|
||||
.matcher(code)
|
||||
.replaceAll("");
|
||||
}
|
||||
|
||||
private static String cleanSnippet(String code) {
|
||||
// if it contains "public class" then it's a full class, return immediately
|
||||
if (code.contains("public class")) {
|
||||
return code;
|
||||
}
|
||||
code = removeImports(code);
|
||||
String wrappedCode = """
|
||||
import com.microsoft.playwright.*;
|
||||
import static com.microsoft.playwright.assertions.PlaywrightAssertions.*;
|
||||
|
||||
public class Example {
|
||||
public static void main(String[] args) {
|
||||
try (Playwright playwright = Playwright.create()) {
|
||||
Browser browser = playwright.chromium().launch();
|
||||
BrowserContext context = browser.newContext();
|
||||
Page page = context.newPage();
|
||||
%s
|
||||
}
|
||||
}
|
||||
}
|
||||
""".formatted(code);
|
||||
return wrappedCode;
|
||||
}
|
||||
|
||||
private static List<CodeSnippet> readCodeSnippets(String filePath) {
|
||||
try (FileReader reader = new FileReader(filePath)) {
|
||||
Type listType = new TypeToken<ArrayList<CodeSnippet>>(){}.getType();
|
||||
return new Gson().fromJson(reader, listType);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CodeSnippet {
|
||||
String filePath;
|
||||
String codeLang;
|
||||
String code;
|
||||
|
||||
public CodeSnippet(String filePath, String codeLang, String code) {
|
||||
this.filePath = filePath;
|
||||
this.codeLang = codeLang;
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import json
|
||||
import sys
|
||||
import black
|
||||
|
||||
def check_code_snippet(code_snippet: str):
|
||||
try:
|
||||
formatted_code = black.format_str(code_snippet, mode=black.FileMode())
|
||||
except Exception as e:
|
||||
return {
|
||||
'status': 'error',
|
||||
'error': str(e),
|
||||
}
|
||||
if formatted_code.strip() == code_snippet.strip():
|
||||
return {
|
||||
'status': 'success',
|
||||
}
|
||||
return {
|
||||
'status': 'updated',
|
||||
'newCode': formatted_code,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
code_snippets_path = sys.argv[1]
|
||||
if not code_snippets_path:
|
||||
print("No code snippets path provided")
|
||||
return
|
||||
code_snippets = json.load(open(code_snippets_path))
|
||||
formatted_codes = [check_code_snippet(snippet["code"]) for snippet in code_snippets]
|
||||
print(json.dumps(formatted_codes))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1 @@
|
||||
black==26.3.1
|
||||
40
참고/playwright-main/utils/doclint/templates/interface.cs
Normal file
40
참고/playwright-main/utils/doclint/templates/interface.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Playwright;
|
||||
|
||||
[CONTENT]
|
||||
Reference in New Issue
Block a user