diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 660de17f9e668adebd9ce310ba82879cd14d32e9..6a14a8d872d4943637386fbb658e08bdd508030f 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -215,6 +215,7 @@ await context.AddCookiesAsync(new[] { cookie1, cookie2 }); ``` ### param: BrowserContext.addCookies.cookies +* langs: go - `cookies` <[Array]<[Object]>> - `name` <[string]> - `value` <[string]> @@ -287,7 +288,17 @@ Script to be evaluated in all pages in the browser context. * langs: csharp, java - `script` <[string]|[path]> -Script to be evaluated in all pages in the browser context. +### param: BrowserContext.addInitScript.script +* langs: go +- `script` <[string]> + +Optional Script source to be evaluated in all pages in the browser context. + +### param: BrowserContext.addInitScript.path +* langs: go +- `path` <[string]> + +Optional Script path to be evaluated in all pages in the browser context. ### param: BrowserContext.addInitScript.arg * langs: js @@ -1021,7 +1032,7 @@ it gets merged via the [`new URL()`](https://developer.mozilla.org/en-US/docs/We handler function to route the request. ### param: BrowserContext.route.handler -* langs: csharp, java +* langs: csharp, java,go - `handler` <[function]\([Route]\)> handler function to route the request. @@ -1186,7 +1197,7 @@ A glob pattern, regex pattern or predicate receiving [URL] used to register a ro [`method: BrowserContext.route`]. ### param: BrowserContext.unroute.handler -* langs: js, python +* langs: js, python, go - `handler` <[function]\([Route], [Request]\)> Optional handler function used to register a routing with [`method: BrowserContext.route`]. diff --git a/docs/src/api/class-frame.md b/docs/src/api/class-frame.md index 34b59c1a05eae03d9bde75e4a2a34fff180b8b2d..67c810045ea3c4b3a1bd9b87029703c85cc9e3b0 100644 --- a/docs/src/api/class-frame.md +++ b/docs/src/api/class-frame.md @@ -1449,7 +1449,7 @@ await page.MainFrame.WaitForFunctionAsync("selector => !!document.querySelector( Optional argument to pass to [`param: expression`]. -### option: Frame.waitForFunction.polling = %%-js-python-wait-for-function-polling-%% +### option: Frame.waitForFunction.polling = %%-js-python-go-wait-for-function-polling-%% ### option: Frame.waitForFunction.polling = %%-csharp-java-wait-for-function-polling-%% diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 5979b8d5528b771fd01e1d70d5f9a247b6d183c5..4b0bceb89242f328435020d6d782133a001ea328 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -522,6 +522,18 @@ Script to be evaluated in all pages in the browser context. Optional argument to pass to [`param: script`] (only supported when passing a function). +### param: Page.addInitScript.script +* langs: go +- `script` <[string]> + +Optional Script source to be evaluated in all pages in the browser context. + +### param: Page.addInitScript.path +* langs: go +- `path` <[string]> + +Optional Script path to be evaluated in all pages in the browser context. + ## async method: Page.addScriptTag - returns: <[ElementHandle]> @@ -969,7 +981,7 @@ Changes the CSS media type of the page. The only allowed values are `'screen'`, Passing `null` disables CSS media emulation. ### option: Page.emulateMedia.media -* langs: csharp +* langs: csharp, go - `media` <[Media]<"screen"|"print"|"null">> Changes the CSS media type of the page. The only allowed values are `'Screen'`, `'Print'` and `'Null'`. @@ -983,7 +995,7 @@ Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'` `null` disables color scheme emulation. ### option: Page.emulateMedia.colorScheme -* langs: csharp +* langs: csharp, go - `colorScheme` <[ColorScheme]<"light"|"dark"|"no-preference"|"null">> Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. Passing @@ -996,7 +1008,7 @@ Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'` Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. Passing `null` disables reduced motion emulation. ### option: Page.emulateMedia.reducedMotion -* langs: csharp +* langs: csharp, go - `reducedMotion` <[ReducedMotion]<"reduce"|"no-preference"|"null">> Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. Passing `null` disables reduced motion emulation. @@ -2284,7 +2296,7 @@ Paper format. If set, takes priority over [`option: width`] or [`option: height` Paper width, accepts values labeled with units. ### option: Page.pdf.width -* langs: csharp, java +* langs: csharp, java, go - `width` <[string]> Paper width, accepts values labeled with units. @@ -2296,7 +2308,7 @@ Paper width, accepts values labeled with units. Paper height, accepts values labeled with units. ### option: Page.pdf.height -* langs: csharp, java +* langs: csharp, java, go - `height` <[string]> Paper height, accepts values labeled with units. @@ -2312,7 +2324,7 @@ Paper height, accepts values labeled with units. Paper margins, defaults to none. ### option: Page.pdf.margin -* langs: csharp, java +* langs: csharp, java, go - `margin` <[Object]> - `top` <[string]> Top margin, accepts values labeled with units. Defaults to `0`. - `right` <[string]> Right margin, accepts values labeled with units. Defaults to `0`. @@ -2627,7 +2639,7 @@ A glob pattern, regex pattern or predicate receiving [URL] to match while routin When a [`option: baseURL`] via the context options was provided and the passed URL is a path, it gets merged via the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. ### param: Page.route.handler -* langs: js, python +* langs: js, python, go - `handler` <[function]\([Route], [Request]\)> handler function to route the request. @@ -3018,7 +3030,7 @@ the [`param: url`]. A glob pattern, regex pattern or predicate receiving [URL] to match while routing. ### param: Page.unroute.handler -* langs: js, python +* langs: js, python, go - `handler` <[function]\([Route], [Request]\)> Optional handler function to route the request. @@ -3268,7 +3280,7 @@ Shortcut for main frame's [`method: Frame.waitForFunction`]. Optional argument to pass to [`param: expression`]. -### option: Page.waitForFunction.polling = %%-js-python-wait-for-function-polling-%% +### option: Page.waitForFunction.polling = %%-js-python-go-wait-for-function-polling-%% ### option: Page.waitForFunction.polling = %%-csharp-java-wait-for-function-polling-%% diff --git a/docs/src/api/class-route.md b/docs/src/api/class-route.md index 61dd9e3ad4e9b83a8132bc61c026d2c18b35d979..fc440fe38af9dfbd4f6d3c7ef651890657c16008 100644 --- a/docs/src/api/class-route.md +++ b/docs/src/api/class-route.md @@ -103,10 +103,17 @@ If set changes the request URL. New URL must have same protocol as original one. If set changes the request method (e.g. GET or POST) ### option: Route.continue.postData +* langs: js - `postData` <[string]|[Buffer]> If set changes the post data of request +### option: Route.continue.postData +* langs: go +- `postData` <[any]> + +If set changes the post data of request + ### option: Route.continue.headers - `headers` <[Object]<[string], [string]>> @@ -202,6 +209,12 @@ If set, equals to setting `Content-Type` response header. Response body. +### option: Route.fulfill.body +* langs: go +- `body` <[any]> + +Response body. + ### option: Route.fulfill.body * langs: csharp, java - `body` <[string]> diff --git a/docs/src/api/params.md b/docs/src/api/params.md index f214cf943ae2c95c9377a093f0132e237b9bea2e..bb8269a16898d456ffd3601264469a5bda7518ac 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -146,8 +146,8 @@ Defaults to `'visible'`. Can be either: * `'hidden'` - wait for element to be either detached from DOM, or have an empty bounding box or `visibility:hidden`. This is opposite to the `'visible'` option. -## js-python-wait-for-function-polling -* langs: js, python +## js-python-go-wait-for-function-polling +* langs: js, python, go - `polling` <[float]|"raf"> If [`option: polling`] is `'raf'`, then [`param: expression`] is constantly executed in `requestAnimationFrame` @@ -168,14 +168,14 @@ If `true`, Playwright does not pass its own configurations args and only uses th array is given, then filters out the given default arguments. Dangerous option; use with care. Defaults to `false`. ## csharp-java-browser-option-ignoredefaultargs -* langs: csharp, java +* langs: csharp, java, go - `ignoreDefaultArgs` <[Array]<[string]>> If `true`, Playwright does not pass its own configurations args and only uses the ones from [`option: args`]. Dangerous option; use with care. ## csharp-java-browser-option-ignorealldefaultargs -* langs: csharp, java +* langs: csharp, java, go - `ignoreAllDefaultArgs` <[boolean]> If `true`, Playwright does not pass its own configurations args and only uses the ones from [`option: args`]. @@ -194,7 +194,7 @@ Dangerous option; use with care. Defaults to `false`. Network proxy settings. ## csharp-java-browser-option-env -* langs: csharp, java +* langs: csharp, java, go - `env` <[Object]<[string], [string]>> Specify environment variables that will be visible to the browser. Defaults to `process.env`. @@ -206,7 +206,7 @@ Specify environment variables that will be visible to the browser. Defaults to ` Specify environment variables that will be visible to the browser. Defaults to `process.env`. ## js-python-context-option-storage-state -* langs: js, python +* langs: js, python, go - `storageState` <[path]|[Object]> - `cookies` <[Array]<[Object]>> cookies to set for context - `name` <[string]> @@ -234,7 +234,7 @@ Populates context with given storage state. This option can be used to initializ obtained via [`method: BrowserContext.storageState`]. ## csharp-java-context-option-storage-state-path -* langs: csharp, java +* langs: csharp, java, go - `storageStatePath` <[path]> Populates context with given storage state. This option can be used to initialize context with logged-in information @@ -407,7 +407,7 @@ Function to be evaluated in the worker context. Function to be evaluated in the worker context. ## python-context-option-viewport -* langs: python +* langs: python, go - `viewport` <[null]|[Object]> - `width` <[int]> page width in pixels. - `height` <[int]> page height in pixels. @@ -415,7 +415,7 @@ Function to be evaluated in the worker context. Sets a consistent viewport for each page. Defaults to an 1280x720 viewport. `no_viewport` disables the fixed viewport. ## python-context-option-no-viewport -* langs: python +* langs: python, go - `noViewport` <[boolean]> Does not enforce fixed viewport, allows resizing window in the headed mode. @@ -557,7 +557,7 @@ call [`method: BrowserContext.close`] for the HAR to be saved. Optional setting to control whether to omit request content from the HAR. Defaults to `false`. ## context-option-recordvideo -* langs: js +* langs: js, go - `recordVideo` <[Object]> - `dir` <[path]> Path to the directory to put videos into. - `size` <[Object]> Optional dimensions of the recorded videos. If not specified the size will be equal to `viewport` @@ -831,7 +831,7 @@ Firefox user preferences. Learn more about the Firefox user preferences at [`about:config`](https://support.mozilla.org/en-US/kb/about-config-editor-firefox). ## csharp-java-browser-option-firefoxuserprefs -* langs: csharp, java +* langs: csharp, java, go - `firefoxUserPrefs` <[Object]<[string], [any]>> Firefox user preferences. Learn more about the Firefox user preferences at @@ -884,12 +884,14 @@ Slows down Playwright operations by the specified amount of milliseconds. Useful - %%-browser-option-tracesdir-%% ## locator-option-has-text +* langs: js, python, csharp, go - `hasText` <[string]|[RegExp]> Matches elements containing specified text somewhere inside, possibly in a child or a descendant element. For example, `"Playwright"` matches `
Playwright
`. ## locator-option-has +* langs: js, python, csharp, go - `has` <[Locator]> Matches elements containing an element that matches an inner locator. Inner locator is queried against the outer one. @@ -934,6 +936,7 @@ saved to the disk. Specify screenshot type, defaults to `png`. ## screenshot-option-mask +* langs: js, python, csharp - `mask` <[Array]<[Locator]>> Specify locators that should be masked when the screenshot is taken. Masked elements will be overlayed with diff --git a/utils/doclint/generateGoApi.js b/utils/doclint/generateGoApi.js new file mode 100644 index 0000000000000000000000000000000000000000..1c30b40e9fe05745829b34acd907bcdb06f53d8e --- /dev/null +++ b/utils/doclint/generateGoApi.js @@ -0,0 +1,765 @@ +/** + * 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 Documentation = require('./documentation'); +const XmlDoc = require('./xmlDocumentation') +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; + +/** @type {Map} */ +const additionalTypes = new Map(); // this will hold types that we discover, because of .NET specifics, like results +/** @type {Map} */ +const documentedResults = new Map(); // will hold documentation for new types +/** @type {Map} */ +const enumTypes = new Map(); + +let documentation; +/** @type {Map} */ +let classNameMap; + +{ + const typesDir = process.argv[2] || path.join(__dirname, 'generate_types', 'go'); + if (!fs.existsSync(typesDir)) + fs.mkdirSync(typesDir, { recursive: true }); + + const structsFile = path.join(typesDir, "generated-structs.go"); + const enumsFile = path.join(typesDir, "generated-enums.go"); + + for (const file of [structsFile, enumsFile]) + fs.writeFileSync(file, "package playwright\n") + + documentation = parseApi(path.join(PROJECT_DIR, 'docs', 'src', 'api')); + documentation.filterForLanguage('go'); + documentation.setLinkRenderer(item => { + if (item.clazz) + return translateMemberName("interface", item.clazz.name, null); + else if (item.option || item.param) + return `\`${item.option || item.param}\`` + else if (item.member) + return `${translateMemberName("interface", item.member.clazz.name, null)}.${translateMemberName(item.member.kind, item.member.name, item.member)}()`; + }); + // we have some "predefined" types, like the mixed state enum, that we can map in advance + enumTypes.set("MixedState", ["On", "Off", "Mixed"]); + + // map the name to a C# friendly one (we prepend an I to denote an interface) + classNameMap = new Map(documentation.classesArray.map(x => [x.name, translateMemberName('interface', x.name, null)])); + + // map some types that we know of + classNameMap.set('Error', 'Exception'); + classNameMap.set('TimeoutError', 'TimeoutException'); + classNameMap.set('EvaluationArgument', 'interface{}'); + classNameMap.set('boolean', '*bool'); + classNameMap.set('Serializable', 'T'); + classNameMap.set('any', 'interface{}'); + classNameMap.set('Buffer', '[]byte'); // TODO(mxschmitt): use bytes.Buffer + classNameMap.set('path', '*string'); + classNameMap.set('URL', 'string'); + classNameMap.set('RegExp', 'Regex'); + + // this are types that we don't explicility render even if we get the specs + const ignoredTypes = ['TimeoutException']; + + /** + * @param {string} file + * @param {string[]} data + */ + let appendFile = (file, data) => { + let content = data.join(`${EOL}\t`); + fs.appendFileSync(file, content); + } + + for (const element of documentation.classesArray) { + const name = classNameMap.get(element.name); + if (ignoredTypes.includes(name)) + continue; + + const out = []; + console.log(`Generating ${name}`); + + if (element.spec) + out.push(...XmlDoc.renderXmlDoc(element.spec, maxDocumentationColumnWidth)); + else { + let ownDocumentation = documentedResults.get(name); + if (ownDocumentation) { + out.push(`// ${ownDocumentation}`); + } + } + + if (element.extends === 'IEventEmitter') + element.extends = null; + + out.push(`type ${name} interface {`); + if (element.extends) + out.push(element.extends) + + for (const member of element.membersArray) { + renderMember(member, element, out); + } + + // we want to separate the items with a space and this is nicer, than holding + // an index in each iterator down the line + const lastLine = out.pop(); + if (lastLine !== '') + out.push(lastLine); + + out.push('}'); + } + + additionalTypes.forEach((type, name) => { + if (name.startsWith("Android") || !type.properties?.length) + return + const out = [] + let ownDocumentation = documentedResults.get(name); + if (ownDocumentation) + out.push(`// ${ownDocumentation}`) + out.push(`type ${name} struct {`) + // 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) { + let fakeType = new Type(name, null); + renderMember(member, fakeType, out); + } + } else if (type.union) { + console.log("enum", type) + } else { + console.log(type); + throw new Error(`Not sure what to do in this case.`); + } + out.push("}\n") + appendFile(structsFile, out); + }); + + enumTypes.forEach((values, name) => { + const out = [] + const fcall = `get${name}` + out.push(`func ${fcall}(in string) *${name} { + v := ${name}(in) + return &v + } + `) + + out.push(`type ${name} string`) + out.push(" var (") + values.forEach((v, i) => { + // strip out the quotes + v = v.replace(/[\"]/g, ``) + let escapedEnumValue = v.replace(/[-]/g, ' ') + .split(' ') + .map(word => word[0].toUpperCase() + word.substring(1)).join(''); + + if (i === 0) + out.push(`${name}${escapedEnumValue} *${name} = ${fcall}("${v}")`) + else + out.push(`${name}${escapedEnumValue} = ${fcall}("${v}")`) + }); + out.push(")\n") + + appendFile(enumsFile, out); + }); +} + +/** + * @param {string} memberKind + * @param {string} name + * @param {Documentation.Member} member + */ +function translateMemberName(memberKind, name, member = null) { + if (!name) return name; + + // we strip it for special chars, like @ because we might get called back with it in some special cases + // like, when generating classes inside methods for params + name = name.replace(/[@-]/g, ''); + + if (memberKind === 'argument') { + if (['params', 'event'].includes(name)) { // just in case we want to add others + return `@${name}`; + } else { + return name; + } + } + + // check if there's an alias in the docs, in which case + // we return that, otherwise, we apply our dotnet magic to it + if (member) { + if (member.alias !== name) { + return member.alias; + } + } + + // we sanitize some common abbreviations to ensure consistency + name = name.replace(/(HTTP[S]?)/g, (m, g) => { + return g[0].toUpperCase() + g.substring(1).toLowerCase(); + }); + + if (name === "url") + return "URL" + + let assumedName = name.charAt(0).toUpperCase() + name.substring(1); + + switch (memberKind) { + case "interface": + // apply name mapping if the map exists + let mappedName = classNameMap ? classNameMap.get(assumedName) : null; + if (mappedName) + return mappedName; + return `${assumedName}`; + case "method": + if (member) + return `${assumedName}`; + return assumedName; + case "event": + return `${assumedName}`; + case "enum": + return `${assumedName}`; + default: + return `${assumedName}`; + } +} + +/** + * + * @param {Documentation.Member} member + * @param {Documentation.Class|Documentation.Type} parent + * @param {string[]} out + */ +function renderMember(member, parent, out) { + let output = line => { + if (typeof (line) === 'string') + out.push(`\t${line}`); + else + out.push(...line.map(x => `\t${x}`)); + } + + let name = translateMemberName(member.kind, member.name, member); + if (member.kind === 'method') { + renderMethod(member, parent, output, name); + } else { + 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}`); + if (member.spec) + output(XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth)); + } else if (member.kind === 'property') { + if (member.spec) { + const text = member.spec + if (text.length > 0) { + text.forEach(line => { + let words = line.text.split(' '); + let lines = []; + let lineText = ""; + for (let i = 0; i < words.length; i++) { + lineText += ' ' + words[i]; + if (lineText.length >= 80) { + lines.push(lineText); + lineText = ''; + } + } + lines.push(lineText); + lines.filter(line => !["", " "].includes(line)).forEach(line => output(`//${line}`)); + } + ) + } + } + let propertyOrigin = member.name; + if (member.type.expression === '[string]|[float]') + propertyOrigin = `${member.name}String`; + if (parent && member && member.name === 'children') { // this is a special hack for Accessibility + console.warn(`children property found in ${parent.name}, assuming array.`); + type = `[]${parent.name}`; + } + if (parent.name.endsWith("Result")) { + type = type.replace("*", "") + } + output(`${name} ${type} \`json:"${propertyOrigin}"\``); + } else { + throw new Error(`Problem rendering a member: ${type} - ${name} (${member.kind})`); + } + } +} + +/** + * + * @param {Documentation.Member} member + * @param {string} name + * @param {Documentation.Type} t + * @param {*} parent + */ +function generateNameDefault(member, name, t, parent) { + if (!t.properties + && !t.templates + && !t.union + && t.expression === '[Object]') + return 'interface{}'; + + // we'd get this call for enums, primarily + let enumName = generateEnumNameIfApplicable(member, name, t, parent); + if (!enumName && member) { + if (member.kind === 'method' || member.kind === 'property') { + // this should be easy to name... let's call it the same as the argument (eternal optimist) + let probableName = `${parent.name}${translateMemberName(``, name, null)}`; + let probableType = additionalTypes.get(probableName); + if (probableType) { + // compare it with what? + if (probableType.expression != t.expression) { + throw new Error(`Non-matching types with the same name. Panic.`); + } + } else { + additionalTypes.set(probableName, t); + } + + return probableName; + } + + if (member.kind === 'event') { + return `${name}Payload`; + } + } + + return enumName || t.name; +} + +function generateEnumNameIfApplicable(member, name, type, parent) { + 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 + + if (type && type.name) + return type.name; + + // our enum naming policy leaves a few bits to be desired, but it'll do for now + // however, with the recent changes, this almost never gets called anymore + return translateMemberName('enum', name, type); +} + +/** + * + * @param {string} v + * @returns {string} + */ +function makeFirstCharUpperCase(v) { + return v[0].toUpperCase() + v.slice(1) +} + +/** + * 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 {Function} output + */ +function renderMethod(member, parent, output, name) { + const typeResolve = (type) => translateType(type, parent, (t) => { + let newName = `${parent.name}${translateMemberName(member.kind, member.name, null)}Result`; + documentedResults.set(newName, `Result of calling .`); + return newName; + }); + + /** @type {Map} */ + const paramDocs = new Map(); + /** + * @param {string} paramName + * @param {string[]} docs + */ + const addParamsDoc = (paramName, docs) => { + if (paramName.startsWith('@')) + paramName = paramName.substring(1); + if (paramDocs.get(paramName)) + throw new Error(`Parameter ${paramName} already exists in the docs.`); + paramDocs.set(paramName, docs); + }; + + /** @type {string} */ + let type = null; + // need to check the original one + if (member.type.name === 'Object' || member.type.name === 'Array') { + let innerType = member.type; + let isArray = false; + if (innerType.name === 'Array') { + // we want to influence the name, but also change the object type + innerType = member.type.templates[0]; + isArray = true; + } + + if (innerType.expression === '[Object]<[string], [string]>') { + // do nothing, because this is handled down the road + } else if (!isArray && !innerType.properties) { + type = `dynamic`; + } else { + type = classNameMap.get(innerType.name); + if (!type) { + type = typeResolve(innerType); + } + + if (isArray) + type = `IReadOnlyCollection<${type}>`; + } + } + + type = type || typeResolve(member.type); + + const optionsStructName = `${parent.name}${makeFirstCharUpperCase(member.name)}Options` + let optionsStructMembers = member.argsArray.find(a => a.name === "options")?.type.properties || [] + if (!optionsStructMembers.length) + optionsStructMembers = member.argsArray.filter(a => (!a.required || a.langs.only?.includes("go"))) + + if (optionsStructMembers.length > 0) { + let fakeType = new Type("Object", optionsStructMembers); + additionalTypes.set(optionsStructName, fakeType) + } + // 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')) { + if (!member.async) { + if (member.spec) + output(XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth)); + output(`${type} ${name} { get; }`); + return; + } + name = `Get${name}`; + } else if (member.args.size == 1 + && type === 'void' + && name.startsWith('Set') + && !member.async) { + name = name.substring(3); // remove the 'Set' + if (member.spec) + output(XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth)); + output(`${translateType(member.argsArray[0].type, parent)} ${name} { set; }`); + return; + } + + // HACK: special case for generics handling! + if (type === 'T') { + name = `${name}`; + } + + // adjust the return type for async methods + // if (member.async) { + // if (type === 'void') + // type = `Task`; + // else + // type = `Task<${type}>`; + // } + + // render args + let args = []; + /** + * + * @param {string} innerArgType + * @param {string} innerArgName + * @param {Documentation.Member} argument + */ + const pushArg = (innerArgType, innerArgName, argument) => { + let isNullable = ['int', 'bool', 'decimal', 'float'].includes(innerArgType); + const requiredPrefix = argument.required ? "" : isNullable ? "?" : ""; + const requiredSuffix = argument.required ? "" : " = default"; + args.push(`${innerArgType}${requiredPrefix} ${innerArgName}${requiredSuffix}`); + }; + + let parseArg = (/** @type {Documentation.Member} */ arg) => { + if (arg.name === "options") { + arg.type.properties.forEach(prop => { + parseArg(prop); + }); + return; + } + + if (arg.type.expression === '[string]|[path]') { + let argName = translateMemberName('argument', arg.name, null); + pushArg("string", argName, arg); + pushArg("string", `${argName}Path`, arg); + if (arg.spec) { + addParamsDoc(argName, XmlDoc.renderTextOnly(arg.spec, maxDocumentationColumnWidth)); + addParamsDoc(`${argName}Path`, [`Instead of specifying , 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 + let argName = translateMemberName('argument', arg.name, null); + let leftArgType = translateType(arg.type.union[0], parent, (t) => { throw new Error('Not supported'); }); + let 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 is true.`]); + + return; + } + + const argName = translateMemberName('argument', arg.alias || arg.name, null); + 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 + let 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.'); + + let argDocumentation = XmlDoc.renderTextOnly(arg.spec, maxDocumentationColumnWidth); + for (const newArg of translatedArguments) { + const sanitizedArgName = newArg.match(/(?<=^[\s"']*)(\w+)/g, '')?.[0] || newArg; + const newArgName = `${argName}${sanitizedArgName[0].toUpperCase() + sanitizedArgName.substring(1)}`; + pushArg(newArg, newArgName, arg); + addParamsDoc(newArgName, argDocumentation); + } + 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); + }; + + member.args.forEach(parseArg); + + output(XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth)); + paramDocs.forEach((val, ind) => { + if (val && val.length === 1) + output(`/// ${val}`); + else { + output(`/// `); + output(val.map(l => `/// ${l}`)); + output(`/// `); + } + }); + output(`${name}(${args.join(', ')}) ${type}`); +} + +/** + * + * @callback generateNameCallback + * @param {Documentation.Type} t + * @returns {string} + */ + +/** + * @param {Documentation.Type} type + * @param {Documentation.Class|Documentation.Type} parent + * @param {generateNameCallback} generateNameCallback +*/ +function translateType(type, parent, generateNameCallback = t => t.name) { + if (type.name === "int") + return "*int" + if (type.name === "string") + return "*string" + if (type.name === "float") + return "*float64" + if (type.name === "Serializable") + return "interface{}" + if (type.name === "Logger") + return "interface{}" + // a few special cases we can fix automatically + if (type.expression === '[null]|[Error]') + return 'void'; + else if (type.expression === '[boolean]|"mixed"') + return 'MixedState'; + + let isNullableEnum = false; + if (type.union) { + if (type.union[0].name === 'null') { + // for dotnet, this is a nullable type + // if the other side is a primitive type + if (type.union.length > 2) { + if (type.union.filter(x => x.name.startsWith('"')).length == type.union.length - 1) + isNullableEnum = true; + else + return `interface{}` + // throw new Error(`Union (${parent.name}) with null is too long.`); + } else { + const innerTypeName = translateType(type.union[1], parent, generateNameCallback); + // if type is primitive, or an enum, then it's nullable + if (innerTypeName === 'bool' + || innerTypeName === 'int') { + return `${innerTypeName}?`; + } + + // if it's not a value type, it'll be nullable by default, so we can ignore it + return `${innerTypeName}`; + } + } + + if (type.union.filter(u => u.name.startsWith(`"`)).length == type.union.length + || isNullableEnum) { + // this is an enum + let enumName = generateNameCallback(type); + if (!enumName) + throw new Error(`This was supposed to be an enum, but it failed generating a name, ${type.name} ${parent ? parent.name : ""}.`); + + // make sure we map the enum, or invalidate the name, in case it doesn't match well + const potentialEnum = enumTypes.get(enumName); + let enumValues = type.union.filter(x => x.name !== 'null').map(x => x.name); + if (potentialEnum) { + // compare values + if (potentialEnum.join(',') !== enumValues.join(',')) { + // for now, we'll merge the two enums, if they have the same name, and we'll go from there + potentialEnum.concat(enumValues.filter(x => !potentialEnum.includes(x))); // merge & de-dupe + // TODO: think about doing global type annotation, where we can add comments, such as this? + enumTypes.set(enumName, potentialEnum); + } + } else { + enumTypes.set(enumName, enumValues); + } + if (isNullableEnum) + return `*${enumName}?`; + return `*${enumName}`; + } + + if (type.expression === '[string]|[Buffer]') + return `[]byte`; // TODO: make sure we implement extension methods for this! + else 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`; + } else if (type.union.length == 2 && type.union[1].name === 'Array' && type.union[1].templates[0].name === type.union[0].name) + return `[]${type.union[0].name}`; // an example of this is [string]|[Array]<[string]> + else if (type.union[0].name === 'path') + // we don't support path, but we know it's usually an object on the other end, and we expect + // the dotnet folks to use [NameOfTheObject].LoadFromPath(); method which we can provide separately + return translateType(type.union[1], parent, generateNameCallback); + else if (type.expression === '[float]|"raf"') + return `interface{}`; // hardcoded because there's no other way to denote this + else if (type.expression === '[string]|[RegExp]') + return "interface{}" + if (type.expression === "[string]|[RegExp]|[function]([URL]):[boolean]") + return "interface{}" + return null; + } + + const removePointer = i => i.replace(/^\*(.*)/g, "$1") + if (type.name === 'Array') { + if (type.templates.length != 1) + throw new Error(`Array (${type.name} from ${parent.name}) has more than 1 dimension. Panic.`); + + let innerType = translateType(type.templates[0], parent, generateNameCallback); + return `[]${removePointer(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, + let keyType = translateType(type.templates[0], parent, generateNameCallback); + let valueType = translateType(type.templates[1], parent, generateNameCallback); + return `map[${removePointer(keyType)}]${removePointer(valueType)}`; + } + + if ((type.name === 'Object') + && !type.properties + && !type.union) { + return 'interface{}'; + } + // this is an additional type that we need to generate + let objectName = generateNameCallback(type); + if (objectName === 'Object') { + throw new Error('Object unexpected'); + } else if (type.name === 'Object') { + registerAdditionalType(objectName, type); + } + return `*${objectName}`; + } + + if (type.name === 'Map') { + if (type.templates && type.templates.length == 2) { + // we map to a dictionary + let keyType = translateType(type.templates[0], parent, generateNameCallback); + let valueType = translateType(type.templates[1], parent, generateNameCallback); + return `Dictionary<${keyType}, ${valueType}>`; + } else { + throw 'Map has invalid number of templates.'; + } + } + + if (type.name === 'function') { + if (type.expression === '[function]' || !type.args) + return 'interface{}'; // super simple mapping + + let argsList = ''; + if (type.args) { + let translatedCallbackArguments = type.args.map(t => translateType(t, parent, generateNameCallback)); + 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 `func(${argsList})`; + } else { + let returnType = translateType(type.returnType, parent, generateNameCallback); + if (returnType == null) + throw new Error('Unexpected null as return type.'); + + return `Func<${argsList}, ${returnType}>`; + } + } + + // 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. + let name = classNameMap.get(type.name) || type.name; + return `${name}`; +} + +/** + * + * @param {string} typeName + * @param {Documentation.Type} type + */ +function registerAdditionalType(typeName, type) { + if (['object', 'string', 'int'].includes(typeName)) + return; + + let potentialType = additionalTypes.get(typeName); + if (potentialType) { + console.log(`Type ${typeName} already exists, so skipping...`); + return; + } + + additionalTypes.set(typeName, type); +}