happy-dom
Version:
Happy DOM is a JavaScript implementation of a web browser without its graphical user interface. It includes many web standards from WHATWG DOM and HTML.
382 lines (353 loc) • 9.77 kB
text/typescript
import BrowserErrorCaptureEnum from '../browser/enums/BrowserErrorCaptureEnum.js';
import type BrowserWindow from '../window/BrowserWindow.js';
import WindowBrowserContext from '../window/WindowBrowserContext.js';
import * as PropertySymbol from '../PropertySymbol.js';
import type IJavaScriptCompiledResult from './IJavaScriptCompiledResult.js';
/**
* Statement regexp.
*
* Group 1: Dynamic import function call.
*/
const STATEMENT_REGEXP = /import\s*\(([^)]+)\)/gm;
/**
* Syntax regexp.
*
* Group 1: Slash (RegExp or comment).
* Group 2: Parentheses.
* Group 3: Curly braces.
* Group 4: Square brackets.
* Group 5: Escape template string (${).
* Group 6: Template string apostrophe (`).
* Group 7: String apostrophe (').
* Group 8: String apostrophe (").
* Group 9: Line feed character.
*/
const SYNTAX_REGEXP = /(\/)|(\(|\))|({|})|(\[|\])|(\${)|(`)|(')|(")|(\n)/gm;
/**
* Valid preceding token before a statement.
*/
const PRECEDING_STATEMENT_TOKEN_REGEXP = /['"`(){}\s;=><\[\]+-,:&]/;
/**
* Valid preceding token before a regexp.
*/
const PRECEDING_REGEXP_TOKEN_REGEXP = /['"`({};=><\[+-,:&]/;
/**
* ECMAScript module compiler.
*/
export default class JavaScriptCompiler {
public readonly window: BrowserWindow;
private count = {
comment: 0,
singleLineComment: 0,
parentheses: 0,
curlyBraces: 0,
squareBrackets: 0,
regExp: 0,
regExpSquareBrackets: 0,
escapeTemplateString: 0,
simpleString: 0,
doubleString: 0
};
private templateString: number[] = [];
/**
* Constructor.
*
* @param window Window.
* @param url Module URL.
*/
constructor(window: BrowserWindow) {
this.window = window;
}
/**
* Compiles code.
*
* @param sourceURL Source URL.
* @param code Code.
* @returns Result.
*/
public compile(sourceURL: string, code: string): IJavaScriptCompiledResult {
const browserSettings = new WindowBrowserContext(this.window).getSettings();
if (!browserSettings) {
return { execute: () => {} };
}
const regExp = new RegExp(STATEMENT_REGEXP);
const count = this.count;
let newCode = '(function anonymous($happy_dom) {';
let match: RegExpExecArray | null = null;
let precedingToken: string;
let textBetweenStatements: string;
let lastIndex = 0;
const importStartIndex = -1;
if (
!browserSettings.disableErrorCapturing &&
browserSettings.errorCapture === BrowserErrorCaptureEnum.tryAndCatch
) {
newCode += 'try {';
}
while ((match = regExp.exec(code))) {
precedingToken = code[match.index - 1] || ' ';
textBetweenStatements = code.substring(lastIndex, match.index);
this.parseSyntax(textBetweenStatements, lastIndex);
if (importStartIndex === -1) {
newCode += textBetweenStatements;
}
if (
match[1] &&
count.simpleString === 0 &&
count.doubleString === 0 &&
count.comment === 0 &&
count.singleLineComment === 0 &&
count.regExp === 0 &&
(this.templateString.length === 0 || this.templateString[0] > 0) &&
PRECEDING_STATEMENT_TOKEN_REGEXP.test(precedingToken)
) {
// Dynamic import function call
newCode += `$happy_dom.dynamicImport(${match[1]})`;
} else {
// No valid statement found
// This happens when there is a statement inside a string or comment
// E.g. "const str = 'import defaultExport from invalid;';"
newCode += match[0];
this.parseSyntax(match[0], match.index);
}
lastIndex = regExp.lastIndex;
}
if (lastIndex === 0) {
newCode += code;
} else {
newCode += code.substring(lastIndex);
}
if (
!browserSettings.disableErrorCapturing &&
browserSettings.errorCapture === BrowserErrorCaptureEnum.tryAndCatch
) {
newCode += '} catch (error) { $happy_dom.dispatchError(error); }';
}
newCode += '})';
try {
return {
execute: this.window[PropertySymbol.evaluateScript](newCode, {
filename: sourceURL
})
};
} catch (error) {
(<Error>error).message =
`Failed to parse JavaScript in '${sourceURL}': ${(<Error>error).message}`;
if (
browserSettings.disableErrorCapturing ||
browserSettings.errorCapture !== BrowserErrorCaptureEnum.tryAndCatch
) {
throw error;
} else {
this.window[PropertySymbol.dispatchError](<Error>error);
return {
execute: () => {}
};
}
}
}
/**
* Parses syntax.
*
* @param code Code.
* @param index Index.
*/
private parseSyntax(code: string, index: number): void {
const regExp = new RegExp(SYNTAX_REGEXP);
const count = this.count;
let match: RegExpExecArray | null = null;
let precedingToken: string;
let isEscaped: boolean;
index++;
while ((match = regExp.exec(code))) {
precedingToken = code[match.index - 1] || ' ';
isEscaped = precedingToken === '\\' && code[match.index - 2] !== '\\';
if (match[1]) {
// Slash (RegExp or Comment)
if (
count.simpleString === 0 &&
count.doubleString === 0 &&
count.singleLineComment === 0 &&
count.regExpSquareBrackets === 0 &&
(this.templateString.length === 0 || this.templateString[0] > 0)
) {
if (count.comment === 1) {
if (precedingToken === '*') {
count.comment = 0;
}
} else {
if (count.regExp === 0) {
if (code[match.index + 1] === '*') {
count.comment = 1;
} else if (code[match.index + 1] === '/') {
count.singleLineComment = 1;
} else {
if (
PRECEDING_REGEXP_TOKEN_REGEXP.test(
this.getNonSpacePrecedingToken(code, match.index)
)
) {
count.regExp = 1;
}
}
} else if (!isEscaped) {
count.regExp = 0;
}
}
}
} else if (match[2]) {
// Parentheses
if (
count.simpleString === 0 &&
count.doubleString === 0 &&
count.regExp === 0 &&
count.comment === 0 &&
count.singleLineComment === 0 &&
(this.templateString.length === 0 || this.templateString[0] > 0)
) {
if (match[2] === '(') {
count.parentheses++;
} else if (match[2] === ')' && count.parentheses > 0) {
count.parentheses--;
}
}
} else if (match[3]) {
// Curly braces
if (
count.simpleString === 0 &&
count.doubleString === 0 &&
count.regExp === 0 &&
count.comment === 0 &&
count.singleLineComment === 0 &&
(this.templateString.length === 0 || this.templateString[0] > 0)
) {
if (match[3] === '{') {
if (this.templateString.length) {
this.templateString[0]++;
}
count.curlyBraces++;
} else if (match[3] === '}') {
if (this.templateString.length && this.templateString[0] > 0) {
this.templateString[0]--;
}
if (count.curlyBraces > 0) {
count.curlyBraces--;
}
}
}
} else if (match[4]) {
// Square brackets
if (
count.simpleString === 0 &&
count.doubleString === 0 &&
count.comment === 0 &&
count.singleLineComment === 0 &&
(this.templateString.length === 0 || this.templateString[0] > 0)
) {
// We need to check for square brackets in RegExp as well to know when the RegExp ends
if (count.regExp === 1) {
if (!isEscaped) {
if (match[4] === '[' && count.regExpSquareBrackets === 0) {
count.regExpSquareBrackets = 1;
} else if (match[4] === ']' && count.regExpSquareBrackets === 1) {
count.regExpSquareBrackets = 0;
}
}
} else {
if (match[4] === '[') {
count.squareBrackets++;
} else if (match[4] === ']' && count.squareBrackets > 0) {
count.squareBrackets--;
}
}
}
} else if (match[5]) {
// Escape template string (${)
if (
count.simpleString === 0 &&
count.doubleString === 0 &&
count.comment === 0 &&
count.singleLineComment === 0 &&
count.regExp === 0 &&
!isEscaped
) {
if (this.templateString[0] === 0) {
this.templateString[0] = 1;
count.curlyBraces++;
}
}
} else if (match[6]) {
// Template string
if (
count.simpleString === 0 &&
count.doubleString === 0 &&
count.comment === 0 &&
count.singleLineComment === 0 &&
count.regExp === 0 &&
!isEscaped
) {
if (this.templateString?.[0] === 0) {
this.templateString.shift();
} else {
this.templateString.unshift(0);
}
}
} else if (match[7]) {
// String apostrophe (')
if (
count.doubleString === 0 &&
count.comment === 0 &&
count.singleLineComment === 0 &&
count.regExp === 0 &&
!isEscaped &&
(this.templateString.length === 0 || this.templateString[0] > 0)
) {
if (count.simpleString === 0) {
count.simpleString = 1;
} else {
count.simpleString = 0;
}
}
} else if (match[8]) {
// String apostrophe (")
if (
count.simpleString === 0 &&
count.comment === 0 &&
count.singleLineComment === 0 &&
count.regExp === 0 &&
!isEscaped &&
(this.templateString.length === 0 || this.templateString[0] > 0)
) {
if (count.doubleString === 0) {
count.doubleString = 1;
} else {
count.doubleString = 0;
}
}
} else if (match[9]) {
// Line feed character
count.singleLineComment = 0;
}
}
}
/**
* Get the non-space preceding token.
*
* @param code Code.
* @param index Index.
* @returns Non-space preceding token.
*/
private getNonSpacePrecedingToken(code: string, index: number): string {
index--;
let nonSpacePrecedingToken = code[index];
while (
nonSpacePrecedingToken === ' ' ||
nonSpacePrecedingToken === '\n' ||
nonSpacePrecedingToken === '\t'
) {
index--;
nonSpacePrecedingToken = code[index];
}
return nonSpacePrecedingToken || ';';
}
}