feat(tracker): 3.4.0 - iframe, resourceBaseHref & finder performance options

This commit is contained in:
ShiKhu 2021-09-23 19:14:45 +02:00
parent 43862e32e1
commit ba7ae009c4
12 changed files with 228 additions and 430 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@openreplay/tracker",
"version": "3.2.5",
"version": "3.3.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View file

@ -1,7 +1,7 @@
{
"name": "@openreplay/tracker",
"description": "The OpenReplay tracker main package",
"version": "3.3.0",
"version": "3.4.0",
"keywords": [
"logging",
"replay"

View file

@ -1,4 +1,4 @@
import { timestamp, log } from '../utils';
import { timestamp, log, warn } from '../utils';
import { Timestamp, TechnicalInfo, PageClose } from '../../messages';
import Message from '../../messages/message';
import Nodes from './nodes';
@ -24,6 +24,8 @@ export type Options = {
session_pageno_key: string;
local_uuid_key: string;
ingestPoint: string;
resourceBaseHref: string, // resourceHref?
//resourceURLRewriter: (url: string) => string | boolean,
__is_snippet: boolean;
__debug_report_edp: string | null;
onStart?: (info: OnStartInfo) => void;
@ -65,10 +67,12 @@ export default class App {
session_pageno_key: '__openreplay_pageno',
local_uuid_key: '__openreplay_uuid',
ingestPoint: DEFAULT_INGEST_POINT,
resourceBaseHref: '',
__is_snippet: false,
__debug_report_edp: null,
obscureTextEmails: true,
obscureTextNumbers: false,
captureIFrames: false,
},
opts,
);
@ -118,6 +122,7 @@ export default class App {
if(this.options.__debug_report_edp !== null) {
fetch(this.options.__debug_report_edp, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
context,
error: `${e}`
@ -199,11 +204,26 @@ export default class App {
return this._sessionID || undefined;
}
getHost(): string {
return new URL(this.options.ingestPoint).host;
return new URL(this.options.ingestPoint).hostname
}
getProjectKey(): string {
return this.projectKey
}
getBaseHref(): string {
if (this.options.resourceBaseHref) {
return this.options.resourceBaseHref
}
if (document.baseURI) {
return document.baseURI
}
// IE only
return document.head
?.getElementsByTagName("base")[0]
?.getAttribute("href") || location.origin + location.pathname
}
isServiceURL(url: string): boolean {
return url.startsWith(this.options.ingestPoint);
return url.startsWith(this.options.ingestPoint)
}
active(): boolean {
@ -211,10 +231,10 @@ export default class App {
}
private _start(reset: boolean): Promise<OnStartInfo> {
if (!this.isActive) {
this.isActive = true;
if (!this.worker) {
throw new Error("Stranger things: no worker found");
return Promise.reject("No worker found: perhaps, CSP is not set.");
}
this.isActive = true;
let pageNo: number = 0;
const pageNoStr = sessionStorage.getItem(this.options.session_pageno_key);
@ -273,7 +293,7 @@ export default class App {
this._sessionID = sessionID;
}
if (!this.worker) {
throw new Error("Stranger things: no worker found after start request");
throw new Error("no worker found after start request (this might not happen)");
}
this.worker.postMessage({ token, beaconSizeLimit });
this.startCallbacks.forEach((cb) => cb());
@ -289,6 +309,7 @@ export default class App {
})
.catch(e => {
this.stop();
warn("OpenReplay was unable to start. ", e)
this.sendDebugReport("session_start", e);
throw e;
})

View file

@ -1,4 +1,4 @@
import { stars, hasOpenreplayAttribute, getBaseURI } from '../utils';
import { stars, hasOpenreplayAttribute } from '../utils';
import {
CreateDocument,
CreateElementNode,
@ -10,37 +10,49 @@ import {
RemoveNodeAttribute,
MoveNode,
RemoveNode,
CreateIFrameDocument,
} from '../../messages';
import App from './index';
interface Window extends WindowProxy {
HTMLInputElement: typeof HTMLInputElement,
HTMLLinkElement: typeof HTMLLinkElement,
HTMLStyleElement: typeof HTMLStyleElement,
SVGStyleElement: typeof SVGStyleElement,
HTMLIFrameElement: typeof HTMLIFrameElement,
Text: typeof Text,
Element: typeof Element,
//parent: Window,
}
type WindowConstructor =
Document |
Element |
Text |
HTMLInputElement |
HTMLLinkElement |
HTMLStyleElement |
HTMLIFrameElement
// type ConstructorNames =
// 'Element' |
// 'Text' |
// 'HTMLInputElement' |
// 'HTMLLinkElement' |
// 'HTMLStyleElement' |
// 'HTMLIFrameElement'
type Constructor<T> = { new (...args: any[]): T , name: string };
function isSVGElement(node: Element): node is SVGElement {
return node.namespaceURI === 'http://www.w3.org/2000/svg';
}
function isIgnored(node: Node): boolean {
if (node instanceof Text) {
return false;
}
if (!(node instanceof Element)) {
return true;
}
const tag = node.tagName.toUpperCase();
if (tag === 'LINK') {
const rel = node.getAttribute('rel');
const as = node.getAttribute('as');
return !(rel?.includes('stylesheet') || as === "style" || as === "font");
}
return (
tag === 'SCRIPT' ||
tag === 'NOSCRIPT' ||
tag === 'META' ||
tag === 'TITLE' ||
tag === 'BASE'
);
}
export interface Options {
obscureTextEmails: boolean;
obscureTextNumbers: boolean;
captureIFrames: boolean;
}
export default class Observer {
@ -51,17 +63,33 @@ export default class Observer {
private readonly attributesList: Array<Set<string> | undefined>;
private readonly textSet: Set<number>;
private readonly textMasked: Set<number>;
private readonly options: Options;
constructor(private readonly app: App, opts: Options) {
this.options = opts;
constructor(private readonly app: App, private readonly options: Options, private readonly context: Window = window) {
this.observer = new MutationObserver(
this.app.safe((mutations) => {
for (const mutation of mutations) {
const target = mutation.target;
if (isIgnored(target) || !document.contains(target)) {
const type = mutation.type;
// Special case
// Document 'childList' might happen in case of iframe.
// TODO: generalize as much as possible
if (this.isInstance(target, Document)
&& type === 'childList'
//&& new Array(mutation.addedNodes).some(node => this.isInstance(node, HTMLHtmlElement))
) {
const parentFrame = target.defaultView?.frameElement
if (!parentFrame) { continue }
this.bindTree(target.documentElement)
const frameID = this.app.nodes.getID(parentFrame)
const docID = this.app.nodes.getID(target.documentElement)
if (frameID === undefined || docID === undefined) { continue }
this.app.send(CreateIFrameDocument(frameID, docID));
continue;
}
if (this.isIgnored(target) || !context.document.contains(target)) {
continue;
}
const type = mutation.type;
if (type === 'childList') {
for (let i = 0; i < mutation.removedNodes.length; i++) {
this.bindTree(mutation.removedNodes[i]);
@ -114,6 +142,43 @@ export default class Observer {
this.textMasked.clear();
}
// TODO: we need a type expert here so we won't have to ignore the lines
private isInstance<T extends WindowConstructor>(node: Node, constr: Constructor<T>): node is T {
let context = this.context;
while(context.parent && context.parent !== context) {
// @ts-ignore
if (node instanceof context[constr.name]) {
return true
}
// @ts-ignore
context = context.parent
}
// @ts-ignore
return node instanceof context[constr.name]
}
private isIgnored(node: Node): boolean {
if (this.isInstance(node, Text)) {
return false;
}
if (!this.isInstance(node, Element)) {
return true;
}
const tag = node.tagName.toUpperCase();
if (tag === 'LINK') {
const rel = node.getAttribute('rel');
const as = node.getAttribute('as');
return !(rel?.includes('stylesheet') || as === "style" || as === "font");
}
return (
tag === 'SCRIPT' ||
tag === 'NOSCRIPT' ||
tag === 'META' ||
tag === 'TITLE' ||
tag === 'BASE'
);
}
private sendNodeAttribute(
id: number,
node: Element,
@ -130,7 +195,7 @@ export default class Observer {
if (value.length > 1e5) {
value = '';
}
this.app.send(new SetNodeAttributeURLBased(id, name, value, getBaseURI()));
this.app.send(new SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref()));
} else {
this.app.send(new SetNodeAttribute(id, name, value));
}
@ -148,7 +213,7 @@ export default class Observer {
}
if (
name === 'value' &&
node instanceof HTMLInputElement &&
this.isInstance(node, HTMLInputElement) &&
node.type !== 'button' &&
node.type !== 'reset' &&
node.type !== 'submit'
@ -159,8 +224,8 @@ export default class Observer {
this.app.send(new RemoveNodeAttribute(id, name));
return;
}
if (name === 'style' || name === 'href' && node instanceof HTMLLinkElement) {
this.app.send(new SetNodeAttributeURLBased(id, name, value, getBaseURI()));
if (name === 'style' || name === 'href' && this.isInstance(node, HTMLLinkElement)) {
this.app.send(new SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref()));
return;
}
if (name === 'href' || value.length > 1e5) {
@ -170,8 +235,8 @@ export default class Observer {
}
private sendNodeData(id: number, parentElement: Element, data: string): void {
if (parentElement instanceof HTMLStyleElement || parentElement instanceof SVGStyleElement) {
this.app.send(new SetCSSDataURLBased(id, data, getBaseURI()));
if (this.isInstance(parentElement, HTMLStyleElement) || this.isInstance(parentElement, SVGStyleElement)) {
this.app.send(new SetCSSDataURLBased(id, data, this.app.getBaseHref()));
return;
}
if (this.textMasked.has(id)) {
@ -201,7 +266,7 @@ export default class Observer {
}
private bindTree(node: Node): void {
if (isIgnored(node)) {
if (this.isIgnored(node)) {
return;
}
this.bindNode(node);
@ -210,7 +275,7 @@ export default class Observer {
NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT,
{
acceptNode: (node) =>
isIgnored(node) || this.app.nodes.getID(node) !== undefined
this.isIgnored(node) || this.app.nodes.getID(node) !== undefined
? NodeFilter.FILTER_REJECT
: NodeFilter.FILTER_ACCEPT,
},
@ -231,7 +296,9 @@ export default class Observer {
private _commitNode(id: number, node: Node): boolean {
const parent = node.parentNode;
let parentID: number | undefined;
if (id !== 0) {
if (this.isInstance(node, HTMLHtmlElement)) {
this.indexes[id] = 0
} else {
if (parent === null) {
this.unbindNode(node);
return false;
@ -247,7 +314,7 @@ export default class Observer {
}
if (
this.textMasked.has(parentID) ||
(node instanceof Element && hasOpenreplayAttribute(node, 'masked'))
(this.isInstance(node, Element) && hasOpenreplayAttribute(node, 'masked'))
) {
this.textMasked.add(id);
}
@ -271,7 +338,7 @@ export default class Observer {
throw 'commitNode: missing node index';
}
if (isNew === true) {
if (node instanceof Element) {
if (this.isInstance(node, Element)) {
if (parentID !== undefined) {
this.app.send(new
CreateElementNode(
@ -287,7 +354,12 @@ export default class Observer {
const attr = node.attributes[i];
this.sendNodeAttribute(id, node, attr.nodeName, attr.value);
}
} else if (node instanceof Text) {
if (this.isInstance(node, HTMLIFrameElement) &&
(this.options.captureIFrames || node.getAttribute("data-openreplay-capture"))) {
this.handleIframe(node);
}
} else if (this.isInstance(node, Text)) {
// for text node id != 0, hence parentID !== undefined and parent is Element
this.app.send(new CreateTextNode(id, parentID as number, index));
this.sendNodeData(id, parent as Element, node.data);
@ -299,7 +371,7 @@ export default class Observer {
}
const attr = this.attributesList[id];
if (attr !== undefined) {
if (!(node instanceof Element)) {
if (!this.isInstance(node, Element)) {
throw 'commitNode: node is not an element';
}
for (const name of attr) {
@ -307,7 +379,7 @@ export default class Observer {
}
}
if (this.textSet.has(id)) {
if (!(node instanceof Text)) {
if (!this.isInstance(node, Text)) {
throw 'commitNode: node is not a text';
}
// for text node id != 0, hence parent is Element
@ -337,8 +409,44 @@ export default class Observer {
this.clear();
}
private iframeObservers: Observer[] = [];
private handleIframe(iframe: HTMLIFrameElement): void {
const handle = () => {
const context = iframe.contentWindow as Window | null
const id = this.app.nodes.getID(iframe)
if (!context || id === undefined) { return }
const observer = new Observer(this.app, this.options, context)
this.iframeObservers.push(observer)
observer.observeIframe(id, context)
}
this.app.attachEventListener(iframe, "load", handle)
handle()
}
// TODO: abstract common functionality, separate FrameObserver
private observeIframe(id: number, context: Window) {
const doc = context.document;
this.observer.observe(doc, {
childList: true,
attributes: true,
characterData: true,
subtree: true,
attributeOldValue: false,
characterDataOldValue: false,
});
this.bindTree(doc.documentElement);
const docID = this.app.nodes.getID(doc.documentElement);
if (docID === undefined) {
console.log("Wrong")
return;
}
this.app.send(CreateIFrameDocument(id,docID));
this.commitNodes();
}
observe(): void {
this.observer.observe(document, {
this.observer.observe(this.context.document, {
childList: true,
attributes: true,
characterData: true,
@ -347,11 +455,13 @@ export default class Observer {
characterDataOldValue: false,
});
this.app.send(new CreateDocument());
this.bindTree(document.documentElement);
this.bindTree(this.context.document.documentElement);
this.commitNodes();
}
disconnect(): void {
this.iframeObservers.forEach(o => o.disconnect());
this.iframeObservers = [];
this.observer.disconnect();
this.clear();
}

View file

@ -1,6 +1,5 @@
import App from '../app';
import { CSSInsertRuleURLBased, CSSDeleteRule, TechnicalInfo } from '../../messages';
import { getBaseURI } from '../utils';
export default function(app: App | null) {
if (app === null) {
@ -14,7 +13,7 @@ export default function(app: App | null) {
const processOperation = app.safe(
(stylesheet: CSSStyleSheet, index: number, rule?: string) => {
const sendMessage = typeof rule === 'string'
? (nodeID: number) => app.send(new CSSInsertRuleURLBased(nodeID, rule, index, getBaseURI()))
? (nodeID: number) => app.send(new CSSInsertRuleURLBased(nodeID, rule, index, app.getBaseHref()))
: (nodeID: number) => app.send(new CSSDeleteRule(nodeID, index));
// TODO: Extend messages to maintain nested rules (CSSGroupingRule prototype, as well as CSSKeyframesRule)
if (stylesheet.ownerNode == null) {

View file

@ -1,4 +1,4 @@
import { timestamp, isURL, getBaseURI } from '../utils';
import { timestamp, isURL } from '../utils';
import App from '../app';
import { ResourceTiming, SetNodeAttributeURLBased } from '../../messages';
@ -17,7 +17,7 @@ export default function (app: App): void {
app.send(new ResourceTiming(timestamp(), 0, 0, 0, 0, 0, src, 'img'));
}
} else if (src.length < 1e5) {
app.send(new SetNodeAttributeURLBased(id, 'src', src, getBaseURI()));
app.send(new SetNodeAttributeURLBased(id, 'src', src, app.getBaseHref()));
}
});
@ -30,7 +30,7 @@ export default function (app: App): void {
return;
}
const src = target.src;
app.send(new SetNodeAttributeURLBased(id, 'src', src, getBaseURI()));
app.send(new SetNodeAttributeURLBased(id, 'src', src, app.getBaseHref()));
}
}
});

View file

@ -72,14 +72,23 @@ function getTargetLabel(target: Element): string {
return '';
}
interface HeatmapsOptions {
finder: FinderOptions,
}
export interface Options {
selectorFinder: boolean | FinderOptions;
heatmaps: boolean | HeatmapsOptions;
}
export default function (app: App, opts: Partial<Options>): void {
const options: Options = Object.assign(
{
selectorFinder: true,
heatmaps: {
finder: {
threshold: 5,
maxNumberOfTries: 600,
},
},
},
opts,
);
@ -106,9 +115,9 @@ export default function (app: App, opts: Partial<Options>): void {
const selectorMap: {[id:number]: string} = {};
function getSelector(id: number, target: Element): string {
if (options.selectorFinder === false) { return '' }
if (options.heatmaps === false) { return '' }
return selectorMap[id] = selectorMap[id] ||
finder(target, options.selectorFinder === true ? undefined : options.selectorFinder);
finder(target, options.heatmaps === true ? undefined : options.heatmaps.finder);
}
app.attachEventListener(

View file

@ -16,16 +16,6 @@ export function isURL(s: string): boolean {
return s.substr(0, 8) === 'https://' || s.substr(0, 7) === 'http://';
}
export function getBaseURI(): string {
if (document.baseURI) {
return document.baseURI;
}
// IE only
return document.head
?.getElementsByTagName("base")[0]
?.getAttribute("href") || location.origin + location.pathname;
}
export const IN_BROWSER = !(typeof window === "undefined");
export const log = console.log

View file

@ -1,12 +0,0 @@
export declare type Options = {
root: Element;
idName: (name: string) => boolean;
className: (name: string) => boolean;
tagName: (name: string) => boolean;
attr: (name: string, value: string) => boolean;
seedMinLength: number;
optimizedMinLength: number;
threshold: number;
maxNumberOfTries: number;
};
export declare function finder(input: Element, options?: Partial<Options>): string;

View file

@ -1,339 +0,0 @@
var Limit;
(function (Limit) {
Limit[Limit["All"] = 0] = "All";
Limit[Limit["Two"] = 1] = "Two";
Limit[Limit["One"] = 2] = "One";
})(Limit || (Limit = {}));
let config;
let rootDocument;
export function finder(input, options) {
if (input.nodeType !== Node.ELEMENT_NODE) {
throw new Error(`Can't generate CSS selector for non-element node type.`);
}
if ("html" === input.tagName.toLowerCase()) {
return "html";
}
const defaults = {
root: document.body,
idName: (name) => true,
className: (name) => true,
tagName: (name) => true,
attr: (name, value) => false,
seedMinLength: 1,
optimizedMinLength: 2,
threshold: 1000,
maxNumberOfTries: 10000,
};
config = Object.assign(Object.assign({}, defaults), options);
rootDocument = findRootDocument(config.root, defaults);
let path = bottomUpSearch(input, Limit.All, () => bottomUpSearch(input, Limit.Two, () => bottomUpSearch(input, Limit.One)));
if (path) {
const optimized = sort(optimize(path, input));
if (optimized.length > 0) {
path = optimized[0];
}
return selector(path);
}
else {
throw new Error(`Selector was not found.`);
}
}
function findRootDocument(rootNode, defaults) {
if (rootNode.nodeType === Node.DOCUMENT_NODE) {
return rootNode;
}
if (rootNode === defaults.root) {
return rootNode.ownerDocument;
}
return rootNode;
}
function bottomUpSearch(input, limit, fallback) {
let path = null;
let stack = [];
let current = input;
let i = 0;
while (current && current !== config.root.parentElement) {
let level = maybe(id(current)) || maybe(...attr(current)) || maybe(...classNames(current)) || maybe(tagName(current)) || [any()];
const nth = index(current);
if (limit === Limit.All) {
if (nth) {
level = level.concat(level.filter(dispensableNth).map(node => nthChild(node, nth)));
}
}
else if (limit === Limit.Two) {
level = level.slice(0, 1);
if (nth) {
level = level.concat(level.filter(dispensableNth).map(node => nthChild(node, nth)));
}
}
else if (limit === Limit.One) {
const [node] = level = level.slice(0, 1);
if (nth && dispensableNth(node)) {
level = [nthChild(node, nth)];
}
}
for (let node of level) {
node.level = i;
}
stack.push(level);
if (stack.length >= config.seedMinLength) {
path = findUniquePath(stack, fallback);
if (path) {
break;
}
}
current = current.parentElement;
i++;
}
if (!path) {
path = findUniquePath(stack, fallback);
}
return path;
}
function findUniquePath(stack, fallback) {
const paths = sort(combinations(stack));
if (paths.length > config.threshold) {
return fallback ? fallback() : null;
}
for (let candidate of paths) {
if (unique(candidate)) {
return candidate;
}
}
return null;
}
function selector(path) {
let node = path[0];
let query = node.name;
for (let i = 1; i < path.length; i++) {
const level = path[i].level || 0;
if (node.level === level - 1) {
query = `${path[i].name} > ${query}`;
}
else {
query = `${path[i].name} ${query}`;
}
node = path[i];
}
return query;
}
function penalty(path) {
return path.map(node => node.penalty).reduce((acc, i) => acc + i, 0);
}
function unique(path) {
switch (rootDocument.querySelectorAll(selector(path)).length) {
case 0:
throw new Error(`Can't select any node with this selector: ${selector(path)}`);
case 1:
return true;
default:
return false;
}
}
function id(input) {
const elementId = input.getAttribute("id");
if (elementId && config.idName(elementId)) {
return {
name: "#" + cssesc(elementId, { isIdentifier: true }),
penalty: 0,
};
}
return null;
}
function attr(input) {
const attrs = Array.from(input.attributes).filter((attr) => config.attr(attr.name, attr.value));
return attrs.map((attr) => ({
name: "[" + cssesc(attr.name, { isIdentifier: true }) + "=\"" + cssesc(attr.value) + "\"]",
penalty: 0.5
}));
}
function classNames(input) {
const names = Array.from(input.classList)
.filter(config.className);
return names.map((name) => ({
name: "." + cssesc(name, { isIdentifier: true }),
penalty: 1
}));
}
function tagName(input) {
const name = input.tagName.toLowerCase();
if (config.tagName(name)) {
return {
name,
penalty: 2
};
}
return null;
}
function any() {
return {
name: "*",
penalty: 3
};
}
function index(input) {
const parent = input.parentNode;
if (!parent) {
return null;
}
let child = parent.firstChild;
if (!child) {
return null;
}
let i = 0;
while (child) {
if (child.nodeType === Node.ELEMENT_NODE) {
i++;
}
if (child === input) {
break;
}
child = child.nextSibling;
}
return i;
}
function nthChild(node, i) {
return {
name: node.name + `:nth-child(${i})`,
penalty: node.penalty + 1
};
}
function dispensableNth(node) {
return node.name !== "html" && !node.name.startsWith("#");
}
function maybe(...level) {
const list = level.filter(notEmpty);
if (list.length > 0) {
return list;
}
return null;
}
function notEmpty(value) {
return value !== null && value !== undefined;
}
function* combinations(stack, path = []) {
if (stack.length > 0) {
for (let node of stack[0]) {
yield* combinations(stack.slice(1, stack.length), path.concat(node));
}
}
else {
yield path;
}
}
function sort(paths) {
return Array.from(paths).sort((a, b) => penalty(a) - penalty(b));
}
function* optimize(path, input, scope = {
counter: 0,
visited: new Map()
}) {
if (path.length > 2 && path.length > config.optimizedMinLength) {
for (let i = 1; i < path.length - 1; i++) {
if (scope.counter > config.maxNumberOfTries) {
return; // Okay At least I tried!
}
scope.counter += 1;
const newPath = [...path];
newPath.splice(i, 1);
const newPathKey = selector(newPath);
if (scope.visited.has(newPathKey)) {
return;
}
if (unique(newPath) && same(newPath, input)) {
yield newPath;
scope.visited.set(newPathKey, true);
yield* optimize(newPath, input, scope);
}
}
}
}
function same(path, input) {
return rootDocument.querySelector(selector(path)) === input;
}
const regexAnySingleEscape = /[ -,\.\/:-@\[-\^`\{-~]/;
const regexSingleEscape = /[ -,\.\/:-@\[\]\^`\{-~]/;
const regexExcessiveSpaces = /(^|\\+)?(\\[A-F0-9]{1,6})\x20(?![a-fA-F0-9\x20])/g;
const defaultOptions = {
"escapeEverything": false,
"isIdentifier": false,
"quotes": "single",
"wrap": false
};
function cssesc(string, opt = {}) {
const options = Object.assign(Object.assign({}, defaultOptions), opt);
if (options.quotes != "single" && options.quotes != "double") {
options.quotes = "single";
}
const quote = options.quotes == "double" ? "\"" : "'";
const isIdentifier = options.isIdentifier;
const firstChar = string.charAt(0);
let output = "";
let counter = 0;
const length = string.length;
while (counter < length) {
const character = string.charAt(counter++);
let codePoint = character.charCodeAt(0);
let value = void 0;
// If its not a printable ASCII character…
if (codePoint < 0x20 || codePoint > 0x7E) {
if (codePoint >= 0xD800 && codePoint <= 0xDBFF && counter < length) {
// Its a high surrogate, and there is a next character.
const extra = string.charCodeAt(counter++);
if ((extra & 0xFC00) == 0xDC00) {
// next character is low surrogate
codePoint = ((codePoint & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000;
}
else {
// Its an unmatched surrogate; only append this code unit, in case
// the next code unit is the high surrogate of a surrogate pair.
counter--;
}
}
value = "\\" + codePoint.toString(16).toUpperCase() + " ";
}
else {
if (options.escapeEverything) {
if (regexAnySingleEscape.test(character)) {
value = "\\" + character;
}
else {
value = "\\" + codePoint.toString(16).toUpperCase() + " ";
}
}
else if (/[\t\n\f\r\x0B]/.test(character)) {
value = "\\" + codePoint.toString(16).toUpperCase() + " ";
}
else if (character == "\\" || !isIdentifier && (character == "\"" && quote == character || character == "'" && quote == character) || isIdentifier && regexSingleEscape.test(character)) {
value = "\\" + character;
}
else {
value = character;
}
}
output += value;
}
if (isIdentifier) {
if (/^-[-\d]/.test(output)) {
output = "\\-" + output.slice(1);
}
else if (/\d/.test(firstChar)) {
output = "\\3" + firstChar + " " + output.slice(1);
}
}
// Remove spaces after `\HEX` escapes that are not followed by a hex digit,
// since theyre redundant. Note that this is only possible if the escape
// sequence isnt preceded by an odd number of backslashes.
output = output.replace(regexExcessiveSpaces, function ($0, $1, $2) {
if ($1 && $1.length % 2) {
// Its not safe to remove the space, so dont.
return $0;
}
// Strip the space.
return ($1 || "") + $2;
});
if (!isIdentifier && options.wrap) {
return quote + output + quote;
}
return output;
}

View file

@ -279,14 +279,16 @@ function notEmpty<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined
}
function* combinations(stack: Node[][], path: Node[] = []): Generator<Node[]> {
function combinations(stack: Node[][], path: Node[] = []): Node[][] {
const paths: Node[][] = []
if (stack.length > 0) {
for (let node of stack[0]) {
yield* combinations(stack.slice(1, stack.length), path.concat(node))
paths.push(...combinations(stack.slice(1, stack.length), path.concat(node)))
}
} else {
yield path
paths.push(path)
}
return paths
}
function sort(paths: Iterable<Path>): Path[] {
@ -298,29 +300,31 @@ type Scope = {
visited: Map<string, boolean>
}
function* optimize(path: Path, input: Element, scope: Scope = {
function optimize(path: Path, input: Element, scope: Scope = {
counter: 0,
visited: new Map<string, boolean>()
}): Generator<Node[]> {
}): Node[][] {
const paths: Node[][] = []
if (path.length > 2 && path.length > config.optimizedMinLength) {
for (let i = 1; i < path.length - 1; i++) {
if (scope.counter > config.maxNumberOfTries) {
return // Okay At least I tried!
return paths // Okay At least I tried!
}
scope.counter += 1
const newPath = [...path]
newPath.splice(i, 1)
const newPathKey = selector(newPath)
if (scope.visited.has(newPathKey)) {
return
return paths
}
if (unique(newPath) && same(newPath, input)) {
yield newPath
paths.push(newPath)
scope.visited.set(newPathKey, true)
yield* optimize(newPath, input, scope)
paths.push(...optimize(newPath, input, scope))
}
}
}
return paths
}
function same(path: Path, input: Element) {

View file

@ -885,3 +885,19 @@ export const MouseClick = bindNew(_MouseClick);
classes.set(69, MouseClick);
class _CreateIFrameDocument implements Message {
readonly _id: number = 70;
constructor(
public frameID: number,
public id: number
) {}
encode(writer: Writer): boolean {
return writer.uint(70) &&
writer.uint(this.frameID) &&
writer.uint(this.id);
}
}
export const CreateIFrameDocument = bindNew(_CreateIFrameDocument);
classes.set(70, CreateIFrameDocument);