chore: introduce internal protocol

This commit is contained in:
Dmitry Gozman 2020-01-29 10:46:38 -08:00
parent 492304be41
commit d68f57dba8
6 changed files with 1017 additions and 1 deletions

116
src/protocol/browser.ts Normal file
View file

@ -0,0 +1,116 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
* Modifications 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.
*/
import { helper } from '../helper';
import { ClientConnection } from './connection';
import * as platform from '../platform';
import { ConnectionTransport } from '../transport';
import { Events } from '../events';
import * as types from '../types';
export type BrowserContextOptions = {
viewport?: types.Viewport | null,
// ignoreHTTPSErrors?: boolean,
// javaScriptEnabled?: boolean,
// bypassCSP?: boolean,
// userAgent?: string,
// timezoneId?: string,
geolocation?: types.Geolocation,
// permissions?: { [key: string]: string[] };
};
export class Browser extends platform.EventEmitter {
readonly _connection: ClientConnection;
readonly _browserContexts = new Map<string, BrowserContext>();
constructor(transport: ConnectionTransport) {
super();
this._connection = new ClientConnection(transport, () => this.emit(Events.Browser.Disconnected));
}
async newContext(options: BrowserContextOptions = {}): Promise<BrowserContext> {
options = { ...options };
if (!options.viewport && options.viewport !== null)
options.viewport = { width: 800, height: 600 };
if (options.viewport)
options.viewport = { ...options.viewport };
if (options.geolocation)
options.geolocation = verifyGeolocation(options.geolocation);
const { contextId } = await this._connection.send('BrowserContext.create', {
viewportSize: options.viewport ? { width: options.viewport.width, height: options.viewport.height } : undefined,
geolocation: options.geolocation,
});
return new BrowserContext(this, contextId, options);
}
async disconnect() {
const disconnected = new Promise(f => this.once(Events.Browser.Disconnected, f));
this._connection.close();
await disconnected;
}
isConnected(): boolean {
return !this._connection.isClosed();
}
}
export class BrowserContext {
readonly _browser: Browser;
readonly _browserContextId: string;
readonly _options: BrowserContextOptions;
private _closed = false;
constructor(browser: Browser, browserContextId: string, options: BrowserContextOptions) {
this._browser = browser;
this._browserContextId = browserContextId;
this._options = options;
this._browser._browserContexts.set(this._browserContextId, this);
}
async setGeolocation(geolocation: types.Geolocation | null): Promise<void> {
if (geolocation)
geolocation = verifyGeolocation(geolocation);
this._options.geolocation = geolocation || undefined;
await this._browser._connection.send('BrowserContext.setGeolocation', {
contextId: this._browserContextId,
geolocation: this._options.geolocation,
});
}
async close() {
if (this._closed)
return;
this._closed = true;
this._browser._browserContexts.delete(this._browserContextId);
await this._browser._connection.send('BrowserContext.destroy', { contextId: this._browserContextId });
}
}
function verifyGeolocation(geolocation: types.Geolocation): types.Geolocation {
const result = { ...geolocation };
result.accuracy = result.accuracy || 0;
const { longitude, latitude, accuracy } = result;
if (!helper.isNumber(longitude) || longitude < -180 || longitude > 180)
throw new Error(`Invalid longitude "${longitude}": precondition -180 <= LONGITUDE <= 180 failed.`);
if (!helper.isNumber(latitude) || latitude < -90 || latitude > 90)
throw new Error(`Invalid latitude "${latitude}": precondition -90 <= LATITUDE <= 90 failed.`);
if (!helper.isNumber(accuracy) || accuracy < 0)
throw new Error(`Invalid accuracy "${accuracy}": precondition 0 <= ACCURACY failed.`);
return result;
}

205
src/protocol/connection.ts Normal file
View file

@ -0,0 +1,205 @@
/**
* 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.
*/
import { assert } from '../helper';
import * as platform from '../platform';
import { ConnectionTransport } from '../transport';
import { Protocol } from './protocol';
const debugClient = platform.debug('pw:client');
const debugServer = platform.debug('pw:server');
export interface ObjectTransport {
send(message: any): void;
close(): void; // Note: calling close is expected to issue onclose at some point.
onmessage?: (message: any) => void,
onclose?: () => void,
}
export class JSONTransport implements ObjectTransport {
private readonly _transport: ConnectionTransport;
onmessage?: (message: string) => void;
onclose?: () => void;
constructor(transport: ConnectionTransport) {
this._transport = transport;
this._transport.onmessage = message => {
if (this.onmessage)
this.onmessage(JSON.parse(message));
};
this._transport.onclose = () => {
if (this.onclose)
this.onclose();
};
this.onmessage = undefined;
this.onclose = undefined;
}
send(message: any) {
this._transport.send(JSON.stringify(message));
}
close() {
this._transport.close();
}
}
export class ClientConnection extends platform.EventEmitter {
private _lastId = 0;
private readonly _transport: ObjectTransport;
private _closed = false;
private _onDisconnect: () => void;
private readonly _callbacks = new Map<number, {resolve:(o: any) => void, reject: (e: Error) => void, error: Error, method: string}>();
on: <T extends keyof Protocol.Events>(event: T, listener: (payload: Protocol.Events[T]) => void) => this;
addListener: <T extends keyof Protocol.Events>(event: T, listener: (payload: Protocol.Events[T]) => void) => this;
off: <T extends keyof Protocol.Events>(event: T, listener: (payload: Protocol.Events[T]) => void) => this;
removeListener: <T extends keyof Protocol.Events>(event: T, listener: (payload: Protocol.Events[T]) => void) => this;
once: <T extends keyof Protocol.Events>(event: T, listener: (payload: Protocol.Events[T]) => void) => this;
constructor(transport: ObjectTransport, onDisconnect: () => void) {
super();
this._transport = transport;
this._transport.onmessage = this._dispatchMessage.bind(this);
this._transport.onclose = this._onClose.bind(this);
this._onDisconnect = onDisconnect;
this.on = super.on;
this.off = super.removeListener;
this.addListener = super.addListener;
this.removeListener = super.removeListener;
this.once = super.once;
}
send<T extends keyof Protocol.CommandParameters>(
method: T,
params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> {
if (this._closed)
return Promise.reject(new Error(`Internal error (${method}): Connection has been closed.`));
const id = ++this._lastId;
const message = { id, method, params };
const result = new Promise<Protocol.CommandReturnValues[T]>((resolve, reject) => {
this._callbacks.set(id, {resolve, reject, error: new Error(), method});
});
debugClient('SEND ► %o', message);
this._transport.send(message);
return result;
}
private _dispatchMessage(message: any) {
debugClient('◀ RECV %o', message);
if (message.id && this._callbacks.has(message.id)) {
const callback = this._callbacks.get(message.id)!;
this._callbacks.delete(message.id);
if (message.error)
callback.reject(createInternalError(callback.error, callback.method, message));
else
callback.resolve(message.result);
} else {
assert(!message.id);
Promise.resolve().then(() => this.emit(message.method, message.params));
}
}
_onClose() {
this._closed = true;
this._transport.onmessage = undefined;
this._transport.onclose = undefined;
for (const callback of this._callbacks.values())
callback.reject(rewriteError(callback.error, `Internal error (${callback.method}): Connection has been closed.`));
this._callbacks.clear();
this._onDisconnect();
}
isClosed() {
return this._closed;
}
close() {
if (!this._closed)
this._transport.close();
}
}
export class ServerConnection {
private readonly _transport: ObjectTransport;
private _closed = false;
private _onDisconnect: () => void;
private readonly _handlers = new Map<string, (arg: any) => Promise<any>>();
constructor(transport: ObjectTransport, onDisconnect: () => void) {
this._transport = transport;
this._transport.onmessage = this._dispatchMessage.bind(this);
this._transport.onclose = this._onClose.bind(this);
this._onDisconnect = onDisconnect;
}
on<T extends keyof Protocol.CommandParameters>(command: T, handler: (payload: Protocol.CommandParameters[T]) => Promise<Protocol.CommandReturnValues[T]>) {
this._handlers.set(command, handler);
}
send<T extends keyof Protocol.Events>(event: T, params?: Protocol.Events[T]) {
if (this._closed)
return Promise.reject(new Error(`Internal error (${event}): Connection has been closed.`));
const message = { event, params };
debugServer('SEND ► %o', message);
this._transport.send(message);
}
private async _dispatchMessage(message: any) {
debugServer('◀ RECV %o', message);
assert(message.id);
let result;
try {
const handler = this._handlers.get(message.method);
if (!handler)
throw new Error(`Unknown command "${message.method}"`);
result = { id: message.id, result: await handler(message.params) };
} catch (e) {
result = { id: message.id, error: { message: e.message } };
}
this._transport.send(result);
}
_onClose() {
this._closed = true;
this._transport.onmessage = undefined;
this._transport.onclose = undefined;
this._onDisconnect();
}
isClosed() {
return this._closed;
}
close() {
if (!this._closed)
this._transport.close();
}
}
function createInternalError(error: Error, method: string, object: { error: { message: string; data: any; }; }): Error {
let message = `Internal error (${method}): ${object.error.message}`;
if ('data' in object.error)
message += ` ${object.error.data}`;
return rewriteError(error, message);
}
function rewriteError(error: Error, message: string): Error {
error.message = message;
return error;
}

231
src/protocol/protocol.pdl Normal file
View file

@ -0,0 +1,231 @@
# 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.
domain Types
type Size extends object
properties
integer width
integer height
domain BrowserContext
type BrowserContextId extends string
type Geolocation extends object
properties
number longitude
number latitude
optional number accuracy
command create
parameters
optional Types.Size viewportSize
optional Geolocation geolocation
returns
BrowserContextId contextId
command destroy
parameters
BrowserContextId contextId
command setGeolocation
parameters
BrowserContextId contextId
optional Geolocation geolocation
domain Frame
type FrameId extends string
type FrameTree extends object
properties
FrameId frameId
string url
optional array of FrameTree childFrames
type Lifecycle extends string
enum
domcontentloaded
load
networkidle0
networkidle2
command createPage
parameters
BrowserContext.BrowserContextId contextId
optional string url
returns
FrameId mainFrameId
command destroyPage
parameters
FrameId mainFrameId
event pageCreated
parameters
BrowserContext.BrowserContextId contextId
FrameTree frameTree
event pageDestroyed
parameters
FrameId mainFrameId
event attached
parameters
FrameId frameId
FrameId parentFrameId
event navigated
parameters
FrameId frameId
string url
event detached
parameters
FrameId frameId
command navigate
parameters
FrameId frameId
string url
array of Lifecycle waitUntil
optional string referrer
returns
optional Network.RequestId requestId
domain JS
type HandleId extends string
type Handle extends object
properties
HandleId handleId
string asString
boolean isDOMElement
type Value extends object
properties
optional string string
optional number number
optional boolean null
optional boolean undefined
optional boolean boolean
optional array of Value values
optional array of string keys
type CallArgument extends object
properties
optional Value value
optional HandleId handleId
command evaluateHandle
parameters
string functionExpression
array of CallArgument args
optional Frame.FrameId frameId
optional HandleId handleId
returns
Handle handle
command evaluate
parameters
string functionExpression
array of CallArgument args
optional Frame.FrameId frameId
optional HandleId handleId
returns
Value value
command handleAsValue
parameters
HandleId handleId
returns
Value value
command disposeHandle
parameters
HandleId handleId
domain DOM
command ownerFrame
parameters
JS.HandleId handleId
returns
optional Frame.FrameId frameId
command contentFrame
parameters
JS.HandleId handleId
returns
optional Frame.FrameId frameId
command scrollIntoViewIfNeeded
parameters
JS.HandleId handleId
domain Network
type RequestId extends string
type ResourceType extends string
enum
document
type Header extends object
properties
string name
string value
type Headers extends array of Header
event requestStarted
parameters
RequestId requestId
Frame.FrameId frameId
string url
string method
ResourceType resourceType
Headers headers
boolean isIntercepted
boolean isNavigation
optional RequestId redirectedFrom
optional string postData
event responseReceived
parameters
RequestId requestId
event requestFailed
parameters
RequestId requestId
string errorText
event requestFinished
parameters
RequestId requestId
command abortRequest
parameters
RequestId requestId
string errorCode
command fulfillRequest
parameters
RequestId requestId
integer status
Headers headers
string contentType
string base64body
command continueRequest
parameters
RequestId requestId
optional Headers headers

270
src/protocol/protocol.ts Normal file
View file

@ -0,0 +1,270 @@
// This is generated from /utils/protocol-types-generator/index.js
type binary = string;
export module Protocol {
export module Types {
export interface Size {
width: number;
height: number;
}
}
export module BrowserContext {
export type BrowserContextId = string;
export interface Geolocation {
longitude: number;
latitude: number;
accuracy?: number;
}
export type createParameters = {
viewportSize?: Types.Size;
geolocation?: Geolocation;
}
export type createReturnValue = {
contextId: BrowserContextId;
}
export type destroyParameters = {
contextId: BrowserContextId;
}
export type destroyReturnValue = {
}
export type setGeolocationParameters = {
contextId: BrowserContextId;
geolocation?: Geolocation;
}
export type setGeolocationReturnValue = {
}
}
export module Frame {
export type FrameId = string;
export interface FrameTree {
frameId: FrameId;
url: string;
childFrames?: FrameTree[];
}
export type Lifecycle = "domcontentloaded"|"load"|"networkidle0"|"networkidle2";
export type pageCreatedPayload = {
contextId: BrowserContext.BrowserContextId;
frameTree: FrameTree;
}
export type pageDestroyedPayload = {
mainFrameId: FrameId;
}
export type attachedPayload = {
frameId: FrameId;
parentFrameId: FrameId;
}
export type navigatedPayload = {
frameId: FrameId;
url: string;
}
export type detachedPayload = {
frameId: FrameId;
}
export type createPageParameters = {
contextId: BrowserContext.BrowserContextId;
url?: string;
}
export type createPageReturnValue = {
mainFrameId: FrameId;
}
export type destroyPageParameters = {
mainFrameId: FrameId;
}
export type destroyPageReturnValue = {
}
export type navigateParameters = {
frameId: FrameId;
url: string;
waitUntil: Lifecycle[];
referrer?: string;
}
export type navigateReturnValue = {
requestId?: Network.RequestId;
}
}
export module JS {
export type HandleId = string;
export interface Handle {
handleId: HandleId;
asString: string;
isDOMElement: boolean;
}
export interface Value {
string?: string;
number?: number;
null?: boolean;
undefined?: boolean;
boolean?: boolean;
values?: Value[];
keys?: string[];
}
export interface CallArgument {
value?: Value;
handleId?: HandleId;
}
export type evaluateHandleParameters = {
functionExpression: string;
args: CallArgument[];
frameId?: Frame.FrameId;
handleId?: HandleId;
}
export type evaluateHandleReturnValue = {
handle: Handle;
}
export type evaluateParameters = {
functionExpression: string;
args: CallArgument[];
frameId?: Frame.FrameId;
handleId?: HandleId;
}
export type evaluateReturnValue = {
value: Value;
}
export type handleAsValueParameters = {
handleId: HandleId;
}
export type handleAsValueReturnValue = {
value: Value;
}
export type disposeHandleParameters = {
handleId: HandleId;
}
export type disposeHandleReturnValue = {
}
}
export module DOM {
export type ownerFrameParameters = {
handleId: JS.HandleId;
}
export type ownerFrameReturnValue = {
frameId?: Frame.FrameId;
}
export type contentFrameParameters = {
handleId: JS.HandleId;
}
export type contentFrameReturnValue = {
frameId?: Frame.FrameId;
}
export type scrollIntoViewIfNeededParameters = {
handleId: JS.HandleId;
}
export type scrollIntoViewIfNeededReturnValue = {
}
}
export module Network {
export type RequestId = string;
export type ResourceType = "document";
export interface Header {
name: string;
value: string;
}
export type Headers = Header[];
export type requestStartedPayload = {
requestId: RequestId;
frameId: Frame.FrameId;
url: string;
method: string;
resourceType: ResourceType;
headers: Headers;
isIntercepted: boolean;
isNavigation: boolean;
redirectedFrom?: RequestId;
postData?: string;
}
export type responseReceivedPayload = {
requestId: RequestId;
}
export type requestFailedPayload = {
requestId: RequestId;
errorText: string;
}
export type requestFinishedPayload = {
requestId: RequestId;
}
export type abortRequestParameters = {
requestId: RequestId;
errorCode: string;
}
export type abortRequestReturnValue = {
}
export type fulfillRequestParameters = {
requestId: RequestId;
status: number;
headers: Headers;
contentType: string;
base64body: string;
}
export type fulfillRequestReturnValue = {
}
export type continueRequestParameters = {
requestId: RequestId;
headers?: Headers;
}
export type continueRequestReturnValue = {
}
}
export interface Events {
"Frame.pageCreated": Frame.pageCreatedPayload;
"Frame.pageDestroyed": Frame.pageDestroyedPayload;
"Frame.attached": Frame.attachedPayload;
"Frame.navigated": Frame.navigatedPayload;
"Frame.detached": Frame.detachedPayload;
"Network.requestStarted": Network.requestStartedPayload;
"Network.responseReceived": Network.responseReceivedPayload;
"Network.requestFailed": Network.requestFailedPayload;
"Network.requestFinished": Network.requestFinishedPayload;
}
export interface CommandParameters {
"BrowserContext.create": BrowserContext.createParameters;
"BrowserContext.destroy": BrowserContext.destroyParameters;
"BrowserContext.setGeolocation": BrowserContext.setGeolocationParameters;
"Frame.createPage": Frame.createPageParameters;
"Frame.destroyPage": Frame.destroyPageParameters;
"Frame.navigate": Frame.navigateParameters;
"JS.evaluateHandle": JS.evaluateHandleParameters;
"JS.evaluate": JS.evaluateParameters;
"JS.handleAsValue": JS.handleAsValueParameters;
"JS.disposeHandle": JS.disposeHandleParameters;
"DOM.ownerFrame": DOM.ownerFrameParameters;
"DOM.contentFrame": DOM.contentFrameParameters;
"DOM.scrollIntoViewIfNeeded": DOM.scrollIntoViewIfNeededParameters;
"Network.abortRequest": Network.abortRequestParameters;
"Network.fulfillRequest": Network.fulfillRequestParameters;
"Network.continueRequest": Network.continueRequestParameters;
}
export interface CommandReturnValues {
"BrowserContext.create": BrowserContext.createReturnValue;
"BrowserContext.destroy": BrowserContext.destroyReturnValue;
"BrowserContext.setGeolocation": BrowserContext.setGeolocationReturnValue;
"Frame.createPage": Frame.createPageReturnValue;
"Frame.destroyPage": Frame.destroyPageReturnValue;
"Frame.navigate": Frame.navigateReturnValue;
"JS.evaluateHandle": JS.evaluateHandleReturnValue;
"JS.evaluate": JS.evaluateReturnValue;
"JS.handleAsValue": JS.handleAsValueReturnValue;
"JS.disposeHandle": JS.disposeHandleReturnValue;
"DOM.ownerFrame": DOM.ownerFrameReturnValue;
"DOM.contentFrame": DOM.contentFrameReturnValue;
"DOM.scrollIntoViewIfNeeded": DOM.scrollIntoViewIfNeededReturnValue;
"Network.abortRequest": Network.abortRequestReturnValue;
"Network.fulfillRequest": Network.fulfillRequestReturnValue;
"Network.continueRequest": Network.continueRequestReturnValue;
}
}

View file

@ -0,0 +1,2 @@
const { generateInternalProtocol } = require('./protocol-types-generator');
generateInternalProtocol();

View file

@ -216,4 +216,196 @@ function firefoxTypeToString(type, indent=' ') {
return type['$type']; return type['$type'];
} }
module.exports = {generateChromiunProtocol, generateFirefoxProtocol, generateWebKitProtocol}; function generateInternalProtocol() {
const pdlPath = path.join(__dirname, '..', '..', 'src', 'protocol', 'protocol.pdl');
const tsPath = path.join(__dirname, '..', '..', 'src', 'protocol', 'protocol.ts');
const json = pdlToJSON(fs.readFileSync(pdlPath, 'utf-8'));
fs.writeFileSync(tsPath, jsonToTS(json));
console.log(`Wrote protocol.ts to ${path.relative(process.cwd(), tsPath)}`);
}
function pdlToJSON(pdl) {
const lines = pdl.split('\n');
let lineIndex = 0;
let current;
function next() {
while (lineIndex < lines.length) {
let line = lines[lineIndex++];
let indent = 0;
while (line[indent] === ' ')
indent++;
let commentIndex = line.indexOf('#');
if (commentIndex !== -1)
line = line.substring(0, commentIndex);
line = line.trim();
if (line) {
current = { line, indent };
return current;
}
}
current = { indent: -1, line: '' };
return current;
}
function readDomains() {
const domains = [];
while (current.indent !== -1)
domains.push(readDomain());
return domains;
}
function readDomain() {
const indent = current.indent;
const match = current.line.match(/^domain\s+([a-zA-Z0-9_]+)$/);
next();
const domain = {
domain: match[1],
commands: [],
events: [],
types: [],
};
while (current.indent > indent) {
if (current.line.startsWith('command'))
domain.commands.push(readCommand());
else if (current.line.startsWith('event'))
domain.events.push(readEvent());
else if (current.line.startsWith('type'))
domain.types.push(readType());
else
throw new Error(`Cannot parse: ${current.line}`);
}
return domain;
}
function readCommand() {
const indent = current.indent;
const match = current.line.match(/^command\s+([a-zA-Z0-9_]+)$/);
next();
const command = {
name: match[1],
parameters: [],
returns: [],
};
while (current.indent > indent) {
if (current.line === 'parameters') {
const i = current.indent;
next();
while (current.indent > i)
command.parameters.push(readProperty());
} else if (current.line === 'returns') {
const i = current.indent;
next();
while (current.indent > i)
command.returns.push(readProperty());
} else {
throw new Error(`Cannot parse: ${current.line}`);
}
}
return command;
}
function readEvent() {
const indent = current.indent;
const match = current.line.match(/^event\s+([a-zA-Z0-9_]+)$/);
next();
const event = {
name: match[1],
parameters: [],
returns: [],
};
while (current.indent > indent) {
if (current.line === 'parameters') {
const i = current.indent;
next();
while (current.indent > i)
event.parameters.push(readProperty());
} else {
throw new Error(`Cannot parse: ${current.line}`);
}
}
return event;
}
function readType() {
const match = current.line.match(/^type\s+([a-zA-Z0-9_]+)\s+extends(.*)$/);
const type = {
id: match[1],
};
let t = type;
const words = match[2].split(' ').filter(s => !!s);
while (words[0] === 'array' && words[1] === 'of') {
t.type = 'array';
const items = {};
t.items = items;
t = items;
words.shift();
words.shift();
}
if (words.length !== 1)
throw new Error(`Cannot parse: ${current.line}`);
if (['boolean', 'integer', 'number'].includes(words[0])) {
t.type = words[0];
next();
} else if (words[0] === 'object') {
t.type = 'object';
next();
if (current.line !== 'properties')
throw new Error(`Cannot parse: ${current.line}`);
const indent = current.indent;
next();
t.properties = [];
while (current.indent > indent)
t.properties.push(readProperty());
} else if (words[0] === 'string') {
t.type = 'string';
next();
if (current.line === 'enum') {
const indent = current.indent;
next();
t.enum = [];
while (current.indent > indent) {
if (!current.line.match(/^[a-zA-Z0-9_]+$/))
throw new Error(`Cannot parse: ${current.line}`);
t.enum.push(current.line);
next();
}
}
} else {
t.$ref = words[0];
next();
}
return type;
}
function readProperty() {
const match = current.line.match(/^(optional)?\s*(.*)\s+([a-zA-Z0-9_]+)$/);
const prop = {
optional: !!match[1],
name: match[3],
};
let t = prop;
const words = match[2].split(' ').filter(s => !!s);
while (words[0] === 'array' && words[1] === 'of') {
t.type = 'array';
const items = {};
t.items = items;
t = items;
words.shift();
words.shift();
}
if (words.length !== 1)
throw new Error(`Cannot parse: ${current.line}`);
if (['string', 'boolean', 'integer', 'number'].includes(words[0]))
t.type = words[0];
else
t.$ref = words[0];
next();
return prop;
}
next();
return { domains: readDomains() };
}
module.exports = {generateChromiunProtocol, generateFirefoxProtocol, generateWebKitProtocol, generateInternalProtocol};